work on system file tests
authorBen Pfaff <blp@cs.stanford.edu>
Sun, 8 Jun 2025 16:31:18 +0000 (09:31 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Sun, 8 Jun 2025 16:31:18 +0000 (09:31 -0700)
18 files changed:
rust/pspp/src/dictionary.rs
rust/pspp/src/output/mod.rs
rust/pspp/src/output/pivot/test.rs
rust/pspp/src/output/spv.rs
rust/pspp/src/output/text.rs
rust/pspp/src/sys/sack.rs
rust/pspp/src/sys/test.rs
rust/pspp/src/sys/testdata/documents.expected [new file with mode: 0644]
rust/pspp/src/sys/testdata/documents.sack [new file with mode: 0644]
rust/pspp/src/sys/testdata/unspecified_number_of_variable_positions.expected [new file with mode: 0644]
rust/pspp/src/sys/testdata/unspecified_number_of_variable_positions.sack [new file with mode: 0644]
rust/pspp/src/sys/testdata/value_labels.expected [new file with mode: 0644]
rust/pspp/src/sys/testdata/value_labels.sack [new file with mode: 0644]
rust/pspp/src/sys/testdata/variable_labels_and_missing_values.expected [new file with mode: 0644]
rust/pspp/src/sys/testdata/variable_labels_and_missing_values.sack [new file with mode: 0644]
rust/pspp/src/sys/testdata/wrong_variable_positions_but_v13.expected [new file with mode: 0644]
rust/pspp/src/sys/testdata/wrong_variable_positions_but_v13.sack [new file with mode: 0644]
rust/pspp/tests/sack.rs

index 0a759ec1ffefd07e81c567f2df22fc3776f63d47..38a22b1ae5ee5adf76ccf71ba60cea4cb2eaceff 100644 (file)
@@ -570,6 +570,14 @@ impl Dictionary {
             None => values.push(Value::empty()),
         }
 
+        group.push("Documents");
+        values.push(Value::new_user_text(
+            self.documents
+                .iter()
+                .flat_map(|s| [s.as_str(), "\n"])
+                .collect::<String>(),
+        ));
+
         (group, values)
     }
 }
index 4ff8d3bea40dc1fe00e7f172f8a224b3f1591421..ff054b9e4619a7075cb5da259d49fc1d3c067928 100644 (file)
@@ -44,12 +44,13 @@ pub struct Item {
 }
 
 impl Item {
-    pub fn new(details: Details) -> Self {
+    pub fn new(details: impl Into<Details>) -> Self {
+        let details = details.into();
         Self {
             label: None,
             command_name: details.command_name().cloned(),
             show: true,
-            details,
+            details: details,
         }
     }
 
@@ -61,11 +62,20 @@ impl Item {
     }
 }
 
+impl<T> From<T> for Item
+where
+    T: Into<Details>,
+{
+    fn from(value: T) -> Self {
+        Self::new(value)
+    }
+}
+
 pub enum Details {
     Chart,
     Image,
     Group(Vec<Arc<Item>>),
-    Message(Diagnostic),
+    Message(Box<Diagnostic>),
     PageBreak,
     Table(Box<PivotTable>),
     Text(Box<Text>),
@@ -111,21 +121,63 @@ impl Details {
     }
 }
 
+impl From<Diagnostic> for Details {
+    fn from(value: Diagnostic) -> Self {
+        Self::Message(Box::new(value))
+    }
+}
+
+impl From<Box<Diagnostic>> for Details {
+    fn from(value: Box<Diagnostic>) -> Self {
+        Self::Message(value)
+    }
+}
+
+impl From<PivotTable> for Details {
+    fn from(value: PivotTable) -> Self {
+        Self::Table(Box::new(value))
+    }
+}
+
+impl From<Box<PivotTable>> for Details {
+    fn from(value: Box<PivotTable>) -> Self {
+        Self::Table(value)
+    }
+}
+
+impl From<Text> for Details {
+    fn from(value: Text) -> Self {
+        Self::Text(Box::new(value))
+    }
+}
+
+impl From<Box<Text>> for Details {
+    fn from(value: Box<Text>) -> Self {
+        Self::Text(value)
+    }
+}
+
 pub struct Text {
     type_: TextType,
 
     content: Value,
 }
 
-impl From<&Diagnostic> for Text {
-    fn from(value: &Diagnostic) -> Self {
-        Text {
+impl Text {
+    pub fn new_log(s: impl Into<String>) -> Self {
+        Self {
             type_: TextType::Log,
-            content: Value::new_user_text(value.to_string()),
+            content: Value::new_user_text(s),
         }
     }
 }
 
+impl From<&Diagnostic> for Text {
+    fn from(value: &Diagnostic) -> Self {
+        Self::new_log(value.to_string())
+    }
+}
+
 pub enum TextType {
     /// `TITLE` and `SUBTITLE` commands.
     PageTitle,
index a867cdd80e765b3c99b1ddc82b63b2c8d3c01b26..9c55855e8606c8d23284ea589636b0aecf6fce51 100644 (file)
@@ -1,4 +1,4 @@
-use std::{fs::File, path::Path, sync::Arc};
+use std::{fmt::Display, fs::File, path::Path, sync::Arc};
 
 use enum_map::EnumMap;
 
@@ -123,19 +123,38 @@ fn d2(title: &str, axes: [Axis3; 2], dimension_labels: Option<LabelPosition>) ->
 }
 
 #[track_caller]
-pub fn assert_rendering(name: &str, pivot_table: &PivotTable, expected: &str) {
-    let actual = pivot_table.to_string();
-    if actual != expected {
-        eprintln!("Unexpected pivot table rendering:\n--- expected\n+++ actual");
+pub fn assert_lines_eq<E, A>(expected: &str, expected_name: E, actual: &str, actual_name: A)
+where
+    E: Display,
+    A: Display,
+{
+    if expected != actual {
+        eprintln!("Unexpected output:\n--- {expected_name}\n+++ {actual_name}");
         for result in diff::lines(expected, &actual) {
-            match result {
-                diff::Result::Left(line) => eprintln!("-{line:?}"),
-                diff::Result::Both(line, _) => eprintln!(" {line:?}"),
-                diff::Result::Right(line) => eprintln!("+{line:?}"),
-            }
+            let (prefix, line) = match result {
+                diff::Result::Left(line) => ('-', line),
+                diff::Result::Both(line, _) => (' ', line),
+                diff::Result::Right(line) => ('+', line),
+            };
+            let suffix = if line.trim_end().len() != line.len() {
+                "$"
+            } else {
+                ""
+            };
+            eprintln!("{prefix}{line}{suffix}");
         }
         panic!();
     }
+}
+
+#[track_caller]
+pub fn assert_rendering(name: &str, pivot_table: &PivotTable, expected: &str) {
+    assert_lines_eq(
+        expected,
+        format!("{name} expected"),
+        &pivot_table.to_string(),
+        format!("{name} actual"),
+    );
 
     let item = Arc::new(Item::new(Details::Table(Box::new(pivot_table.clone()))));
     if let Some(dir) = std::env::var_os("PSPP_TEST_HTML_DIR") {
index 9635655355e1bd750a5aa0639e45f2b92b88d65e..96345c93a28ee7eea0e9818e0b4d273e13d8e40a 100644 (file)
@@ -166,7 +166,7 @@ where
                     .unwrap();
             }
             super::Details::Message(diagnostic) => {
-                self.write_text(item, &diagnostic.into(), structure)
+                self.write_text(item, &Text::from(diagnostic.as_ref()), structure)
             }
             super::Details::PageBreak => {
                 self.needs_page_break = true;
index 538d80d50a5ce77f290290a3bec4c588df23bee2..c29fad05451836f884186ab72ba4f0593b48cd7b 100644 (file)
@@ -1,8 +1,8 @@
 use std::{
     borrow::Cow,
-    fmt::Display,
+    fmt::{Display, Error as FmtError, Result as FmtResult, Write as FmtWrite},
     fs::File,
-    io::{BufWriter, Write},
+    io::{BufWriter, Write as IoWrite},
     ops::{Index, Range},
     sync::{Arc, LazyLock},
 };
@@ -326,10 +326,13 @@ impl<'a> DisplayPivotTable<'a> {
 
 impl Display for DisplayPivotTable<'_> {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        for line in TextRenderer::default().render(self.pt) {
-            writeln!(f, "{}", line)?;
-        }
-        Ok(())
+        TextRenderer::default().render_table(self.pt, f)
+    }
+}
+
+impl Display for Item {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        TextRenderer::default().render(self, f)
     }
 }
 
@@ -348,20 +351,47 @@ impl TextDriver {
 }
 
 impl TextRenderer {
-    fn render(&mut self, table: &PivotTable) -> Vec<TextLine> {
-        let mut output = Vec::new();
+    fn render<W>(&mut self, item: &Item, writer: &mut W) -> FmtResult
+    where
+        W: FmtWrite,
+    {
+        match &item.details {
+            Details::Chart => todo!(),
+            Details::Image => todo!(),
+            Details::Group(children) => {
+                for (index, child) in children.iter().enumerate() {
+                    if index > 0 {
+                        writeln!(writer)?;
+                    }
+                    self.render(child, writer)?;
+                }
+                Ok(())
+            }
+            Details::Message(_diagnostic) => todo!(),
+            Details::PageBreak => Ok(()),
+            Details::Table(pivot_table) => self.render_table(&*pivot_table, writer),
+            Details::Text(_text) => todo!(),
+        }
+    }
+
+    fn render_table<W>(&mut self, table: &PivotTable, writer: &mut W) -> FmtResult
+    where
+        W: FmtWrite,
+    {
         for (index, layer_indexes) in table.layers(true).enumerate() {
             if index > 0 {
-                output.push(TextLine::new());
+                writeln!(writer)?;
             }
 
             let mut pager = Pager::new(self, table, Some(layer_indexes.as_slice()));
             while pager.has_next(self) {
                 pager.draw_next(self, usize::MAX);
-                output.append(&mut self.lines);
+                for line in self.lines.drain(..) {
+                    writeln!(writer, "{}", line)?;
+                }
             }
         }
-        output
+        Ok(())
     }
 
     fn layout_cell(&self, text: &str, bb: Rect2) -> Coord2 {
@@ -472,19 +502,18 @@ impl Driver for TextDriver {
     }
 
     fn write(&mut self, item: &Arc<Item>) {
-        match &item.details {
-            Details::Chart => todo!(),
-            Details::Image => todo!(),
-            Details::Group(_) => todo!(),
-            Details::Message(_diagnostic) => todo!(),
-            Details::PageBreak => (),
-            Details::Table(pivot_table) => {
-                for line in self.renderer.render(pivot_table) {
-                    writeln!(self.file, "{}", line.str()).unwrap();
-                }
-            }
-            Details::Text(_text) => todo!(),
-        }
+        let _ = self.renderer.render(item, &mut FmtAdapter(&mut self.file));
+    }
+}
+
+struct FmtAdapter<W>(W);
+
+impl<W> FmtWrite for FmtAdapter<W>
+where
+    W: IoWrite,
+{
+    fn write_str(&mut self, s: &str) -> FmtResult {
+        self.0.write_all(s.as_bytes()).map_err(|_| FmtError)
     }
 }
 
index b2ac013beb51acba4c68b1b5a6c5311fe2bffe31..4620c4c9a8ac15875908e3b2cdafa2d7d51df71f 100644 (file)
@@ -6,6 +6,7 @@ use std::{
     error::Error as StdError,
     fmt::{Display, Formatter, Result as FmtResult},
     iter::repeat_n,
+    path::{Path, PathBuf},
 };
 
 use crate::endian::{Endian, ToBytes};
@@ -14,7 +15,7 @@ pub type Result<T, F = Error> = std::result::Result<T, F>;
 
 #[derive(Debug)]
 pub struct Error {
-    pub file_name: Option<String>,
+    pub file_name: Option<PathBuf>,
     pub line_number: Option<usize>,
     pub token: Option<String>,
     pub message: String,
@@ -22,13 +23,13 @@ pub struct Error {
 
 impl Error {
     fn new(
-        file_name: Option<&str>,
+        file_name: Option<&Path>,
         line_number: Option<usize>,
         token: Option<&str>,
         message: String,
     ) -> Error {
         Error {
-            file_name: file_name.map(String::from),
+            file_name: file_name.map(PathBuf::from),
             line_number,
             token: token.map(String::from),
             message,
@@ -41,8 +42,10 @@ impl StdError for Error {}
 impl Display for Error {
     fn fmt(&self, f: &mut Formatter) -> FmtResult {
         match (self.file_name.as_ref(), self.line_number) {
-            (Some(ref file_name), Some(line_number)) => write!(f, "{file_name}:{line_number}: ")?,
-            (Some(ref file_name), None) => write!(f, "{file_name}: ")?,
+            (Some(ref file_name), Some(line_number)) => {
+                write!(f, "{}:{line_number}: ", file_name.display())?
+            }
+            (Some(ref file_name), None) => write!(f, "{}: ", file_name.display())?,
             (None, Some(line_number)) => write!(f, "line {line_number}: ")?,
             (None, None) => (),
         }
@@ -53,7 +56,7 @@ impl Display for Error {
     }
 }
 
-pub fn sack(input: &str, input_file_name: Option<&str>, endian: Endian) -> Result<Vec<u8>> {
+pub fn sack(input: &str, input_file_name: Option<&Path>, endian: Endian) -> Result<Vec<u8>> {
     let mut symbol_table = HashMap::new();
     let output = _sack(input, input_file_name, endian, &mut symbol_table)?;
     let output = if !symbol_table.is_empty() {
@@ -76,7 +79,7 @@ pub fn sack(input: &str, input_file_name: Option<&str>, endian: Endian) -> Resul
 
 fn _sack(
     input: &str,
-    input_file_name: Option<&str>,
+    input_file_name: Option<&Path>,
     endian: Endian,
     symbol_table: &mut HashMap<String, Option<u32>>,
 ) -> Result<Vec<u8>> {
@@ -337,7 +340,7 @@ enum Token {
 struct Lexer<'a> {
     input: &'a str,
     token: Option<(Token, &'a str)>,
-    input_file_name: Option<&'a str>,
+    input_file_name: Option<&'a Path>,
     line_number: usize,
     endian: Endian,
 }
@@ -363,7 +366,7 @@ fn skip_comments(mut s: &str) -> (&str, usize) {
 }
 
 impl<'a> Lexer<'a> {
-    fn new(input: &'a str, input_file_name: Option<&'a str>, endian: Endian) -> Result<Lexer<'a>> {
+    fn new(input: &'a str, input_file_name: Option<&'a Path>, endian: Endian) -> Result<Lexer<'a>> {
         let mut lexer = Lexer {
             input,
             token: None,
index 95f380b9b4849bc3adfbd6a86669c81ba6278c60..ce107c1cc3080373f160f49d6f1ab1f59f91eac9 100644 (file)
@@ -1,8 +1,14 @@
-use std::io::Cursor;
+use std::{io::Cursor, path::Path, sync::Arc};
 
 use crate::{
     endian::Endian,
-    output::pivot::{test::assert_rendering, Axis3, Dimension, PivotTable},
+    output::{
+        pivot::{
+            test::{assert_lines_eq, assert_rendering},
+            Axis3, Dimension, PivotTable,
+        },
+        Details, Item, Text,
+    },
     sys::{
         cooked::{decode, Headers, Metadata},
         raw::{encoding_from_headers, Decoder, Reader, Record},
@@ -15,306 +21,39 @@ use enum_iterator::all;
 
 #[test]
 fn variable_labels_and_missing_values() {
-    for endian in all::<Endian>() {
-        let input = r#"
-# File header.
-"$FL2"; s60 "$(#) SPSS DATA FILE PSPP synthetic test file";
-2; # Layout code
-28; # Nominal case size
-0; # Not compressed
-0; # Not weighted
-1; # 1 case.
-100.0; # Bias.
-"05 Jan 11"; "20:53:52";
-"PSPP synthetic test file: "; i8 244; i8 245; i8 246; i8 248; s34 "";
-i8 0 *3;
-
-# Numeric variable, no label or missing values.
-2; 0; 0; 0; 0x050800 *2; s8 "NUM1";
-
-# Numeric variable, variable label.
-2; 0; 1; 0; 0x050800 *2; s8 "NUM2";
-32; "Numeric variable 2's label ("; i8 249; i8 250; i8 251; ")";
-
-# Numeric variable, one missing value.
-2; 0; 0; 1; 0x050800 *2; s8 "NUM3";
-1.0;
-
-# Numeric variable, variable label and missing value.
-2; 0; 1; 1; 0x050800 *2; s8 "NUM4";
-30; "Another numeric variable label"; i8 0 * 2;
-1.0;
-
-# Numeric variable, two missing values.
-2; 0; 0; 2; 0x050800 *2; s8 "NUM5"; 1.0; 2.0;
-
-# Numeric variable, three missing values.
-2; 0; 0; 3; 0x050800 *2; s8 "NUM6"; 1.0; 2.0; 3.0;
-
-# Numeric variable, range of missing values.
-2; 0; 0; -2; 0x050800 *2; s8 "NUM7"; 1.0; 3.0;
-
-# Numeric variables, range of missing values plus discrete value.
-2; 0; 0; -3; 0x050800 *2; s8 "NUM8"; 1.0; 3.0; 5.0;
-2; 0; 0; -3; 0x050800 *2; s8 "NUM9"; 1.0; HIGHEST; -5.0;
-2; 0; 0; -3; 0x050800 *2; "NUM"; i8 192; i8 200; i8 204; i8 209; i8 210;
-LOWEST; 1.0; 5.0;
-
-# String variable, no label or missing values.
-2; 4; 0; 0; 0x010400 *2; s8 "STR1";
-
-# String variable, variable label.
-2; 4; 1; 0; 0x010400 *2; s8 "STR2";
-25; "String variable 2's label"; i8 0 * 3;
-
-# String variable, one missing value.
-2; 4; 0; 1; 0x010400 *2; s8 "STR3"; s8 "MISS";
-
-# String variable, variable label and missing value.
-2; 4; 1; 1; 0x010400 *2; s8 "STR4";
-29; "Another string variable label"; i8 0 * 3;
-s8 "OTHR";
-
-# String variable, two missing values.
-2; 4; 0; 2; 0x010400 *2; s8 "STR5"; s8 "MISS"; s8 "OTHR";
-
-# String variable, three missing values.
-2; 4; 0; 3; 0x010400 *2; s8 "STR6"; s8 "MISS"; s8 "OTHR"; s8 "MORE";
-
-# Long string variable, one missing value.
-# (This is not how SPSS represents missing values for long strings--it
-# uses a separate record as shown later below--but old versions of PSPP
-# did use this representation so we continue supporting it for backward
-# compatibility.
-2; 11; 0; 1; 0x010b00 *2; s8 "STR7"; "first8by";
-2; -1; 0; 0; 0; 0; s8 "";
-
-# Long string variables that will have missing values added with a
-# later record.
-2; 9; 0; 0; 0x010900 *2; s8 "STR8";
-2; -1; 0; 0; 0; 0; s8 "";
-2; 10; 0; 0; 0x010a00 *2; s8 "STR9";
-2; -1; 0; 0; 0; 0; s8 "";
-2; 11; 0; 0; 0x010b00 *2; s8 "STR10";
-2; -1; 0; 0; 0; 0; s8 "";
-
-# Long string variable, value label.
-2; 25; 1; 0; 0x011900 *2; s8 "STR11"; 14; "25-byte string"; i8 0 * 2;
-( 2; -1; 0; 0; 0; 0; s8 ""; ) * 2;
-# Variable label fields on continuation records have been spotted in system
-# files created by "SPSS Power Macintosh Release 6.1".
-2; -1; 1; 0; 0; 0; s8 ""; 20; "dummy variable label";
-
-# Machine integer info record.
-7; 3; 4; 8; 1; 2; 3; -1; 1; 1; ENDIAN; 1252;
-
-# Machine floating-point info record.
-7; 4; 8; 3; SYSMIS; HIGHEST; LOWEST;
-
-# Long string variable missing values record.
-7; 22; 1; COUNT (
-# One missing value for STR8.
-COUNT("STR8"); i8 1; 8; "abcdefgh";
-
-# Two missing values for STR9.
-COUNT("STR9"); i8 2; 8; "abcdefgh"; "01234567";
-
-# Three missing values for STR9.
-COUNT("STR10"); i8 3; 8; "abcdefgh"; "01234567"; "0       ";
-);
-
-# Character encoding record.
-7; 20; 1; 12; "windows-1252";
-
-# Dictionary termination record.
-999; 0;
-
-# Data.
-1.0; 2.0; 3.0; 4.0; 5.0; 6.0; 7.0; 8.0; 9.0; 10.0;
-s8 "abcd"; s8 "efgh"; s8 "ijkl"; s8 "mnop"; s8 "qrst"; s8 "uvwx";
-s16 "yzABCDEFGHI"; s16 "JKLMNOPQR"; s16 "STUVWXYZ01";
-s16 "23456789abc"; s32 "defghijklmnopqstuvwxyzABC";
-"#;
-        let sysfile = sack(input, None, endian).unwrap();
-        let cursor = Cursor::new(sysfile);
-        let reader = Reader::new(cursor, |warning| println!("{warning}")).unwrap();
-        let headers: Vec<Record> = reader.collect::<Result<Vec<_>, _>>().unwrap();
-        let encoding = encoding_from_headers(&headers, &|e| eprintln!("{e}")).unwrap();
-        let decoder = Decoder::new(encoding, |e| eprintln!("{e}"));
-        let mut decoded_records = Vec::new();
-        for header in headers {
-            decoded_records.push(header.decode(&decoder).unwrap());
-        }
-
-        let mut errors = Vec::new();
-        let headers = Headers::new(decoded_records, &mut |e| errors.push(e)).unwrap();
-        let (dictionary, metadata) = decode(headers, encoding, |e| errors.push(e)).unwrap();
-        assert_eq!(errors, vec![]);
-        assert_eq!(
-            metadata,
-            Metadata {
-                product: "$(#) SPSS DATA FILE PSPP synthetic test file".into(),
-                ..test_metadata(endian)
-            }
-        );
-        assert_eq!(
-            dictionary.file_label.as_ref().map(|s| s.as_str()),
-            Some("PSPP synthetic test file: ôõöø")
-        );
-        assert!(dictionary.output_value_labels().to_pivot_table().is_none());
-        let pt = dictionary.output_variables().to_pivot_table();
-        assert_rendering(
-            "variable_labels_and_missing_values",
-            &pt,
-            r#"╭────────────────────────────────┬────────┬────────────────────────────────┬─────────────────┬─────┬─────┬─────────┬────────────┬────────────┬──────────────────────╮
-│                                │Position│              Label             │Measurement Level│ Role│Width│Alignment│Print Format│Write Format│    Missing Values    │
-├────────────────────────────────┼────────┼────────────────────────────────┼─────────────────┼─────┼─────┼─────────┼────────────┼────────────┼──────────────────────┤
-│num1                            │       1│                                │                 │Input│    8│Right    │F8.0        │F8.0        │                      │
-│Numeric variable 2's label (ùúû)│       2│Numeric variable 2's label (ùúû)│                 │Input│    8│Right    │F8.0        │F8.0        │                      │
-│num3                            │       3│                                │                 │Input│    8│Right    │F8.0        │F8.0        │1                     │
-│Another numeric variable label  │       4│Another numeric variable label  │                 │Input│    8│Right    │F8.0        │F8.0        │1                     │
-│num5                            │       5│                                │                 │Input│    8│Right    │F8.0        │F8.0        │1; 2                  │
-│num6                            │       6│                                │                 │Input│    8│Right    │F8.0        │F8.0        │1; 2; 3               │
-│num7                            │       7│                                │                 │Input│    8│Right    │F8.0        │F8.0        │1 THRU 3              │
-│num8                            │       8│                                │                 │Input│    8│Right    │F8.0        │F8.0        │1 THRU 3; 5           │
-│num9                            │       9│                                │                 │Input│    8│Right    │F8.0        │F8.0        │1 THRU HIGH; -5       │
-│numàèìñò                        │      10│                                │                 │Input│    8│Right    │F8.0        │F8.0        │LOW THRU 1; 5         │
-│str1                            │      11│                                │Nominal          │Input│    4│Left     │A4          │A4          │                      │
-│String variable 2's label       │      12│String variable 2's label       │Nominal          │Input│    4│Left     │A4          │A4          │                      │
-│str3                            │      13│                                │Nominal          │Input│    4│Left     │A4          │A4          │"MISS"                │
-│Another string variable label   │      14│Another string variable label   │Nominal          │Input│    4│Left     │A4          │A4          │"OTHR"                │
-│str5                            │      15│                                │Nominal          │Input│    4│Left     │A4          │A4          │"MISS"; "OTHR"        │
-│str6                            │      16│                                │Nominal          │Input│    4│Left     │A4          │A4          │"MISS"; "OTHR"; "MORE"│
-│str7                            │      17│                                │Nominal          │Input│   11│Left     │A11         │A11         │"first8by"            │
-│str8                            │      18│                                │Nominal          │Input│    9│Left     │A9          │A9          │                      │
-│str9                            │      19│                                │Nominal          │Input│   10│Left     │A10         │A10         │                      │
-│str10                           │      20│                                │Nominal          │Input│   11│Left     │A11         │A11         │                      │
-│25-byte string                  │      21│25-byte string                  │Nominal          │Input│   25│Left     │A25         │A25         │                      │
-╰────────────────────────────────┴────────┴────────────────────────────────┴─────────────────┴─────┴─────┴─────────┴────────────┴────────────┴──────────────────────╯
-"#,
-        );
-    }
-}
-
-fn test_metadata(endian: Endian) -> Metadata {
-    Metadata {
-        creation: NaiveDate::from_ymd_opt(2011, 1, 5)
-            .unwrap()
-            .and_time(NaiveTime::from_hms_opt(20, 53, 52).unwrap()),
-        endian,
-        compression: None,
-        n_cases: Some(1),
-        product: "$(#) SPSS DATA FILE PSPP synthetic test file".into(),
-        product_ext: None,
-        version: Some((1, 2, 3)),
-    }
+    test_sysfile("variable_labels_and_missing_values");
 }
 
 #[test]
 fn unspecified_number_of_variable_positions() {
-    for endian in all::<Endian>() {
-        let input = r#"
-# File header.
-"$FL2"; s60 "$(#) SPSS DATA FILE PSPP synthetic test file";
-2; # Layout code
--1; # Nominal case size (unspecified)
-0; # Not compressed
-0; # Not weighted
-1; # 1 case.
-100.0; # Bias.
-"05 Jan 11"; "20:53:52"; s64 "PSPP synthetic test file";
-i8 0 *3;
-
-# Numeric variable, no label or missing values.
-2; 0; 0; 0; 0x050800 *2; s8 "NUM1";
-
-# Numeric variable, variable label.
-2; 0; 1; 0; 0x050800 *2; s8 "NUM2";
-26; "Numeric variable 2's label"; i8 0 *2;
-
-# Character encoding record.
-7; 20; 1; 12; "windows-1252";
-
-# Dictionary termination record.
-999; 0;
-
-# Data.
-1.0; 2.0;
-"#;
-        let sysfile = sack(input, None, endian).unwrap();
-        let cursor = Cursor::new(sysfile);
-        let reader = Reader::new(cursor, |warning| println!("{warning}")).unwrap();
-        let headers: Vec<Record> = reader.collect::<Result<Vec<_>, _>>().unwrap();
-        let encoding = encoding_from_headers(&headers, &|e| eprintln!("{e}")).unwrap();
-        let decoder = Decoder::new(encoding, |e| eprintln!("{e}"));
-        let mut decoded_records = Vec::new();
-        for header in headers {
-            decoded_records.push(header.decode(&decoder).unwrap());
-        }
-
-        let mut errors = Vec::new();
-        let headers = Headers::new(decoded_records, &mut |e| errors.push(e)).unwrap();
-        let (dictionary, metadata) = decode(headers, encoding, |e| errors.push(e)).unwrap();
-        assert_eq!(errors, vec![]);
-        assert_eq!(
-            metadata,
-            Metadata {
-                version: None,
-                ..test_metadata(endian)
-            }
-        );
-        assert_eq!(
-            dictionary.file_label.as_ref().map(|s| s.as_str()),
-            Some("PSPP synthetic test file")
-        );
-        assert!(dictionary.output_value_labels().to_pivot_table().is_none());
-        let pt = dictionary.output_variables().to_pivot_table();
-        assert_rendering("value_labels_dictionary", &pt, "\
-╭──────────────────────────┬────────┬──────────────────────────┬─────────────────┬─────┬─────┬─────────┬────────────┬────────────┬──────────────╮
-│                          │Position│           Label          │Measurement Level│ Role│Width│Alignment│Print Format│Write Format│Missing Values│
-├──────────────────────────┼────────┼──────────────────────────┼─────────────────┼─────┼─────┼─────────┼────────────┼────────────┼──────────────┤
-│num1                      │       1│                          │                 │Input│    8│Right    │F8.0        │F8.0        │              │
-│Numeric variable 2's label│       2│Numeric variable 2's label│                 │Input│    8│Right    │F8.0        │F8.0        │              │
-╰──────────────────────────┴────────┴──────────────────────────┴─────────────────┴─────┴─────┴─────────┴────────────┴────────────┴──────────────╯
-");
-    }
+    test_sysfile("unspecified_number_of_variable_positions");
 }
 
 #[test]
 fn wrong_variable_positions_but_v13() {
-    for endian in all::<Endian>() {
-        let input = r#"
-# File header.
-"$FL2"; s60 "$(#) SPSS DATA FILE PSPP synthetic test file";
-2; # Layout code
--1; # Nominal case size (unspecified)
-0; # Not compressed
-0; # Not weighted
-1; # 1 case.
-100.0; # Bias.
-"05 Jan 11"; "20:53:52"; s64 "PSPP synthetic test file";
-i8 0 *3;
-
-# Numeric variable, no label or missing values.
-2; 0; 0; 0; 0x050800 *2; s8 "NUM1";
-
-# Numeric variable, variable label.
-2; 0; 1; 0; 0x050800 *2; s8 "NUM2";
-26; "Numeric variable 2's label"; i8 0 *2;
-
-# Machine integer info record (SPSS 13).
-7; 3; 4; 8; 13; 2; 3; -1; 1; 1; ENDIAN; 1252;
+    test_sysfile("wrong_variable_positions_but_v13");
+}
 
-# Character encoding record.
-7; 20; 1; 12; "windows-1252";
+#[test]
+fn value_labels() {
+    test_sysfile("value_labels");
+}
 
-# Dictionary termination record.
-999; 0;
+#[test]
+fn documents() {
+    test_sysfile("documents");
+}
 
-# Data.
-1.0; 2.0;
-"#;
-        let sysfile = sack(input, None, endian).unwrap();
+fn test_sysfile(name: &str) {
+    let input_filename = Path::new(env!("CARGO_MANIFEST_DIR"))
+        .join("src/sys/testdata")
+        .join(name)
+        .with_extension("sack");
+    let input = String::from_utf8(std::fs::read(&input_filename).unwrap()).unwrap();
+    let expected_filename = input_filename.with_extension("expected");
+    let expected = String::from_utf8(std::fs::read(&expected_filename).unwrap()).unwrap();
+    for endian in all::<Endian>() {
+        let sysfile = sack(&input, Some(&input_filename), endian).unwrap();
         let cursor = Cursor::new(sysfile);
         let reader = Reader::new(cursor, |warning| println!("{warning}")).unwrap();
         let headers: Vec<Record> = reader.collect::<Result<Vec<_>, _>>().unwrap();
@@ -328,38 +67,6 @@ i8 0 *3;
         let mut errors = Vec::new();
         let headers = Headers::new(decoded_records, &mut |e| errors.push(e)).unwrap();
         let (dictionary, metadata) = decode(headers, encoding, |e| errors.push(e)).unwrap();
-        assert_eq!(errors, vec![]);
-        /*
-                assert_sysfile(
-                    &dictionary,
-                    &metadata,
-                    "\
-        ╭──────────────────────┬────────────────────────╮
-        │       Created        │    05-JAN-2011 20:53:52│
-        ├──────────────────────┼────────────────────────┤
-        │Writer Product        │PSPP synthetic test file│
-        │       Version        │13.2.3                  │
-        ├──────────────────────┼────────────────────────┤
-        │       Compression    │None                    │
-        │       Number of Cases│                       1│
-        ╰──────────────────────┴────────────────────────╯
-
-        ╭──────────────────────────┬────────┬──────────────────────────┬─────────────────┬─────┬─────┬─────────┬────────────┬────────────┬──────────────╮
-        │                          │Position│           Label          │Measurement Level│ Role│Width│Alignment│Print Format│Write Format│Missing Values│
-        ├──────────────────────────┼────────┼──────────────────────────┼─────────────────┼─────┼─────┼─────────┼────────────┼────────────┼──────────────┤
-        │num1                      │       1│                          │                 │Input│    8│Right    │F8.0        │F8.0        │              │
-        │Numeric variable 2's label│       2│Numeric variable 2's label│                 │Input│    8│Right    │F8.0        │F8.0        │              │
-        ╰──────────────────────────┴────────┴──────────────────────────┴─────────────────┴─────┴─────┴─────────┴────────────┴────────────┴──────────────╯
-        ",
-                );*/
-
-        assert_eq!(
-            metadata,
-            Metadata {
-                version: Some((13, 2, 3)),
-                ..test_metadata(endian)
-            }
-        );
         let (group, data) = metadata.to_pivot_rows();
         let metadata_table = PivotTable::new([(Axis3::Y, Dimension::new(group))]).with_data(
             data.into_iter()
@@ -367,261 +74,34 @@ i8 0 *3;
                 .filter(|(_row, value)| !value.is_empty())
                 .map(|(row, value)| ([row], value)),
         );
-        assert_rendering(
-            "wrong_variable_positions_but_v13_metadata",
-            &metadata_table,
-            "\
-╭──────────────────────┬────────────────────────╮
-│       Created        │    05-JAN-2011 20:53:52│
-├──────────────────────┼────────────────────────┤
-│Writer Product        │PSPP synthetic test file│
-│       Version        │13.2.3                  │
-├──────────────────────┼────────────────────────┤
-│       Compression    │None                    │
-│       Number of Cases│                       1│
-╰──────────────────────┴────────────────────────╯
-",
-        );
-        assert_eq!(
-            dictionary.file_label.as_ref().map(|s| s.as_str()),
-            Some("PSPP synthetic test file")
+        let (group, data) = dictionary.to_pivot_rows();
+        let dictionary_table = PivotTable::new([(Axis3::Y, Dimension::new(group))]).with_data(
+            data.into_iter()
+                .enumerate()
+                .filter(|(_row, value)| !value.is_empty())
+                .map(|(row, value)| ([row], value)),
         );
-        assert!(dictionary.output_value_labels().to_pivot_table().is_none());
-        let pt = dictionary.output_variables().to_pivot_table();
-        assert_rendering(
-            "value_labels_dictionary",
-            &pt,
-            "\
-╭──────────────────────────┬────────┬──────────────────────────┬─────────────────┬─────┬─────┬─────────┬────────────┬────────────┬──────────────╮
-│                          │Position│           Label          │Measurement Level│ Role│Width│Alignment│Print Format│Write Format│Missing Values│
-├──────────────────────────┼────────┼──────────────────────────┼─────────────────┼─────┼─────┼─────────┼────────────┼────────────┼──────────────┤
-│num1                      │       1│                          │                 │Input│    8│Right    │F8.0        │F8.0        │              │
-│Numeric variable 2's label│       2│Numeric variable 2's label│                 │Input│    8│Right    │F8.0        │F8.0        │              │
-╰──────────────────────────┴────────┴──────────────────────────┴─────────────────┴─────┴─────┴─────────┴────────────┴────────────┴──────────────╯
-",
+        let mut output = Vec::new();
+        output.extend(
+            errors
+                .into_iter()
+                .map(|error| Arc::new(Item::from(Text::new_log(error.to_string())))),
         );
-    }
-}
-
-#[test]
-fn value_labels() {
-    for endian in all::<Endian>() {
-        let input = r#"
-# File header.
-"$FL2"; s60 "$(#) SPSS DATA FILE PSPP synthetic test file";
-2; # Layout code
-22; # Nominal case size
-0; # Not compressed
-0; # Not weighted
-1; # 1 case.
-100.0; # Bias.
-"05 Jan 11"; "20:53:52"; s64 "PSPP synthetic test file";
-i8 0 *3;
-
-# Numeric variables.
-2; 0; 0; 0; 0x050800 *2; s8 "NUM1";
-2; 0; 0; 0; 0x050800 *2; s8 "NUM2";
-2; 0; 0; 0; 0x050800 *2; s8 "NUM3";
-2; 0; 0; 0; 0x050800 *2; s8 "NUM4";
-2; 0; 0; 0; 0x050800 *2; s8 "NUM5";
-
-# String variables.
-2; 1; 0; 0; 0x010100 *2; s8 "STR1"; # index 6
-2; 2; 0; 0; 0x010200 *2; s8 "STR2"; # index 7
-2; 3; 0; 0; 0x010300 *2; s8 "STR3"; # index 8
-2; 4; 0; 0; 0x010400 *2; s8 "STR4"; # index 9
-2; 4; 0; 0; 0x010400 *2; s8 "STR5"; # index 10
-2; 6; 0; 0; 0x010600 *2; s8 "STR6"; # index 11
-2; 7; 0; 0; 0x010700 *2; s8 "STR7"; # index 12
-2; 8; 0; 0; 0x010800 *2; s8 "STR8"; # index 13
-2; 9; 0; 0; 0x010900 *2; "STR9"; i8 230; s3 ""; # index 14
-2; -1; 0; 0; 0; 0; s8 "";
-2; 12; 0; 0; 0x010c00 *2; s8 "STR12"; # index 16
-2; -1; 0; 0; 0; 0; s8 "";
-2; 16; 0; 0; 0x011000 *2; s8 "STR16"; # index 18
-2; -1; 0; 0; 0; 0; s8 "";
-2; 17; 0; 0; 0x011100 *2; s8 "STR17"; # index 20
-( 2; -1; 0; 0; 0; 0; s8 ""; ) * 2;
-
-# One value label for NUM1.
-3; 1; 1.0; i8 17; i8 238; i8 228; i8 232; i8 237; s19 " (in Russian)"; 4; 1; 1;
-
-# Two value labels for NUM2, as a single pair of type 3 and type 4 records.
-3; 2; 1.0; i8 3; s7 "one"; 2.0; i8 3; s7 "two"; 4; 1; 2;
-
-# Two value labels for NUM3, as two pairs of type 3 and type 4 records.
-3; 1; 3.0; i8 5; s7 "three"; 4; 1; 3;
-3; 1; 4.0; i8 4; s7 "four"; 4; 1; 3;
-
-# Two common value labels for NUM4 and NUM5, plus two different ones for each.
-3; 1; 5.0; i8 4; s7 "five"; 4; 1; 4;
-3; 1; 6.0; i8 3; s7 "six"; 4; 1; 5;
-3; 2; 7.0; i8 5; s7 "seven"; 8.0; i8 5; s7 "eight"; 4; 2; 4; 5;
-3; 1; 9.0; i8 4; s7 "nine"; 4; 1; 4;
-3; 1; 10.0; i8 3; s7 "ten"; 4; 1; 5;
-
-# One value label for STR1.
-3; 1; s8 "a"; i8 19; s23 "value label for `a'"; 4; 1; 6;
-
-# Two value labels for STR2, as a single pair of type 3 and type 4 records.
-3; 2;
-s8 "bc"; i8 20; s23 "value label for `bc'";
-s8 "de"; i8 20; s23 "value label for `de'";
-4; 1; 7;
-
-# Two value labels for STR3, as two pairs of type 3 and type 4 records.
-3; 1; s8 "fgh"; i8 21; s23 "value label for `fgh'"; 4; 1; 8;
-3; 1; s8 "ijk"; i8 21; s23 "value label for `ijk'"; 4; 1; 8;
-
-# Two common value labels for STR4 and STR5, plus two different ones for each.
-3; 1; s8 "lmno"; i8 22; s23 "value label for `lmno'"; 4; 1; 9;
-3; 1; s8 "pqrs"; i8 22; s23 "value label for `pqrs'"; 4; 1; 10;
-3; 2;
-s8 "tuvw"; i8 22; s23 "value label for `tuvw'";
-s8 "xyzA"; i8 22; s23 "value label for `xyzA'";
-4; 2; 9; 10;
-3; 1; s8 "BCDE"; i8 22; s23 "value label for `BCDE'"; 4; 1; 9;
-3; 1; s8 "FGHI"; i8 22; s23 "value label for `FGHI'"; 4; 1; 10;
-
-# One value label for STR6, STR7, STR8.
-3; 1; s8 "JKLMNO"; i8 24; s31 "value label for `JKLMNO'"; 4; 1; 11;
-3; 1; s8 "JKLMNOP"; i8 25; s31 "value label for `JKLMNOP'"; 4; 1; 12;
-3; 1; s8 "JKLMNOPQ"; i8 26; s31 "value label for `JKLMNOPQ'"; 4; 1; 13;
-
-# Machine integer info record.
-7; 3; 4; 8; 1; 2; 3; -1; 1; 1; ENDIAN; 1251;
-
-# Character encoding record.
-7; 20; 1; 12; "windows-1251";
-
-7; 21; 1; COUNT (
-# One value label for STR9ж,
-COUNT("STR9"; i8 230); 9; 1; COUNT("RSTUVWXYZ"); COUNT("value label for `RSTUVWXYZ'");
-
-# Two value labels for STR12.
-COUNT("STR12"); 12; 2;
-COUNT("0123456789ab"); COUNT("value label for `0123456789ab'");
-COUNT("cdefghijklmn"); COUNT("value label for `cdefghijklmn'");
-
-# Three value labels for STR16.
-COUNT("STR16"); 16; 3;
-COUNT("opqrstuvwxyzABCD"); COUNT("value label for `opqrstuvwxyzABCD'");
-COUNT("EFGHIJKLMNOPQRST"); COUNT("value label for `EFGHIJKLMNOPQRST'");
-COUNT("UVWXYZ0123456789"); COUNT("value label for `UVWXYZ0123456789' with Cyrillic letters: `"; i8 244; i8 245; i8 246; "'");
-
-# One value label for STR17.
-COUNT("STR17"); 17; 1;
-COUNT("abcdefghijklmnopq"); COUNT("value label for `abcdefghijklmnopq'");
-);
-
-# Dictionary termination record.
-999; 0;
-"#;
-        let sysfile = sack(input, None, endian).unwrap();
-        let cursor = Cursor::new(sysfile);
-        let reader = Reader::new(cursor, |warning| println!("{warning}")).unwrap();
-        let headers: Vec<Record> = reader.collect::<Result<Vec<_>, _>>().unwrap();
-        let encoding = encoding_from_headers(&headers, &|e| eprintln!("{e}")).unwrap();
-        let decoder = Decoder::new(encoding, |e| eprintln!("{e}"));
-        let mut decoded_records = Vec::new();
-        for header in headers {
-            decoded_records.push(header.decode(&decoder).unwrap());
+        output.push(Arc::new(metadata_table.into()));
+        output.push(Arc::new(dictionary_table.into()));
+        output.push(Arc::new(
+            dictionary.output_variables().to_pivot_table().into(),
+        ));
+        if let Some(pt) = dictionary.output_value_labels().to_pivot_table() {
+            output.push(Arc::new(pt.into()));
         }
+        let output = Item::new(Details::Group(output));
 
-        let mut errors = Vec::new();
-        let headers = Headers::new(decoded_records, &mut |e| errors.push(e)).unwrap();
-        let (dictionary, metadata) = decode(headers, encoding, |e| errors.push(e)).unwrap();
-        assert_eq!(errors, vec![]);
-        assert_eq!(metadata, test_metadata(endian));
-        assert_eq!(
-            dictionary.file_label.as_ref().map(|s| s.as_str()),
-            Some("PSPP synthetic test file")
-        );
-        assert_rendering(
-            "value_labels_value_labels",
-            &dictionary.output_value_labels().to_pivot_table().unwrap(),
-            "\
-╭────────────────────────────────┬───────────────────────────────────────────────────────────────╮
-│Variable Value                  │                                                               │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│num1           1                │один (in Russian)                                              │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│num2           1                │one                                                            │
-│               2                │two                                                            │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│num3           3                │three                                                          │
-│               4                │four                                                           │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│num4           5                │five                                                           │
-│               7                │seven                                                          │
-│               8                │eight                                                          │
-│               9                │nine                                                           │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│num5           6                │six                                                            │
-│               7                │seven                                                          │
-│               8                │eight                                                          │
-│               10               │ten                                                            │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│str1           a                │value label for `a'                                            │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│str2           bc               │value label for `bc'                                           │
-│               de               │value label for `de'                                           │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│str3           fgh              │value label for `fgh'                                          │
-│               ijk              │value label for `ijk'                                          │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│str4           BCDE             │value label for `BCDE'                                         │
-│               lmno             │value label for `lmno'                                         │
-│               tuvw             │value label for `tuvw'                                         │
-│               xyzA             │value label for `xyzA'                                         │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│str5           FGHI             │value label for `FGHI'                                         │
-│               pqrs             │value label for `pqrs'                                         │
-│               tuvw             │value label for `tuvw'                                         │
-│               xyzA             │value label for `xyzA'                                         │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│str6           JKLMNO           │value label for `JKLMNO'                                       │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│str7           JKLMNOP          │value label for `JKLMNOP'                                      │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│str8           JKLMNOPQ         │value label for `JKLMNOPQ'                                     │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│str9ж          RSTUVWXYZ        │value label for `RSTUVWXYZ'                                    │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│str12          0123456789ab     │value label for `0123456789ab'                                 │
-│               cdefghijklmn     │value label for `cdefghijklmn'                                 │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│str16          EFGHIJKLMNOPQRST │value label for `EFGHIJKLMNOPQRST'                             │
-│               UVWXYZ0123456789 │value label for `UVWXYZ0123456789' with Cyrillic letters: `фхц'│
-│               opqrstuvwxyzABCD │value label for `opqrstuvwxyzABCD'                             │
-├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
-│str17          abcdefghijklmnopq│value label for `abcdefghijklmnopq'                            │
-╰────────────────────────────────┴───────────────────────────────────────────────────────────────╯
-",
+        assert_lines_eq(
+            &expected,
+            expected_filename.display(),
+            &output.to_string(),
+            "actual",
         );
-        let pt = dictionary.output_variables().to_pivot_table();
-        assert_rendering("value_labels_dictionary", &pt, "\
-╭─────┬────────┬─────┬─────────────────┬─────┬─────┬─────────┬────────────┬────────────┬──────────────╮
-│     │Position│Label│Measurement Level│ Role│Width│Alignment│Print Format│Write Format│Missing Values│
-├─────┼────────┼─────┼─────────────────┼─────┼─────┼─────────┼────────────┼────────────┼──────────────┤
-│num1 │       1│     │                 │Input│    8│Right    │F8.0        │F8.0        │              │
-│num2 │       2│     │                 │Input│    8│Right    │F8.0        │F8.0        │              │
-│num3 │       3│     │                 │Input│    8│Right    │F8.0        │F8.0        │              │
-│num4 │       4│     │                 │Input│    8│Right    │F8.0        │F8.0        │              │
-│num5 │       5│     │                 │Input│    8│Right    │F8.0        │F8.0        │              │
-│str1 │       6│     │Nominal          │Input│    1│Left     │A1          │A1          │              │
-│str2 │       7│     │Nominal          │Input│    2│Left     │A2          │A2          │              │
-│str3 │       8│     │Nominal          │Input│    3│Left     │A3          │A3          │              │
-│str4 │       9│     │Nominal          │Input│    4│Left     │A4          │A4          │              │
-│str5 │      10│     │Nominal          │Input│    4│Left     │A4          │A4          │              │
-│str6 │      11│     │Nominal          │Input│    6│Left     │A6          │A6          │              │
-│str7 │      12│     │Nominal          │Input│    7│Left     │A7          │A7          │              │
-│str8 │      13│     │Nominal          │Input│    8│Left     │A8          │A8          │              │
-│str9ж│      14│     │Nominal          │Input│    9│Left     │A9          │A9          │              │
-│str12│      15│     │Nominal          │Input│   12│Left     │A12         │A12         │              │
-│str16│      16│     │Nominal          │Input│   16│Left     │A16         │A16         │              │
-│str17│      17│     │Nominal          │Input│   17│Left     │A17         │A17         │              │
-╰─────┴────────┴─────┴─────────────────┴─────┴─────┴─────────┴────────────┴────────────┴──────────────╯
-");
     }
 }
diff --git a/rust/pspp/src/sys/testdata/documents.expected b/rust/pspp/src/sys/testdata/documents.expected
new file mode 100644 (file)
index 0000000..65439d5
--- /dev/null
@@ -0,0 +1,24 @@
+╭──────────────────────┬────────────────────────╮
+│       Created        │    01-JAN-2011 20:53:52│
+├──────────────────────┼────────────────────────┤
+│Writer Product        │PSPP synthetic test file│
+│       Version        │1.2.3                   │
+├──────────────────────┼────────────────────────┤
+│       Compression    │None                    │
+│       Number of Cases│                       1│
+╰──────────────────────┴────────────────────────╯
+
+╭─────────┬───────────────────────────────────────────────────────╮
+│Label    │PSPP synthetic test file                               │
+│Variables│                                                      1│
+│Documents│First line of documents                                │
+│         │Second line of documents                               │
+│         │abbé appliqué attaché blasé café canapé cliché consommé│
+│         │Last line of documents                                 │
+╰─────────┴───────────────────────────────────────────────────────╯
+
+╭────┬────────┬─────┬─────────────────┬─────┬─────┬─────────┬────────────┬────────────┬──────────────╮
+│    │Position│Label│Measurement Level│ Role│Width│Alignment│Print Format│Write Format│Missing Values│
+├────┼────────┼─────┼─────────────────┼─────┼─────┼─────────┼────────────┼────────────┼──────────────┤
+│num1│       1│     │                 │Input│    8│Right    │F8.0        │F8.0        │              │
+╰────┴────────┴─────┴─────────────────┴─────┴─────┴─────────┴────────────┴────────────┴──────────────╯
diff --git a/rust/pspp/src/sys/testdata/documents.sack b/rust/pspp/src/sys/testdata/documents.sack
new file mode 100644 (file)
index 0000000..3ecaaf0
--- /dev/null
@@ -0,0 +1,34 @@
+# File header.
+"$FL2"; s60 "$(#) SPSS DATA FILE PSPP synthetic test file";
+2; # Layout code
+1; # Nominal case size
+0; # Not compressed
+0; # Not weighted
+1; # 1 case.
+100.0; # Bias.
+"01 Jan 11"; "20:53:52"; s64 "PSPP synthetic test file";
+i8 0 *3;
+
+# Numeric variable, no label or missing values.
+2; 0; 0; 0; 0x050800 *2; s8 "NUM1";
+
+# Machine integer info record.
+7; 3; 4; 8; 1; 2; 3; -1; 1; 1; ENDIAN; 1252;
+
+# Document record.
+6; 5;
+s80 "First line of documents";
+s80 "Second line of documents";
+"abb"; i8 233; " appliqu"; i8 233; " attach"; i8 233; " blas"; i8 233; " caf"; i8 233; " canap"; i8 233; " clich"; i8 233; " consomm"; i8 233;
+s25 "";
+s80 "";
+s80 "Last line of documents";
+
+# Character encoding record.
+7; 20; 1; 12; "windows-1252";
+
+# Dictionary termination record.
+999; 0;
+
+# Data.
+1.0;
diff --git a/rust/pspp/src/sys/testdata/unspecified_number_of_variable_positions.expected b/rust/pspp/src/sys/testdata/unspecified_number_of_variable_positions.expected
new file mode 100644 (file)
index 0000000..bdb0b0d
--- /dev/null
@@ -0,0 +1,20 @@
+╭──────────────────────┬────────────────────────╮
+│       Created        │    05-JAN-2011 20:53:52│
+├──────────────────────┼────────────────────────┤
+│Writer Product        │PSPP synthetic test file│
+├──────────────────────┼────────────────────────┤
+│       Compression    │None                    │
+│       Number of Cases│                       1│
+╰──────────────────────┴────────────────────────╯
+
+╭─────────┬────────────────────────╮
+│Label    │PSPP synthetic test file│
+│Variables│                       2│
+╰─────────┴────────────────────────╯
+
+╭──────────────────────────┬────────┬──────────────────────────┬─────────────────┬─────┬─────┬─────────┬────────────┬────────────┬──────────────╮
+│                          │Position│           Label          │Measurement Level│ Role│Width│Alignment│Print Format│Write Format│Missing Values│
+├──────────────────────────┼────────┼──────────────────────────┼─────────────────┼─────┼─────┼─────────┼────────────┼────────────┼──────────────┤
+│num1                      │       1│                          │                 │Input│    8│Right    │F8.0        │F8.0        │              │
+│Numeric variable 2's label│       2│Numeric variable 2's label│                 │Input│    8│Right    │F8.0        │F8.0        │              │
+╰──────────────────────────┴────────┴──────────────────────────┴─────────────────┴─────┴─────┴─────────┴────────────┴────────────┴──────────────╯
diff --git a/rust/pspp/src/sys/testdata/unspecified_number_of_variable_positions.sack b/rust/pspp/src/sys/testdata/unspecified_number_of_variable_positions.sack
new file mode 100644 (file)
index 0000000..53b12bc
--- /dev/null
@@ -0,0 +1,26 @@
+# File header.
+"$FL2"; s60 "$(#) SPSS DATA FILE PSPP synthetic test file";
+2; # Layout code
+-1; # Nominal case size (unspecified)
+0; # Not compressed
+0; # Not weighted
+1; # 1 case.
+100.0; # Bias.
+"05 Jan 11"; "20:53:52"; s64 "PSPP synthetic test file";
+i8 0 *3;
+
+# Numeric variable, no label or missing values.
+2; 0; 0; 0; 0x050800 *2; s8 "NUM1";
+
+# Numeric variable, variable label.
+2; 0; 1; 0; 0x050800 *2; s8 "NUM2";
+26; "Numeric variable 2's label"; i8 0 *2;
+
+# Character encoding record.
+7; 20; 1; 12; "windows-1252";
+
+# Dictionary termination record.
+999; 0;
+
+# Data.
+1.0; 2.0;
diff --git a/rust/pspp/src/sys/testdata/value_labels.expected b/rust/pspp/src/sys/testdata/value_labels.expected
new file mode 100644 (file)
index 0000000..c62f700
--- /dev/null
@@ -0,0 +1,93 @@
+╭──────────────────────┬────────────────────────╮
+│       Created        │    05-JAN-2011 20:53:52│
+├──────────────────────┼────────────────────────┤
+│Writer Product        │PSPP synthetic test file│
+│       Version        │1.2.3                   │
+├──────────────────────┼────────────────────────┤
+│       Compression    │None                    │
+│       Number of Cases│                       1│
+╰──────────────────────┴────────────────────────╯
+
+╭─────────┬────────────────────────╮
+│Label    │PSPP synthetic test file│
+│Variables│                      17│
+╰─────────┴────────────────────────╯
+
+╭─────┬────────┬─────┬─────────────────┬─────┬─────┬─────────┬────────────┬────────────┬──────────────╮
+│     │Position│Label│Measurement Level│ Role│Width│Alignment│Print Format│Write Format│Missing Values│
+├─────┼────────┼─────┼─────────────────┼─────┼─────┼─────────┼────────────┼────────────┼──────────────┤
+│num1 │       1│     │                 │Input│    8│Right    │F8.0        │F8.0        │              │
+│num2 │       2│     │                 │Input│    8│Right    │F8.0        │F8.0        │              │
+│num3 │       3│     │                 │Input│    8│Right    │F8.0        │F8.0        │              │
+│num4 │       4│     │                 │Input│    8│Right    │F8.0        │F8.0        │              │
+│num5 │       5│     │                 │Input│    8│Right    │F8.0        │F8.0        │              │
+│str1 │       6│     │Nominal          │Input│    1│Left     │A1          │A1          │              │
+│str2 │       7│     │Nominal          │Input│    2│Left     │A2          │A2          │              │
+│str3 │       8│     │Nominal          │Input│    3│Left     │A3          │A3          │              │
+│str4 │       9│     │Nominal          │Input│    4│Left     │A4          │A4          │              │
+│str5 │      10│     │Nominal          │Input│    4│Left     │A4          │A4          │              │
+│str6 │      11│     │Nominal          │Input│    6│Left     │A6          │A6          │              │
+│str7 │      12│     │Nominal          │Input│    7│Left     │A7          │A7          │              │
+│str8 │      13│     │Nominal          │Input│    8│Left     │A8          │A8          │              │
+│str9ж│      14│     │Nominal          │Input│    9│Left     │A9          │A9          │              │
+│str12│      15│     │Nominal          │Input│   12│Left     │A12         │A12         │              │
+│str16│      16│     │Nominal          │Input│   16│Left     │A16         │A16         │              │
+│str17│      17│     │Nominal          │Input│   17│Left     │A17         │A17         │              │
+╰─────┴────────┴─────┴─────────────────┴─────┴─────┴─────────┴────────────┴────────────┴──────────────╯
+
+╭────────────────────────────────┬───────────────────────────────────────────────────────────────╮
+│Variable Value                  │                                                               │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│num1           1                │один (in Russian)                                              │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│num2           1                │one                                                            │
+│               2                │two                                                            │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│num3           3                │three                                                          │
+│               4                │four                                                           │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│num4           5                │five                                                           │
+│               7                │seven                                                          │
+│               8                │eight                                                          │
+│               9                │nine                                                           │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│num5           6                │six                                                            │
+│               7                │seven                                                          │
+│               8                │eight                                                          │
+│               10               │ten                                                            │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│str1           a                │value label for `a'                                            │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│str2           bc               │value label for `bc'                                           │
+│               de               │value label for `de'                                           │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│str3           fgh              │value label for `fgh'                                          │
+│               ijk              │value label for `ijk'                                          │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│str4           BCDE             │value label for `BCDE'                                         │
+│               lmno             │value label for `lmno'                                         │
+│               tuvw             │value label for `tuvw'                                         │
+│               xyzA             │value label for `xyzA'                                         │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│str5           FGHI             │value label for `FGHI'                                         │
+│               pqrs             │value label for `pqrs'                                         │
+│               tuvw             │value label for `tuvw'                                         │
+│               xyzA             │value label for `xyzA'                                         │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│str6           JKLMNO           │value label for `JKLMNO'                                       │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│str7           JKLMNOP          │value label for `JKLMNOP'                                      │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│str8           JKLMNOPQ         │value label for `JKLMNOPQ'                                     │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│str9ж          RSTUVWXYZ        │value label for `RSTUVWXYZ'                                    │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│str12          0123456789ab     │value label for `0123456789ab'                                 │
+│               cdefghijklmn     │value label for `cdefghijklmn'                                 │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│str16          EFGHIJKLMNOPQRST │value label for `EFGHIJKLMNOPQRST'                             │
+│               UVWXYZ0123456789 │value label for `UVWXYZ0123456789' with Cyrillic letters: `фхц'│
+│               opqrstuvwxyzABCD │value label for `opqrstuvwxyzABCD'                             │
+├────────────────────────────────┼───────────────────────────────────────────────────────────────┤
+│str17          abcdefghijklmnopq│value label for `abcdefghijklmnopq'                            │
+╰────────────────────────────────┴───────────────────────────────────────────────────────────────╯
diff --git a/rust/pspp/src/sys/testdata/value_labels.sack b/rust/pspp/src/sys/testdata/value_labels.sack
new file mode 100644 (file)
index 0000000..8198711
--- /dev/null
@@ -0,0 +1,109 @@
+# File header.
+"$FL2"; s60 "$(#) SPSS DATA FILE PSPP synthetic test file";
+2; # Layout code
+22; # Nominal case size
+0; # Not compressed
+0; # Not weighted
+1; # 1 case.
+100.0; # Bias.
+"05 Jan 11"; "20:53:52"; s64 "PSPP synthetic test file";
+i8 0 *3;
+
+# Numeric variables.
+2; 0; 0; 0; 0x050800 *2; s8 "NUM1";
+2; 0; 0; 0; 0x050800 *2; s8 "NUM2";
+2; 0; 0; 0; 0x050800 *2; s8 "NUM3";
+2; 0; 0; 0; 0x050800 *2; s8 "NUM4";
+2; 0; 0; 0; 0x050800 *2; s8 "NUM5";
+
+# String variables.
+2; 1; 0; 0; 0x010100 *2; s8 "STR1"; # index 6
+2; 2; 0; 0; 0x010200 *2; s8 "STR2"; # index 7
+2; 3; 0; 0; 0x010300 *2; s8 "STR3"; # index 8
+2; 4; 0; 0; 0x010400 *2; s8 "STR4"; # index 9
+2; 4; 0; 0; 0x010400 *2; s8 "STR5"; # index 10
+2; 6; 0; 0; 0x010600 *2; s8 "STR6"; # index 11
+2; 7; 0; 0; 0x010700 *2; s8 "STR7"; # index 12
+2; 8; 0; 0; 0x010800 *2; s8 "STR8"; # index 13
+2; 9; 0; 0; 0x010900 *2; "STR9"; i8 230; s3 ""; # index 14
+2; -1; 0; 0; 0; 0; s8 "";
+2; 12; 0; 0; 0x010c00 *2; s8 "STR12"; # index 16
+2; -1; 0; 0; 0; 0; s8 "";
+2; 16; 0; 0; 0x011000 *2; s8 "STR16"; # index 18
+2; -1; 0; 0; 0; 0; s8 "";
+2; 17; 0; 0; 0x011100 *2; s8 "STR17"; # index 20
+( 2; -1; 0; 0; 0; 0; s8 ""; ) * 2;
+
+# One value label for NUM1.
+3; 1; 1.0; i8 17; i8 238; i8 228; i8 232; i8 237; s19 " (in Russian)"; 4; 1; 1;
+
+# Two value labels for NUM2, as a single pair of type 3 and type 4 records.
+3; 2; 1.0; i8 3; s7 "one"; 2.0; i8 3; s7 "two"; 4; 1; 2;
+
+# Two value labels for NUM3, as two pairs of type 3 and type 4 records.
+3; 1; 3.0; i8 5; s7 "three"; 4; 1; 3;
+3; 1; 4.0; i8 4; s7 "four"; 4; 1; 3;
+
+# Two common value labels for NUM4 and NUM5, plus two different ones for each.
+3; 1; 5.0; i8 4; s7 "five"; 4; 1; 4;
+3; 1; 6.0; i8 3; s7 "six"; 4; 1; 5;
+3; 2; 7.0; i8 5; s7 "seven"; 8.0; i8 5; s7 "eight"; 4; 2; 4; 5;
+3; 1; 9.0; i8 4; s7 "nine"; 4; 1; 4;
+3; 1; 10.0; i8 3; s7 "ten"; 4; 1; 5;
+
+# One value label for STR1.
+3; 1; s8 "a"; i8 19; s23 "value label for `a'"; 4; 1; 6;
+
+# Two value labels for STR2, as a single pair of type 3 and type 4 records.
+3; 2;
+s8 "bc"; i8 20; s23 "value label for `bc'";
+s8 "de"; i8 20; s23 "value label for `de'";
+4; 1; 7;
+
+# Two value labels for STR3, as two pairs of type 3 and type 4 records.
+3; 1; s8 "fgh"; i8 21; s23 "value label for `fgh'"; 4; 1; 8;
+3; 1; s8 "ijk"; i8 21; s23 "value label for `ijk'"; 4; 1; 8;
+
+# Two common value labels for STR4 and STR5, plus two different ones for each.
+3; 1; s8 "lmno"; i8 22; s23 "value label for `lmno'"; 4; 1; 9;
+3; 1; s8 "pqrs"; i8 22; s23 "value label for `pqrs'"; 4; 1; 10;
+3; 2;
+s8 "tuvw"; i8 22; s23 "value label for `tuvw'";
+s8 "xyzA"; i8 22; s23 "value label for `xyzA'";
+4; 2; 9; 10;
+3; 1; s8 "BCDE"; i8 22; s23 "value label for `BCDE'"; 4; 1; 9;
+3; 1; s8 "FGHI"; i8 22; s23 "value label for `FGHI'"; 4; 1; 10;
+
+# One value label for STR6, STR7, STR8.
+3; 1; s8 "JKLMNO"; i8 24; s31 "value label for `JKLMNO'"; 4; 1; 11;
+3; 1; s8 "JKLMNOP"; i8 25; s31 "value label for `JKLMNOP'"; 4; 1; 12;
+3; 1; s8 "JKLMNOPQ"; i8 26; s31 "value label for `JKLMNOPQ'"; 4; 1; 13;
+
+# Machine integer info record.
+7; 3; 4; 8; 1; 2; 3; -1; 1; 1; ENDIAN; 1251;
+
+# Character encoding record.
+7; 20; 1; 12; "windows-1251";
+
+7; 21; 1; COUNT (
+# One value label for STR9ж,
+COUNT("STR9"; i8 230); 9; 1; COUNT("RSTUVWXYZ"); COUNT("value label for `RSTUVWXYZ'");
+
+# Two value labels for STR12.
+COUNT("STR12"); 12; 2;
+COUNT("0123456789ab"); COUNT("value label for `0123456789ab'");
+COUNT("cdefghijklmn"); COUNT("value label for `cdefghijklmn'");
+
+# Three value labels for STR16.
+COUNT("STR16"); 16; 3;
+COUNT("opqrstuvwxyzABCD"); COUNT("value label for `opqrstuvwxyzABCD'");
+COUNT("EFGHIJKLMNOPQRST"); COUNT("value label for `EFGHIJKLMNOPQRST'");
+COUNT("UVWXYZ0123456789"); COUNT("value label for `UVWXYZ0123456789' with Cyrillic letters: `"; i8 244; i8 245; i8 246; "'");
+
+# One value label for STR17.
+COUNT("STR17"); 17; 1;
+COUNT("abcdefghijklmnopq"); COUNT("value label for `abcdefghijklmnopq'");
+);
+
+# Dictionary termination record.
+999; 0;
diff --git a/rust/pspp/src/sys/testdata/variable_labels_and_missing_values.expected b/rust/pspp/src/sys/testdata/variable_labels_and_missing_values.expected
new file mode 100644 (file)
index 0000000..f3c6549
--- /dev/null
@@ -0,0 +1,40 @@
+╭──────────────────────┬────────────────────────╮
+│       Created        │    05-JAN-2011 20:53:52│
+├──────────────────────┼────────────────────────┤
+│Writer Product        │PSPP synthetic test file│
+│       Version        │1.2.3                   │
+├──────────────────────┼────────────────────────┤
+│       Compression    │None                    │
+│       Number of Cases│                       1│
+╰──────────────────────┴────────────────────────╯
+
+╭─────────┬──────────────────────────────╮
+│Label    │PSPP synthetic test file: ôõöø│
+│Variables│                            21│
+╰─────────┴──────────────────────────────╯
+
+╭────────────────────────────────┬────────┬────────────────────────────────┬─────────────────┬─────┬─────┬─────────┬────────────┬────────────┬──────────────────────╮
+│                                │Position│              Label             │Measurement Level│ Role│Width│Alignment│Print Format│Write Format│    Missing Values    │
+├────────────────────────────────┼────────┼────────────────────────────────┼─────────────────┼─────┼─────┼─────────┼────────────┼────────────┼──────────────────────┤
+│num1                            │       1│                                │                 │Input│    8│Right    │F8.0        │F8.0        │                      │
+│Numeric variable 2's label (ùúû)│       2│Numeric variable 2's label (ùúû)│                 │Input│    8│Right    │F8.0        │F8.0        │                      │
+│num3                            │       3│                                │                 │Input│    8│Right    │F8.0        │F8.0        │1                     │
+│Another numeric variable label  │       4│Another numeric variable label  │                 │Input│    8│Right    │F8.0        │F8.0        │1                     │
+│num5                            │       5│                                │                 │Input│    8│Right    │F8.0        │F8.0        │1; 2                  │
+│num6                            │       6│                                │                 │Input│    8│Right    │F8.0        │F8.0        │1; 2; 3               │
+│num7                            │       7│                                │                 │Input│    8│Right    │F8.0        │F8.0        │1 THRU 3              │
+│num8                            │       8│                                │                 │Input│    8│Right    │F8.0        │F8.0        │1 THRU 3; 5           │
+│num9                            │       9│                                │                 │Input│    8│Right    │F8.0        │F8.0        │1 THRU HIGH; -5       │
+│numàèìñò                        │      10│                                │                 │Input│    8│Right    │F8.0        │F8.0        │LOW THRU 1; 5         │
+│str1                            │      11│                                │Nominal          │Input│    4│Left     │A4          │A4          │                      │
+│String variable 2's label       │      12│String variable 2's label       │Nominal          │Input│    4│Left     │A4          │A4          │                      │
+│str3                            │      13│                                │Nominal          │Input│    4│Left     │A4          │A4          │"MISS"                │
+│Another string variable label   │      14│Another string variable label   │Nominal          │Input│    4│Left     │A4          │A4          │"OTHR"                │
+│str5                            │      15│                                │Nominal          │Input│    4│Left     │A4          │A4          │"MISS"; "OTHR"        │
+│str6                            │      16│                                │Nominal          │Input│    4│Left     │A4          │A4          │"MISS"; "OTHR"; "MORE"│
+│str7                            │      17│                                │Nominal          │Input│   11│Left     │A11         │A11         │"first8by"            │
+│str8                            │      18│                                │Nominal          │Input│    9│Left     │A9          │A9          │                      │
+│str9                            │      19│                                │Nominal          │Input│   10│Left     │A10         │A10         │                      │
+│str10                           │      20│                                │Nominal          │Input│   11│Left     │A11         │A11         │                      │
+│25-byte string                  │      21│25-byte string                  │Nominal          │Input│   25│Left     │A25         │A25         │                      │
+╰────────────────────────────────┴────────┴────────────────────────────────┴─────────────────┴─────┴─────┴─────────┴────────────┴────────────┴──────────────────────╯
diff --git a/rust/pspp/src/sys/testdata/variable_labels_and_missing_values.sack b/rust/pspp/src/sys/testdata/variable_labels_and_missing_values.sack
new file mode 100644 (file)
index 0000000..3f8f22f
--- /dev/null
@@ -0,0 +1,117 @@
+# File header.
+"$FL2"; s60 "$(#) SPSS DATA FILE PSPP synthetic test file";
+2; # Layout code
+28; # Nominal case size
+0; # Not compressed
+0; # Not weighted
+1; # 1 case.
+100.0; # Bias.
+"05 Jan 11"; "20:53:52";
+"PSPP synthetic test file: "; i8 244; i8 245; i8 246; i8 248; s34 "";
+i8 0 *3;
+
+# Numeric variable, no label or missing values.
+2; 0; 0; 0; 0x050800 *2; s8 "NUM1";
+
+# Numeric variable, variable label.
+2; 0; 1; 0; 0x050800 *2; s8 "NUM2";
+32; "Numeric variable 2's label ("; i8 249; i8 250; i8 251; ")";
+
+# Numeric variable, one missing value.
+2; 0; 0; 1; 0x050800 *2; s8 "NUM3";
+1.0;
+
+# Numeric variable, variable label and missing value.
+2; 0; 1; 1; 0x050800 *2; s8 "NUM4";
+30; "Another numeric variable label"; i8 0 * 2;
+1.0;
+
+# Numeric variable, two missing values.
+2; 0; 0; 2; 0x050800 *2; s8 "NUM5"; 1.0; 2.0;
+
+# Numeric variable, three missing values.
+2; 0; 0; 3; 0x050800 *2; s8 "NUM6"; 1.0; 2.0; 3.0;
+
+# Numeric variable, range of missing values.
+2; 0; 0; -2; 0x050800 *2; s8 "NUM7"; 1.0; 3.0;
+
+# Numeric variables, range of missing values plus discrete value.
+2; 0; 0; -3; 0x050800 *2; s8 "NUM8"; 1.0; 3.0; 5.0;
+2; 0; 0; -3; 0x050800 *2; s8 "NUM9"; 1.0; HIGHEST; -5.0;
+2; 0; 0; -3; 0x050800 *2; "NUM"; i8 192; i8 200; i8 204; i8 209; i8 210;
+LOWEST; 1.0; 5.0;
+
+# String variable, no label or missing values.
+2; 4; 0; 0; 0x010400 *2; s8 "STR1";
+
+# String variable, variable label.
+2; 4; 1; 0; 0x010400 *2; s8 "STR2";
+25; "String variable 2's label"; i8 0 * 3;
+
+# String variable, one missing value.
+2; 4; 0; 1; 0x010400 *2; s8 "STR3"; s8 "MISS";
+
+# String variable, variable label and missing value.
+2; 4; 1; 1; 0x010400 *2; s8 "STR4";
+29; "Another string variable label"; i8 0 * 3;
+s8 "OTHR";
+
+# String variable, two missing values.
+2; 4; 0; 2; 0x010400 *2; s8 "STR5"; s8 "MISS"; s8 "OTHR";
+
+# String variable, three missing values.
+2; 4; 0; 3; 0x010400 *2; s8 "STR6"; s8 "MISS"; s8 "OTHR"; s8 "MORE";
+
+# Long string variable, one missing value.
+# (This is not how SPSS represents missing values for long strings--it
+# uses a separate record as shown later below--but old versions of PSPP
+# did use this representation so we continue supporting it for backward
+# compatibility.
+2; 11; 0; 1; 0x010b00 *2; s8 "STR7"; "first8by";
+2; -1; 0; 0; 0; 0; s8 "";
+
+# Long string variables that will have missing values added with a
+# later record.
+2; 9; 0; 0; 0x010900 *2; s8 "STR8";
+2; -1; 0; 0; 0; 0; s8 "";
+2; 10; 0; 0; 0x010a00 *2; s8 "STR9";
+2; -1; 0; 0; 0; 0; s8 "";
+2; 11; 0; 0; 0x010b00 *2; s8 "STR10";
+2; -1; 0; 0; 0; 0; s8 "";
+
+# Long string variable, value label.
+2; 25; 1; 0; 0x011900 *2; s8 "STR11"; 14; "25-byte string"; i8 0 * 2;
+( 2; -1; 0; 0; 0; 0; s8 ""; ) * 2;
+# Variable label fields on continuation records have been spotted in system
+# files created by "SPSS Power Macintosh Release 6.1".
+2; -1; 1; 0; 0; 0; s8 ""; 20; "dummy variable label";
+
+# Machine integer info record.
+7; 3; 4; 8; 1; 2; 3; -1; 1; 1; ENDIAN; 1252;
+
+# Machine floating-point info record.
+7; 4; 8; 3; SYSMIS; HIGHEST; LOWEST;
+
+# Long string variable missing values record.
+7; 22; 1; COUNT (
+# One missing value for STR8.
+COUNT("STR8"); i8 1; 8; "abcdefgh";
+
+# Two missing values for STR9.
+COUNT("STR9"); i8 2; 8; "abcdefgh"; "01234567";
+
+# Three missing values for STR9.
+COUNT("STR10"); i8 3; 8; "abcdefgh"; "01234567"; "0       ";
+);
+
+# Character encoding record.
+7; 20; 1; 12; "windows-1252";
+
+# Dictionary termination record.
+999; 0;
+
+# Data.
+1.0; 2.0; 3.0; 4.0; 5.0; 6.0; 7.0; 8.0; 9.0; 10.0;
+s8 "abcd"; s8 "efgh"; s8 "ijkl"; s8 "mnop"; s8 "qrst"; s8 "uvwx";
+s16 "yzABCDEFGHI"; s16 "JKLMNOPQR"; s16 "STUVWXYZ01";
+s16 "23456789abc"; s32 "defghijklmnopqstuvwxyzABC";
diff --git a/rust/pspp/src/sys/testdata/wrong_variable_positions_but_v13.expected b/rust/pspp/src/sys/testdata/wrong_variable_positions_but_v13.expected
new file mode 100644 (file)
index 0000000..119a338
--- /dev/null
@@ -0,0 +1,21 @@
+╭──────────────────────┬────────────────────────╮
+│       Created        │    05-JAN-2011 20:53:52│
+├──────────────────────┼────────────────────────┤
+│Writer Product        │PSPP synthetic test file│
+│       Version        │13.2.3                  │
+├──────────────────────┼────────────────────────┤
+│       Compression    │None                    │
+│       Number of Cases│                       1│
+╰──────────────────────┴────────────────────────╯
+
+╭─────────┬────────────────────────╮
+│Label    │PSPP synthetic test file│
+│Variables│                       2│
+╰─────────┴────────────────────────╯
+
+╭──────────────────────────┬────────┬──────────────────────────┬─────────────────┬─────┬─────┬─────────┬────────────┬────────────┬──────────────╮
+│                          │Position│           Label          │Measurement Level│ Role│Width│Alignment│Print Format│Write Format│Missing Values│
+├──────────────────────────┼────────┼──────────────────────────┼─────────────────┼─────┼─────┼─────────┼────────────┼────────────┼──────────────┤
+│num1                      │       1│                          │                 │Input│    8│Right    │F8.0        │F8.0        │              │
+│Numeric variable 2's label│       2│Numeric variable 2's label│                 │Input│    8│Right    │F8.0        │F8.0        │              │
+╰──────────────────────────┴────────┴──────────────────────────┴─────────────────┴─────┴─────┴─────────┴────────────┴────────────┴──────────────╯
diff --git a/rust/pspp/src/sys/testdata/wrong_variable_positions_but_v13.sack b/rust/pspp/src/sys/testdata/wrong_variable_positions_but_v13.sack
new file mode 100644 (file)
index 0000000..a975825
--- /dev/null
@@ -0,0 +1,29 @@
+# File header.
+"$FL2"; s60 "$(#) SPSS DATA FILE PSPP synthetic test file";
+2; # Layout code
+-1; # Nominal case size (unspecified)
+0; # Not compressed
+0; # Not weighted
+1; # 1 case.
+100.0; # Bias.
+"05 Jan 11"; "20:53:52"; s64 "PSPP synthetic test file";
+i8 0 *3;
+
+# Numeric variable, no label or missing values.
+2; 0; 0; 0; 0x050800 *2; s8 "NUM1";
+
+# Numeric variable, variable label.
+2; 0; 1; 0; 0x050800 *2; s8 "NUM2";
+26; "Numeric variable 2's label"; i8 0 *2;
+
+# Machine integer info record (SPSS 13).
+7; 3; 4; 8; 13; 2; 3; -1; 1; 1; ENDIAN; 1252;
+
+# Character encoding record.
+7; 20; 1; 12; "windows-1252";
+
+# Dictionary termination record.
+999; 0;
+
+# Data.
+1.0; 2.0;
index 5be80ea45fc612e9c34c8c02292ee4ffbe105cb0..d94016b26471b9dad5f75bbdac7ace1b12ddb938 100644 (file)
@@ -83,7 +83,7 @@ fn main() -> Result<()> {
     let input = read_to_string(&input_file_name)
         .map_err(|err| anyhow!("{input_file_str}: read failed ({err})"))?;
 
-    let output = sack(&input, Some(&input_file_str), endian)?;
+    let output = sack(&input, Some(&input_file_name), endian)?;
 
     let output_file_str = output_file_name.to_string_lossy();
     std::fs::write(&output_file_name, output)