rust: Rename `test` modules to `tests` for consistency.
authorBen Pfaff <blp@cs.stanford.edu>
Sun, 14 Sep 2025 21:02:59 +0000 (14:02 -0700)
committerBen Pfaff <blp@cs.stanford.edu>
Wed, 17 Sep 2025 15:22:18 +0000 (08:22 -0700)
24 files changed:
rust/pspp/src/crypto.rs
rust/pspp/src/format/display.rs
rust/pspp/src/format/display/test.rs [deleted file]
rust/pspp/src/format/display/tests.rs [new file with mode: 0644]
rust/pspp/src/format/parse.rs
rust/pspp/src/lex/scan.rs
rust/pspp/src/lex/scan/test.rs [deleted file]
rust/pspp/src/lex/scan/tests.rs [new file with mode: 0644]
rust/pspp/src/lex/segment.rs
rust/pspp/src/lex/segment/test.rs [deleted file]
rust/pspp/src/lex/segment/tests.rs [new file with mode: 0644]
rust/pspp/src/lex/token.rs
rust/pspp/src/output/cairo.rs
rust/pspp/src/output/pivot.rs
rust/pspp/src/output/pivot/look_xml.rs
rust/pspp/src/output/pivot/test.rs [deleted file]
rust/pspp/src/output/pivot/tests.rs [new file with mode: 0644]
rust/pspp/src/output/pivot/tlo.rs
rust/pspp/src/output/text.rs
rust/pspp/src/output/text_line.rs
rust/pspp/src/sys.rs
rust/pspp/src/sys/sack.rs
rust/pspp/src/sys/test.rs [deleted file]
rust/pspp/src/sys/tests.rs [new file with mode: 0644]

index c2e86cdea1a31e1ba760cd705a8d9fb21703f2f7..0d1282f74cf0fc49caf430eba5d864017007e41f 100644 (file)
@@ -581,7 +581,7 @@ impl EncodedPassword {
 }
 
 #[cfg(test)]
-mod test {
+mod tests {
     use std::{io::Cursor, path::Path};
 
     use crate::crypto::{EncodedPassword, EncryptedFile, FileType};
index 5b3bbe2cccc51f7815cb444a8f106f7f6ae8c1ca..93cb275c9ea9aeb33855c207c097d3bf36bab5b0 100644 (file)
@@ -54,7 +54,7 @@ pub struct DisplayDatum<'b, B> {
 }
 
 #[cfg(test)]
-mod test;
+mod tests;
 
 pub trait DisplayPlain {
     fn display_plain(&self) -> DisplayPlainF64;
diff --git a/rust/pspp/src/format/display/test.rs b/rust/pspp/src/format/display/test.rs
deleted file mode 100644 (file)
index 9ddd304..0000000
+++ /dev/null
@@ -1,1757 +0,0 @@
-// PSPP - a program for statistical analysis.
-// Copyright (C) 2025 Free Software Foundation, Inc.
-//
-// This program is free software: you can redistribute it and/or modify it under
-// the terms of the GNU General Public License as published by the Free Software
-// Foundation, either version 3 of the License, or (at your option) any later
-// version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-// FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
-// details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program.  If not, see <http://www.gnu.org/licenses/>.
-
-use std::{fmt::Write, fs::File, io::BufRead, path::Path};
-
-use binrw::{io::BufReader, Endian};
-use encoding_rs::UTF_8;
-use itertools::Itertools;
-use smallstr::SmallString;
-use smallvec::SmallVec;
-
-use crate::{
-    data::{ByteString, Datum, WithEncoding},
-    format::{AbstractFormat, Epoch, Format, Settings, Type, UncheckedFormat, CC},
-    lex::{scan::StringScanner, segment::Syntax, Punct, Token},
-    settings::EndianSettings,
-};
-
-fn test(name: &str) {
-    let filename = Path::new("src/format/testdata/display").join(name);
-    let input = BufReader::new(File::open(&filename).unwrap());
-    let settings = Settings::default()
-        .with_cc(CC::A, ",,,".parse().unwrap())
-        .with_cc(CC::B, "-,[[[,]]],-".parse().unwrap())
-        .with_cc(CC::C, "((,[,],))".parse().unwrap())
-        .with_cc(CC::D, ",XXX,,-".parse().unwrap())
-        .with_cc(CC::E, ",,YYY,-".parse().unwrap());
-    let endian = EndianSettings::new(Endian::Big);
-    let mut value = Some(0.0);
-    let mut value_name = String::new();
-    for (line, line_number) in input.lines().map(|r| r.unwrap()).zip(1..) {
-        let line = line.trim();
-        let tokens = StringScanner::new(line, Syntax::Interactive, true)
-            .unwrapped()
-            .collect::<Vec<_>>();
-        match &tokens[0] {
-            Token::Number(number) => {
-                value = if let Some(Token::Punct(Punct::Exp)) = tokens.get(1) {
-                    assert_eq!(tokens.len(), 3);
-                    let exponent = tokens[2].as_number().unwrap();
-                    Some(number.powf(exponent))
-                } else {
-                    assert_eq!(tokens.len(), 1);
-                    Some(*number)
-                };
-                value_name = String::from(line);
-            }
-            Token::End => {
-                value = None;
-                value_name = String::from(line);
-            }
-            Token::Id(id) => {
-                let format: UncheckedFormat =
-                    id.0.as_str()
-                        .parse::<AbstractFormat>()
-                        .unwrap()
-                        .try_into()
-                        .unwrap();
-                let format: Format = format.try_into().unwrap();
-                assert_eq!(tokens.get(1), Some(&Token::Punct(Punct::Colon)));
-                let expected = tokens[2].as_string().unwrap();
-                let actual = Datum::<WithEncoding<ByteString>>::Number(value)
-                    .display(format)
-                    .with_settings(&settings)
-                    .with_endian(endian)
-                    .to_string();
-                assert_eq!(
-                    expected,
-                    &actual,
-                    "{}:{line_number}: Error formatting {value_name} as {format}",
-                    filename.display()
-                );
-            }
-            _ => panic!(),
-        }
-    }
-}
-
-#[test]
-fn comma() {
-    test("comma.txt");
-}
-
-#[test]
-fn dot() {
-    test("dot.txt");
-}
-
-#[test]
-fn dollar() {
-    test("dollar.txt");
-}
-
-#[test]
-fn pct() {
-    test("pct.txt");
-}
-
-#[test]
-fn e() {
-    test("e.txt");
-}
-
-#[test]
-fn f() {
-    test("f.txt");
-}
-
-#[test]
-fn n() {
-    test("n.txt");
-}
-
-#[test]
-fn z() {
-    test("z.txt");
-}
-
-#[test]
-fn cca() {
-    test("cca.txt");
-}
-
-#[test]
-fn ccb() {
-    test("ccb.txt");
-}
-
-#[test]
-fn ccc() {
-    test("ccc.txt");
-}
-
-#[test]
-fn ccd() {
-    test("ccd.txt");
-}
-
-#[test]
-fn cce() {
-    test("cce.txt");
-}
-
-#[test]
-fn pibhex() {
-    test("pibhex.txt");
-}
-
-#[test]
-fn rbhex() {
-    test("rbhex.txt");
-}
-
-#[test]
-fn leading_zeros() {
-    struct Test {
-        with_leading_zero: Settings,
-        without_leading_zero: Settings,
-    }
-
-    impl Test {
-        fn new() -> Self {
-            Self {
-                without_leading_zero: Settings::default(),
-                with_leading_zero: Settings::default().with_leading_zero(true),
-            }
-        }
-
-        fn test_with_settings(value: f64, expected: [&str; 2], settings: &Settings) {
-            let value = Datum::<WithEncoding<ByteString>>::from(value);
-            for (expected, d) in expected.into_iter().zip([2, 1].into_iter()) {
-                assert_eq!(
-                    &value
-                        .display(Format::new(Type::F, 5, d).unwrap())
-                        .with_settings(settings)
-                        .to_string(),
-                    expected
-                );
-            }
-        }
-        fn test(&self, value: f64, without: [&str; 2], with: [&str; 2]) {
-            Self::test_with_settings(value, without, &self.without_leading_zero);
-            Self::test_with_settings(value, with, &self.with_leading_zero);
-        }
-    }
-    let test = Test::new();
-    test.test(0.5, ["  .50", "   .5"], [" 0.50", "  0.5"]);
-    test.test(0.99, ["  .99", "  1.0"], [" 0.99", "  1.0"]);
-    test.test(0.01, ["  .01", "   .0"], [" 0.01", "  0.0"]);
-    test.test(0.0, ["  .00", "   .0"], [" 0.00", "  0.0"]);
-    test.test(-0.0, ["  .00", "   .0"], [" 0.00", "  0.0"]);
-    test.test(-0.5, [" -.50", "  -.5"], ["-0.50", " -0.5"]);
-    test.test(-0.99, [" -.99", " -1.0"], ["-0.99", " -1.0"]);
-    test.test(-0.01, [" -.01", "   .0"], ["-0.01", "  0.0"]);
-}
-
-#[test]
-fn non_ascii_cc() {
-    fn test(settings: &Settings, value: f64, expected: &str) {
-        assert_eq!(
-            &Datum::<WithEncoding<ByteString>>::from(value)
-                .display(Format::new(Type::CC(CC::A), 10, 2).unwrap())
-                .with_settings(settings)
-                .to_string(),
-            expected
-        );
-    }
-
-    let settings = Settings::default().with_cc(CC::A, "«,¥,€,»".parse().unwrap());
-    test(&settings, 1.0, "   ¥1.00€ ");
-    test(&settings, -1.0, "  «¥1.00€»");
-    test(&settings, 1.5, "   ¥1.50€ ");
-    test(&settings, -1.5, "  «¥1.50€»");
-    test(&settings, 0.75, "    ¥.75€ ");
-    test(&settings, 1.5e10, " ¥2E+010€ ");
-    test(&settings, -1.5e10, "«¥2E+010€»");
-}
-
-fn test_binhex(name: &str) {
-    let filename = Path::new("src/format/testdata/display").join(name);
-    let input = BufReader::new(File::open(&filename).unwrap());
-    let mut value = None;
-    let mut value_name = String::new();
-
-    let endian = EndianSettings::new(Endian::Big);
-    for (line, line_number) in input.lines().map(|r| r.unwrap()).zip(1..) {
-        let line = line.trim();
-        let tokens = StringScanner::new(line, Syntax::Interactive, true)
-            .unwrapped()
-            .collect::<Vec<_>>();
-        match &tokens[0] {
-            Token::Number(number) => {
-                value = Some(*number);
-                value_name = String::from(line);
-            }
-            Token::End => {
-                value = None;
-                value_name = String::from(line);
-            }
-            Token::Id(id) => {
-                let format: UncheckedFormat =
-                    id.0.as_str()
-                        .parse::<AbstractFormat>()
-                        .unwrap()
-                        .try_into()
-                        .unwrap();
-                let format: Format = format.try_into().unwrap();
-                assert_eq!(tokens.get(1), Some(&Token::Punct(Punct::Colon)));
-                let expected = tokens[2].as_string().unwrap();
-                let mut actual = SmallVec::<[u8; 16]>::new();
-                Datum::<WithEncoding<ByteString>>::Number(value)
-                    .display(format)
-                    .with_endian(endian)
-                    .write(&mut actual, UTF_8)
-                    .unwrap();
-                let mut actual_s = SmallString::<[u8; 32]>::new();
-                for b in actual {
-                    write!(&mut actual_s, "{:02x}", b).unwrap();
-                }
-                assert_eq!(
-                    expected,
-                    &*actual_s,
-                    "{}:{line_number}: Error formatting {value_name} as {format}",
-                    filename.display()
-                );
-            }
-            _ => panic!(),
-        }
-    }
-}
-
-#[test]
-fn p() {
-    test_binhex("p.txt");
-}
-
-#[test]
-fn pk() {
-    test_binhex("pk.txt");
-}
-
-#[test]
-fn ib() {
-    test_binhex("ib.txt");
-}
-
-#[test]
-fn pib() {
-    test_binhex("pib.txt");
-}
-
-#[test]
-fn rb() {
-    test_binhex("rb.txt");
-}
-
-fn test_dates(format: Format, expect: &[&str]) {
-    let settings = Settings::default().with_epoch(Epoch(1930));
-    let parser = Type::DateTime.parser(UTF_8).with_settings(&settings);
-    static INPUTS: &[&str; 20] = &[
-        "10-6-1648 0:0:0",
-        "30-6-1680 4:50:38.12301",
-        "24-7-1716 12:31:35.23453",
-        "19-6-1768 12:47:53.34505",
-        "2-8-1819 1:26:0.45615",
-        "27-3-1839 20:58:11.56677",
-        "19-4-1903 7:36:5.18964",
-        "25-8-1929 15:43:49.83132",
-        "29-9-1941 4:25:9.01293",
-        "19-4-1943 6:49:27.52375",
-        "7-10-1943 2:57:52.01565",
-        "17-3-1992 16:45:44.86529",
-        "25-2-1996 21:30:57.82047",
-        "29-9-41 4:25:9.15395",
-        "19-4-43 6:49:27.10533",
-        "7-10-43 2:57:52.48229",
-        "17-3-92 16:45:44.65827",
-        "25-2-96 21:30:57.58219",
-        "10-11-2038 22:30:4.18347",
-        "18-7-2094 1:56:51.59319",
-    ];
-    assert_eq!(expect.len(), INPUTS.len());
-    for (input, expect) in INPUTS.iter().copied().zip_eq(expect.iter().copied()) {
-        let value = parser.parse(input).unwrap().with_encoding(UTF_8);
-        let formatted = value.display(format).with_settings(&settings).to_string();
-        assert_eq!(&formatted, expect);
-    }
-}
-
-#[test]
-fn date9() {
-    test_dates(
-        Format::new(Type::Date, 9, 0).unwrap(),
-        &[
-            "*********",
-            "*********",
-            "*********",
-            "*********",
-            "*********",
-            "*********",
-            "*********",
-            "*********",
-            "29-SEP-41",
-            "19-APR-43",
-            "07-OCT-43",
-            "17-MAR-92",
-            "25-FEB-96",
-            "29-SEP-41",
-            "19-APR-43",
-            "07-OCT-43",
-            "17-MAR-92",
-            "25-FEB-96",
-            "*********",
-            "*********",
-        ],
-    );
-}
-
-#[test]
-fn date11() {
-    test_dates(
-        Format::new(Type::Date, 11, 0).unwrap(),
-        &[
-            "10-JUN-1648",
-            "30-JUN-1680",
-            "24-JUL-1716",
-            "19-JUN-1768",
-            "02-AUG-1819",
-            "27-MAR-1839",
-            "19-APR-1903",
-            "25-AUG-1929",
-            "29-SEP-1941",
-            "19-APR-1943",
-            "07-OCT-1943",
-            "17-MAR-1992",
-            "25-FEB-1996",
-            "29-SEP-1941",
-            "19-APR-1943",
-            "07-OCT-1943",
-            "17-MAR-1992",
-            "25-FEB-1996",
-            "10-NOV-2038",
-            "18-JUL-2094",
-        ],
-    );
-}
-
-#[test]
-fn adate8() {
-    test_dates(
-        Format::new(Type::ADate, 8, 0).unwrap(),
-        &[
-            "********", "********", "********", "********", "********", "********", "********",
-            "********", "09/29/41", "04/19/43", "10/07/43", "03/17/92", "02/25/96", "09/29/41",
-            "04/19/43", "10/07/43", "03/17/92", "02/25/96", "********", "********",
-        ],
-    );
-}
-
-#[test]
-fn adate10() {
-    test_dates(
-        Format::new(Type::ADate, 10, 0).unwrap(),
-        &[
-            "06/10/1648",
-            "06/30/1680",
-            "07/24/1716",
-            "06/19/1768",
-            "08/02/1819",
-            "03/27/1839",
-            "04/19/1903",
-            "08/25/1929",
-            "09/29/1941",
-            "04/19/1943",
-            "10/07/1943",
-            "03/17/1992",
-            "02/25/1996",
-            "09/29/1941",
-            "04/19/1943",
-            "10/07/1943",
-            "03/17/1992",
-            "02/25/1996",
-            "11/10/2038",
-            "07/18/2094",
-        ],
-    );
-}
-
-#[test]
-fn edate8() {
-    test_dates(
-        Format::new(Type::EDate, 8, 0).unwrap(),
-        &[
-            "********", "********", "********", "********", "********", "********", "********",
-            "********", "29.09.41", "19.04.43", "07.10.43", "17.03.92", "25.02.96", "29.09.41",
-            "19.04.43", "07.10.43", "17.03.92", "25.02.96", "********", "********",
-        ],
-    );
-}
-
-#[test]
-fn edate10() {
-    test_dates(
-        Format::new(Type::EDate, 10, 0).unwrap(),
-        &[
-            "10.06.1648",
-            "30.06.1680",
-            "24.07.1716",
-            "19.06.1768",
-            "02.08.1819",
-            "27.03.1839",
-            "19.04.1903",
-            "25.08.1929",
-            "29.09.1941",
-            "19.04.1943",
-            "07.10.1943",
-            "17.03.1992",
-            "25.02.1996",
-            "29.09.1941",
-            "19.04.1943",
-            "07.10.1943",
-            "17.03.1992",
-            "25.02.1996",
-            "10.11.2038",
-            "18.07.2094",
-        ],
-    );
-}
-
-#[test]
-fn jdate5() {
-    test_dates(
-        Format::new(Type::JDate, 5, 0).unwrap(),
-        &[
-            "*****", "*****", "*****", "*****", "*****", "*****", "*****", "*****", "41272",
-            "43109", "43280", "92077", "96056", "41272", "43109", "43280", "92077", "96056",
-            "*****", "*****",
-        ],
-    );
-}
-
-#[test]
-fn jdate7() {
-    test_dates(
-        Format::new(Type::JDate, 7, 0).unwrap(),
-        &[
-            "1648162", "1680182", "1716206", "1768171", "1819214", "1839086", "1903109", "1929237",
-            "1941272", "1943109", "1943280", "1992077", "1996056", "1941272", "1943109", "1943280",
-            "1992077", "1996056", "2038314", "2094199",
-        ],
-    );
-}
-
-#[test]
-fn sdate8() {
-    test_dates(
-        Format::new(Type::SDate, 8, 0).unwrap(),
-        &[
-            "********", "********", "********", "********", "********", "********", "********",
-            "********", "41/09/29", "43/04/19", "43/10/07", "92/03/17", "96/02/25", "41/09/29",
-            "43/04/19", "43/10/07", "92/03/17", "96/02/25", "********", "********",
-        ],
-    );
-}
-
-#[test]
-fn sdate10() {
-    test_dates(
-        Format::new(Type::SDate, 10, 0).unwrap(),
-        &[
-            "1648/06/10",
-            "1680/06/30",
-            "1716/07/24",
-            "1768/06/19",
-            "1819/08/02",
-            "1839/03/27",
-            "1903/04/19",
-            "1929/08/25",
-            "1941/09/29",
-            "1943/04/19",
-            "1943/10/07",
-            "1992/03/17",
-            "1996/02/25",
-            "1941/09/29",
-            "1943/04/19",
-            "1943/10/07",
-            "1992/03/17",
-            "1996/02/25",
-            "2038/11/10",
-            "2094/07/18",
-        ],
-    );
-}
-
-#[test]
-fn qyr6() {
-    test_dates(
-        Format::new(Type::QYr, 6, 0).unwrap(),
-        &[
-            "******", "******", "******", "******", "******", "******", "******", "******",
-            "3 Q 41", "2 Q 43", "4 Q 43", "1 Q 92", "1 Q 96", "3 Q 41", "2 Q 43", "4 Q 43",
-            "1 Q 92", "1 Q 96", "******", "******",
-        ],
-    );
-}
-
-#[test]
-fn qyr8() {
-    test_dates(
-        Format::new(Type::QYr, 8, 0).unwrap(),
-        &[
-            "2 Q 1648", "2 Q 1680", "3 Q 1716", "2 Q 1768", "3 Q 1819", "1 Q 1839", "2 Q 1903",
-            "3 Q 1929", "3 Q 1941", "2 Q 1943", "4 Q 1943", "1 Q 1992", "1 Q 1996", "3 Q 1941",
-            "2 Q 1943", "4 Q 1943", "1 Q 1992", "1 Q 1996", "4 Q 2038", "3 Q 2094",
-        ],
-    );
-}
-
-#[test]
-fn moyr6() {
-    test_dates(
-        Format::new(Type::MoYr, 6, 0).unwrap(),
-        &[
-            "******", "******", "******", "******", "******", "******", "******", "******",
-            "SEP 41", "APR 43", "OCT 43", "MAR 92", "FEB 96", "SEP 41", "APR 43", "OCT 43",
-            "MAR 92", "FEB 96", "******", "******",
-        ],
-    );
-}
-
-#[test]
-fn moyr8() {
-    test_dates(
-        Format::new(Type::MoYr, 8, 0).unwrap(),
-        &[
-            "JUN 1648", "JUN 1680", "JUL 1716", "JUN 1768", "AUG 1819", "MAR 1839", "APR 1903",
-            "AUG 1929", "SEP 1941", "APR 1943", "OCT 1943", "MAR 1992", "FEB 1996", "SEP 1941",
-            "APR 1943", "OCT 1943", "MAR 1992", "FEB 1996", "NOV 2038", "JUL 2094",
-        ],
-    );
-}
-
-#[test]
-fn wkyr8() {
-    test_dates(
-        Format::new(Type::WkYr, 8, 0).unwrap(),
-        &[
-            "********", "********", "********", "********", "********", "********", "********",
-            "********", "39 WK 41", "16 WK 43", "40 WK 43", "11 WK 92", " 8 WK 96", "39 WK 41",
-            "16 WK 43", "40 WK 43", "11 WK 92", " 8 WK 96", "********", "********",
-        ],
-    );
-}
-
-#[test]
-fn wkyr10() {
-    test_dates(
-        Format::new(Type::WkYr, 10, 0).unwrap(),
-        &[
-            "24 WK 1648",
-            "26 WK 1680",
-            "30 WK 1716",
-            "25 WK 1768",
-            "31 WK 1819",
-            "13 WK 1839",
-            "16 WK 1903",
-            "34 WK 1929",
-            "39 WK 1941",
-            "16 WK 1943",
-            "40 WK 1943",
-            "11 WK 1992",
-            " 8 WK 1996",
-            "39 WK 1941",
-            "16 WK 1943",
-            "40 WK 1943",
-            "11 WK 1992",
-            " 8 WK 1996",
-            "45 WK 2038",
-            "29 WK 2094",
-        ],
-    );
-}
-
-#[test]
-fn datetime17() {
-    test_dates(
-        Format::new(Type::DateTime, 17, 0).unwrap(),
-        &[
-            "10-JUN-1648 00:00",
-            "30-JUN-1680 04:50",
-            "24-JUL-1716 12:31",
-            "19-JUN-1768 12:47",
-            "02-AUG-1819 01:26",
-            "27-MAR-1839 20:58",
-            "19-APR-1903 07:36",
-            "25-AUG-1929 15:43",
-            "29-SEP-1941 04:25",
-            "19-APR-1943 06:49",
-            "07-OCT-1943 02:57",
-            "17-MAR-1992 16:45",
-            "25-FEB-1996 21:30",
-            "29-SEP-1941 04:25",
-            "19-APR-1943 06:49",
-            "07-OCT-1943 02:57",
-            "17-MAR-1992 16:45",
-            "25-FEB-1996 21:30",
-            "10-NOV-2038 22:30",
-            "18-JUL-2094 01:56",
-        ],
-    );
-}
-
-#[test]
-fn datetime18() {
-    test_dates(
-        Format::new(Type::DateTime, 18, 0).unwrap(),
-        &[
-            " 10-JUN-1648 00:00",
-            " 30-JUN-1680 04:50",
-            " 24-JUL-1716 12:31",
-            " 19-JUN-1768 12:47",
-            " 02-AUG-1819 01:26",
-            " 27-MAR-1839 20:58",
-            " 19-APR-1903 07:36",
-            " 25-AUG-1929 15:43",
-            " 29-SEP-1941 04:25",
-            " 19-APR-1943 06:49",
-            " 07-OCT-1943 02:57",
-            " 17-MAR-1992 16:45",
-            " 25-FEB-1996 21:30",
-            " 29-SEP-1941 04:25",
-            " 19-APR-1943 06:49",
-            " 07-OCT-1943 02:57",
-            " 17-MAR-1992 16:45",
-            " 25-FEB-1996 21:30",
-            " 10-NOV-2038 22:30",
-            " 18-JUL-2094 01:56",
-        ],
-    );
-}
-
-#[test]
-fn datetime19() {
-    test_dates(
-        Format::new(Type::DateTime, 19, 0).unwrap(),
-        &[
-            "  10-JUN-1648 00:00",
-            "  30-JUN-1680 04:50",
-            "  24-JUL-1716 12:31",
-            "  19-JUN-1768 12:47",
-            "  02-AUG-1819 01:26",
-            "  27-MAR-1839 20:58",
-            "  19-APR-1903 07:36",
-            "  25-AUG-1929 15:43",
-            "  29-SEP-1941 04:25",
-            "  19-APR-1943 06:49",
-            "  07-OCT-1943 02:57",
-            "  17-MAR-1992 16:45",
-            "  25-FEB-1996 21:30",
-            "  29-SEP-1941 04:25",
-            "  19-APR-1943 06:49",
-            "  07-OCT-1943 02:57",
-            "  17-MAR-1992 16:45",
-            "  25-FEB-1996 21:30",
-            "  10-NOV-2038 22:30",
-            "  18-JUL-2094 01:56",
-        ],
-    );
-}
-
-#[test]
-fn datetime20() {
-    test_dates(
-        Format::new(Type::DateTime, 20, 0).unwrap(),
-        &[
-            "10-JUN-1648 00:00:00",
-            "30-JUN-1680 04:50:38",
-            "24-JUL-1716 12:31:35",
-            "19-JUN-1768 12:47:53",
-            "02-AUG-1819 01:26:00",
-            "27-MAR-1839 20:58:11",
-            "19-APR-1903 07:36:05",
-            "25-AUG-1929 15:43:49",
-            "29-SEP-1941 04:25:09",
-            "19-APR-1943 06:49:27",
-            "07-OCT-1943 02:57:52",
-            "17-MAR-1992 16:45:44",
-            "25-FEB-1996 21:30:57",
-            "29-SEP-1941 04:25:09",
-            "19-APR-1943 06:49:27",
-            "07-OCT-1943 02:57:52",
-            "17-MAR-1992 16:45:44",
-            "25-FEB-1996 21:30:57",
-            "10-NOV-2038 22:30:04",
-            "18-JUL-2094 01:56:51",
-        ],
-    );
-}
-
-#[test]
-fn datetime21() {
-    test_dates(
-        Format::new(Type::DateTime, 21, 0).unwrap(),
-        &[
-            " 10-JUN-1648 00:00:00",
-            " 30-JUN-1680 04:50:38",
-            " 24-JUL-1716 12:31:35",
-            " 19-JUN-1768 12:47:53",
-            " 02-AUG-1819 01:26:00",
-            " 27-MAR-1839 20:58:11",
-            " 19-APR-1903 07:36:05",
-            " 25-AUG-1929 15:43:49",
-            " 29-SEP-1941 04:25:09",
-            " 19-APR-1943 06:49:27",
-            " 07-OCT-1943 02:57:52",
-            " 17-MAR-1992 16:45:44",
-            " 25-FEB-1996 21:30:57",
-            " 29-SEP-1941 04:25:09",
-            " 19-APR-1943 06:49:27",
-            " 07-OCT-1943 02:57:52",
-            " 17-MAR-1992 16:45:44",
-            " 25-FEB-1996 21:30:57",
-            " 10-NOV-2038 22:30:04",
-            " 18-JUL-2094 01:56:51",
-        ],
-    );
-}
-
-#[test]
-fn datetime22() {
-    test_dates(
-        Format::new(Type::DateTime, 22, 0).unwrap(),
-        &[
-            "  10-JUN-1648 00:00:00",
-            "  30-JUN-1680 04:50:38",
-            "  24-JUL-1716 12:31:35",
-            "  19-JUN-1768 12:47:53",
-            "  02-AUG-1819 01:26:00",
-            "  27-MAR-1839 20:58:11",
-            "  19-APR-1903 07:36:05",
-            "  25-AUG-1929 15:43:49",
-            "  29-SEP-1941 04:25:09",
-            "  19-APR-1943 06:49:27",
-            "  07-OCT-1943 02:57:52",
-            "  17-MAR-1992 16:45:44",
-            "  25-FEB-1996 21:30:57",
-            "  29-SEP-1941 04:25:09",
-            "  19-APR-1943 06:49:27",
-            "  07-OCT-1943 02:57:52",
-            "  17-MAR-1992 16:45:44",
-            "  25-FEB-1996 21:30:57",
-            "  10-NOV-2038 22:30:04",
-            "  18-JUL-2094 01:56:51",
-        ],
-    );
-}
-
-#[test]
-fn datetime22_1() {
-    test_dates(
-        Format::new(Type::DateTime, 22, 1).unwrap(),
-        &[
-            "10-JUN-1648 00:00:00.0",
-            "30-JUN-1680 04:50:38.1",
-            "24-JUL-1716 12:31:35.2",
-            "19-JUN-1768 12:47:53.3",
-            "02-AUG-1819 01:26:00.5",
-            "27-MAR-1839 20:58:11.6",
-            "19-APR-1903 07:36:05.2",
-            "25-AUG-1929 15:43:49.8",
-            "29-SEP-1941 04:25:09.0",
-            "19-APR-1943 06:49:27.5",
-            "07-OCT-1943 02:57:52.0",
-            "17-MAR-1992 16:45:44.9",
-            "25-FEB-1996 21:30:57.8",
-            "29-SEP-1941 04:25:09.2",
-            "19-APR-1943 06:49:27.1",
-            "07-OCT-1943 02:57:52.5",
-            "17-MAR-1992 16:45:44.7",
-            "25-FEB-1996 21:30:57.6",
-            "10-NOV-2038 22:30:04.2",
-            "18-JUL-2094 01:56:51.6",
-        ],
-    );
-}
-
-#[test]
-fn datetime23_2() {
-    test_dates(
-        Format::new(Type::DateTime, 23, 2).unwrap(),
-        &[
-            "10-JUN-1648 00:00:00.00",
-            "30-JUN-1680 04:50:38.12",
-            "24-JUL-1716 12:31:35.23",
-            "19-JUN-1768 12:47:53.35",
-            "02-AUG-1819 01:26:00.46",
-            "27-MAR-1839 20:58:11.57",
-            "19-APR-1903 07:36:05.19",
-            "25-AUG-1929 15:43:49.83",
-            "29-SEP-1941 04:25:09.01",
-            "19-APR-1943 06:49:27.52",
-            "07-OCT-1943 02:57:52.02",
-            "17-MAR-1992 16:45:44.87",
-            "25-FEB-1996 21:30:57.82",
-            "29-SEP-1941 04:25:09.15",
-            "19-APR-1943 06:49:27.11",
-            "07-OCT-1943 02:57:52.48",
-            "17-MAR-1992 16:45:44.66",
-            "25-FEB-1996 21:30:57.58",
-            "10-NOV-2038 22:30:04.18",
-            "18-JUL-2094 01:56:51.59",
-        ],
-    );
-}
-
-#[test]
-fn datetime24_3() {
-    test_dates(
-        Format::new(Type::DateTime, 24, 3).unwrap(),
-        &[
-            "10-JUN-1648 00:00:00.000",
-            "30-JUN-1680 04:50:38.123",
-            "24-JUL-1716 12:31:35.235",
-            "19-JUN-1768 12:47:53.345",
-            "02-AUG-1819 01:26:00.456",
-            "27-MAR-1839 20:58:11.567",
-            "19-APR-1903 07:36:05.190",
-            "25-AUG-1929 15:43:49.831",
-            "29-SEP-1941 04:25:09.013",
-            "19-APR-1943 06:49:27.524",
-            "07-OCT-1943 02:57:52.016",
-            "17-MAR-1992 16:45:44.865",
-            "25-FEB-1996 21:30:57.820",
-            "29-SEP-1941 04:25:09.154",
-            "19-APR-1943 06:49:27.105",
-            "07-OCT-1943 02:57:52.482",
-            "17-MAR-1992 16:45:44.658",
-            "25-FEB-1996 21:30:57.582",
-            "10-NOV-2038 22:30:04.183",
-            "18-JUL-2094 01:56:51.593",
-        ],
-    );
-}
-
-#[test]
-fn datetime25_4() {
-    test_dates(
-        Format::new(Type::DateTime, 25, 4).unwrap(),
-        &[
-            "10-JUN-1648 00:00:00.0000",
-            "30-JUN-1680 04:50:38.1230",
-            "24-JUL-1716 12:31:35.2345",
-            "19-JUN-1768 12:47:53.3450",
-            "02-AUG-1819 01:26:00.4562",
-            "27-MAR-1839 20:58:11.5668",
-            "19-APR-1903 07:36:05.1896",
-            "25-AUG-1929 15:43:49.8313",
-            "29-SEP-1941 04:25:09.0129",
-            "19-APR-1943 06:49:27.5238",
-            "07-OCT-1943 02:57:52.0156",
-            "17-MAR-1992 16:45:44.8653",
-            "25-FEB-1996 21:30:57.8205",
-            "29-SEP-1941 04:25:09.1539",
-            "19-APR-1943 06:49:27.1053",
-            "07-OCT-1943 02:57:52.4823",
-            "17-MAR-1992 16:45:44.6583",
-            "25-FEB-1996 21:30:57.5822",
-            "10-NOV-2038 22:30:04.1835",
-            "18-JUL-2094 01:56:51.5932",
-        ],
-    );
-}
-
-#[test]
-fn datetime26_5() {
-    test_dates(
-        Format::new(Type::DateTime, 26, 5).unwrap(),
-        &[
-            "10-JUN-1648 00:00:00.00000",
-            "30-JUN-1680 04:50:38.12301",
-            "24-JUL-1716 12:31:35.23453",
-            "19-JUN-1768 12:47:53.34505",
-            "02-AUG-1819 01:26:00.45615",
-            "27-MAR-1839 20:58:11.56677",
-            "19-APR-1903 07:36:05.18964",
-            "25-AUG-1929 15:43:49.83132",
-            "29-SEP-1941 04:25:09.01293",
-            "19-APR-1943 06:49:27.52375",
-            "07-OCT-1943 02:57:52.01565",
-            "17-MAR-1992 16:45:44.86529",
-            "25-FEB-1996 21:30:57.82047",
-            "29-SEP-1941 04:25:09.15395",
-            "19-APR-1943 06:49:27.10533",
-            "07-OCT-1943 02:57:52.48229",
-            "17-MAR-1992 16:45:44.65827",
-            "25-FEB-1996 21:30:57.58219",
-            "10-NOV-2038 22:30:04.18347",
-            "18-JUL-2094 01:56:51.59319",
-        ],
-    );
-}
-
-#[test]
-fn ymdhms16() {
-    test_dates(
-        Format::new(Type::YmdHms, 16, 0).unwrap(),
-        &[
-            "1648-06-10 00:00",
-            "1680-06-30 04:50",
-            "1716-07-24 12:31",
-            "1768-06-19 12:47",
-            "1819-08-02 01:26",
-            "1839-03-27 20:58",
-            "1903-04-19 07:36",
-            "1929-08-25 15:43",
-            "1941-09-29 04:25",
-            "1943-04-19 06:49",
-            "1943-10-07 02:57",
-            "1992-03-17 16:45",
-            "1996-02-25 21:30",
-            "1941-09-29 04:25",
-            "1943-04-19 06:49",
-            "1943-10-07 02:57",
-            "1992-03-17 16:45",
-            "1996-02-25 21:30",
-            "2038-11-10 22:30",
-            "2094-07-18 01:56",
-        ],
-    );
-}
-
-#[test]
-fn ymdhms17() {
-    test_dates(
-        Format::new(Type::YmdHms, 17, 0).unwrap(),
-        &[
-            " 1648-06-10 00:00",
-            " 1680-06-30 04:50",
-            " 1716-07-24 12:31",
-            " 1768-06-19 12:47",
-            " 1819-08-02 01:26",
-            " 1839-03-27 20:58",
-            " 1903-04-19 07:36",
-            " 1929-08-25 15:43",
-            " 1941-09-29 04:25",
-            " 1943-04-19 06:49",
-            " 1943-10-07 02:57",
-            " 1992-03-17 16:45",
-            " 1996-02-25 21:30",
-            " 1941-09-29 04:25",
-            " 1943-04-19 06:49",
-            " 1943-10-07 02:57",
-            " 1992-03-17 16:45",
-            " 1996-02-25 21:30",
-            " 2038-11-10 22:30",
-            " 2094-07-18 01:56",
-        ],
-    );
-}
-
-#[test]
-fn ymdhms18() {
-    test_dates(
-        Format::new(Type::YmdHms, 18, 0).unwrap(),
-        &[
-            "  1648-06-10 00:00",
-            "  1680-06-30 04:50",
-            "  1716-07-24 12:31",
-            "  1768-06-19 12:47",
-            "  1819-08-02 01:26",
-            "  1839-03-27 20:58",
-            "  1903-04-19 07:36",
-            "  1929-08-25 15:43",
-            "  1941-09-29 04:25",
-            "  1943-04-19 06:49",
-            "  1943-10-07 02:57",
-            "  1992-03-17 16:45",
-            "  1996-02-25 21:30",
-            "  1941-09-29 04:25",
-            "  1943-04-19 06:49",
-            "  1943-10-07 02:57",
-            "  1992-03-17 16:45",
-            "  1996-02-25 21:30",
-            "  2038-11-10 22:30",
-            "  2094-07-18 01:56",
-        ],
-    );
-}
-
-#[test]
-fn ymdhms19() {
-    test_dates(
-        Format::new(Type::YmdHms, 19, 0).unwrap(),
-        &[
-            "1648-06-10 00:00:00",
-            "1680-06-30 04:50:38",
-            "1716-07-24 12:31:35",
-            "1768-06-19 12:47:53",
-            "1819-08-02 01:26:00",
-            "1839-03-27 20:58:11",
-            "1903-04-19 07:36:05",
-            "1929-08-25 15:43:49",
-            "1941-09-29 04:25:09",
-            "1943-04-19 06:49:27",
-            "1943-10-07 02:57:52",
-            "1992-03-17 16:45:44",
-            "1996-02-25 21:30:57",
-            "1941-09-29 04:25:09",
-            "1943-04-19 06:49:27",
-            "1943-10-07 02:57:52",
-            "1992-03-17 16:45:44",
-            "1996-02-25 21:30:57",
-            "2038-11-10 22:30:04",
-            "2094-07-18 01:56:51",
-        ],
-    );
-}
-
-#[test]
-fn ymdhms20() {
-    test_dates(
-        Format::new(Type::YmdHms, 20, 0).unwrap(),
-        &[
-            " 1648-06-10 00:00:00",
-            " 1680-06-30 04:50:38",
-            " 1716-07-24 12:31:35",
-            " 1768-06-19 12:47:53",
-            " 1819-08-02 01:26:00",
-            " 1839-03-27 20:58:11",
-            " 1903-04-19 07:36:05",
-            " 1929-08-25 15:43:49",
-            " 1941-09-29 04:25:09",
-            " 1943-04-19 06:49:27",
-            " 1943-10-07 02:57:52",
-            " 1992-03-17 16:45:44",
-            " 1996-02-25 21:30:57",
-            " 1941-09-29 04:25:09",
-            " 1943-04-19 06:49:27",
-            " 1943-10-07 02:57:52",
-            " 1992-03-17 16:45:44",
-            " 1996-02-25 21:30:57",
-            " 2038-11-10 22:30:04",
-            " 2094-07-18 01:56:51",
-        ],
-    );
-}
-
-#[test]
-fn ymdhms21() {
-    test_dates(
-        Format::new(Type::YmdHms, 21, 0).unwrap(),
-        &[
-            "  1648-06-10 00:00:00",
-            "  1680-06-30 04:50:38",
-            "  1716-07-24 12:31:35",
-            "  1768-06-19 12:47:53",
-            "  1819-08-02 01:26:00",
-            "  1839-03-27 20:58:11",
-            "  1903-04-19 07:36:05",
-            "  1929-08-25 15:43:49",
-            "  1941-09-29 04:25:09",
-            "  1943-04-19 06:49:27",
-            "  1943-10-07 02:57:52",
-            "  1992-03-17 16:45:44",
-            "  1996-02-25 21:30:57",
-            "  1941-09-29 04:25:09",
-            "  1943-04-19 06:49:27",
-            "  1943-10-07 02:57:52",
-            "  1992-03-17 16:45:44",
-            "  1996-02-25 21:30:57",
-            "  2038-11-10 22:30:04",
-            "  2094-07-18 01:56:51",
-        ],
-    );
-}
-
-#[test]
-fn ymdhms21_1() {
-    test_dates(
-        Format::new(Type::YmdHms, 21, 1).unwrap(),
-        &[
-            "1648-06-10 00:00:00.0",
-            "1680-06-30 04:50:38.1",
-            "1716-07-24 12:31:35.2",
-            "1768-06-19 12:47:53.3",
-            "1819-08-02 01:26:00.5",
-            "1839-03-27 20:58:11.6",
-            "1903-04-19 07:36:05.2",
-            "1929-08-25 15:43:49.8",
-            "1941-09-29 04:25:09.0",
-            "1943-04-19 06:49:27.5",
-            "1943-10-07 02:57:52.0",
-            "1992-03-17 16:45:44.9",
-            "1996-02-25 21:30:57.8",
-            "1941-09-29 04:25:09.2",
-            "1943-04-19 06:49:27.1",
-            "1943-10-07 02:57:52.5",
-            "1992-03-17 16:45:44.7",
-            "1996-02-25 21:30:57.6",
-            "2038-11-10 22:30:04.2",
-            "2094-07-18 01:56:51.6",
-        ],
-    );
-}
-
-#[test]
-fn ymdhms22_2() {
-    test_dates(
-        Format::new(Type::YmdHms, 22, 2).unwrap(),
-        &[
-            "1648-06-10 00:00:00.00",
-            "1680-06-30 04:50:38.12",
-            "1716-07-24 12:31:35.23",
-            "1768-06-19 12:47:53.35",
-            "1819-08-02 01:26:00.46",
-            "1839-03-27 20:58:11.57",
-            "1903-04-19 07:36:05.19",
-            "1929-08-25 15:43:49.83",
-            "1941-09-29 04:25:09.01",
-            "1943-04-19 06:49:27.52",
-            "1943-10-07 02:57:52.02",
-            "1992-03-17 16:45:44.87",
-            "1996-02-25 21:30:57.82",
-            "1941-09-29 04:25:09.15",
-            "1943-04-19 06:49:27.11",
-            "1943-10-07 02:57:52.48",
-            "1992-03-17 16:45:44.66",
-            "1996-02-25 21:30:57.58",
-            "2038-11-10 22:30:04.18",
-            "2094-07-18 01:56:51.59",
-        ],
-    );
-}
-
-#[test]
-fn ymdhms23_3() {
-    test_dates(
-        Format::new(Type::YmdHms, 23, 3).unwrap(),
-        &[
-            "1648-06-10 00:00:00.000",
-            "1680-06-30 04:50:38.123",
-            "1716-07-24 12:31:35.235",
-            "1768-06-19 12:47:53.345",
-            "1819-08-02 01:26:00.456",
-            "1839-03-27 20:58:11.567",
-            "1903-04-19 07:36:05.190",
-            "1929-08-25 15:43:49.831",
-            "1941-09-29 04:25:09.013",
-            "1943-04-19 06:49:27.524",
-            "1943-10-07 02:57:52.016",
-            "1992-03-17 16:45:44.865",
-            "1996-02-25 21:30:57.820",
-            "1941-09-29 04:25:09.154",
-            "1943-04-19 06:49:27.105",
-            "1943-10-07 02:57:52.482",
-            "1992-03-17 16:45:44.658",
-            "1996-02-25 21:30:57.582",
-            "2038-11-10 22:30:04.183",
-            "2094-07-18 01:56:51.593",
-        ],
-    );
-}
-
-#[test]
-fn ymdhms24_4() {
-    test_dates(
-        Format::new(Type::YmdHms, 24, 4).unwrap(),
-        &[
-            "1648-06-10 00:00:00.0000",
-            "1680-06-30 04:50:38.1230",
-            "1716-07-24 12:31:35.2345",
-            "1768-06-19 12:47:53.3450",
-            "1819-08-02 01:26:00.4562",
-            "1839-03-27 20:58:11.5668",
-            "1903-04-19 07:36:05.1896",
-            "1929-08-25 15:43:49.8313",
-            "1941-09-29 04:25:09.0129",
-            "1943-04-19 06:49:27.5238",
-            "1943-10-07 02:57:52.0156",
-            "1992-03-17 16:45:44.8653",
-            "1996-02-25 21:30:57.8205",
-            "1941-09-29 04:25:09.1539",
-            "1943-04-19 06:49:27.1053",
-            "1943-10-07 02:57:52.4823",
-            "1992-03-17 16:45:44.6583",
-            "1996-02-25 21:30:57.5822",
-            "2038-11-10 22:30:04.1835",
-            "2094-07-18 01:56:51.5932",
-        ],
-    );
-}
-
-#[test]
-fn ymdhms25_5() {
-    test_dates(
-        Format::new(Type::YmdHms, 25, 5).unwrap(),
-        &[
-            "1648-06-10 00:00:00.00000",
-            "1680-06-30 04:50:38.12301",
-            "1716-07-24 12:31:35.23453",
-            "1768-06-19 12:47:53.34505",
-            "1819-08-02 01:26:00.45615",
-            "1839-03-27 20:58:11.56677",
-            "1903-04-19 07:36:05.18964",
-            "1929-08-25 15:43:49.83132",
-            "1941-09-29 04:25:09.01293",
-            "1943-04-19 06:49:27.52375",
-            "1943-10-07 02:57:52.01565",
-            "1992-03-17 16:45:44.86529",
-            "1996-02-25 21:30:57.82047",
-            "1941-09-29 04:25:09.15395",
-            "1943-04-19 06:49:27.10533",
-            "1943-10-07 02:57:52.48229",
-            "1992-03-17 16:45:44.65827",
-            "1996-02-25 21:30:57.58219",
-            "2038-11-10 22:30:04.18347",
-            "2094-07-18 01:56:51.59319",
-        ],
-    );
-}
-
-fn test_times(format: Format, name: &str) {
-    let directory = Path::new("src/format/testdata/display");
-    let input_filename = directory.join("time-input.txt");
-    let input = BufReader::new(File::open(&input_filename).unwrap());
-
-    let output_filename = directory.join(name);
-    let output = BufReader::new(File::open(&output_filename).unwrap());
-
-    let parser = Type::DTime.parser(UTF_8);
-    for ((input, expect), line_number) in input
-        .lines()
-        .map(|r| r.unwrap())
-        .zip_eq(output.lines().map(|r| r.unwrap()))
-        .zip(1..)
-    {
-        let formatted = parser
-            .parse(input)
-            .unwrap()
-            .with_encoding(UTF_8)
-            .display(format)
-            .to_string();
-        assert!(
-                formatted == expect,
-                "formatting {}:{line_number} as {format}:\n  actual: {formatted:?}\nexpected: {expect:?}",
-                input_filename.display()
-            );
-    }
-}
-
-#[test]
-fn time5() {
-    test_times(Format::new(Type::Time, 5, 0).unwrap(), "time5.txt");
-}
-
-#[test]
-fn time6() {
-    test_times(Format::new(Type::Time, 6, 0).unwrap(), "time6.txt");
-}
-
-#[test]
-fn time7() {
-    test_times(Format::new(Type::Time, 7, 0).unwrap(), "time7.txt");
-}
-
-#[test]
-fn time8() {
-    test_times(Format::new(Type::Time, 8, 0).unwrap(), "time8.txt");
-}
-
-#[test]
-fn time9() {
-    test_times(Format::new(Type::Time, 9, 0).unwrap(), "time9.txt");
-}
-
-#[test]
-fn time10() {
-    test_times(Format::new(Type::Time, 10, 0).unwrap(), "time10.txt");
-}
-
-#[test]
-fn time10_1() {
-    test_times(Format::new(Type::Time, 10, 1).unwrap(), "time10.1.txt");
-}
-
-#[test]
-fn time11() {
-    test_times(Format::new(Type::Time, 11, 0).unwrap(), "time11.txt");
-}
-
-#[test]
-fn time11_1() {
-    test_times(Format::new(Type::Time, 11, 1).unwrap(), "time11.1.txt");
-}
-
-#[test]
-fn time11_2() {
-    test_times(Format::new(Type::Time, 11, 2).unwrap(), "time11.2.txt");
-}
-
-#[test]
-fn time12() {
-    test_times(Format::new(Type::Time, 12, 0).unwrap(), "time12.txt");
-}
-
-#[test]
-fn time12_1() {
-    test_times(Format::new(Type::Time, 12, 1).unwrap(), "time12.1.txt");
-}
-
-#[test]
-fn time12_2() {
-    test_times(Format::new(Type::Time, 12, 2).unwrap(), "time12.2.txt");
-}
-
-#[test]
-fn time12_3() {
-    test_times(Format::new(Type::Time, 12, 3).unwrap(), "time12.3.txt");
-}
-
-#[test]
-fn time13() {
-    test_times(Format::new(Type::Time, 13, 0).unwrap(), "time13.txt");
-}
-
-#[test]
-fn time13_1() {
-    test_times(Format::new(Type::Time, 13, 1).unwrap(), "time13.1.txt");
-}
-
-#[test]
-fn time13_2() {
-    test_times(Format::new(Type::Time, 13, 2).unwrap(), "time13.2.txt");
-}
-
-#[test]
-fn time13_3() {
-    test_times(Format::new(Type::Time, 13, 3).unwrap(), "time13.3.txt");
-}
-
-#[test]
-fn time13_4() {
-    test_times(Format::new(Type::Time, 13, 4).unwrap(), "time13.4.txt");
-}
-
-#[test]
-fn time14() {
-    test_times(Format::new(Type::Time, 14, 0).unwrap(), "time14.txt");
-}
-
-#[test]
-fn time14_1() {
-    test_times(Format::new(Type::Time, 14, 1).unwrap(), "time14.1.txt");
-}
-
-#[test]
-fn time14_2() {
-    test_times(Format::new(Type::Time, 14, 2).unwrap(), "time14.2.txt");
-}
-
-#[test]
-fn time14_3() {
-    test_times(Format::new(Type::Time, 14, 3).unwrap(), "time14.3.txt");
-}
-
-#[test]
-fn time14_4() {
-    test_times(Format::new(Type::Time, 14, 4).unwrap(), "time14.4.txt");
-}
-
-#[test]
-fn time14_5() {
-    test_times(Format::new(Type::Time, 14, 5).unwrap(), "time14.5.txt");
-}
-
-#[test]
-fn time15() {
-    test_times(Format::new(Type::Time, 15, 0).unwrap(), "time15.txt");
-}
-
-#[test]
-fn time15_1() {
-    test_times(Format::new(Type::Time, 15, 1).unwrap(), "time15.1.txt");
-}
-
-#[test]
-fn time15_2() {
-    test_times(Format::new(Type::Time, 15, 2).unwrap(), "time15.2.txt");
-}
-
-#[test]
-fn time15_3() {
-    test_times(Format::new(Type::Time, 15, 3).unwrap(), "time15.3.txt");
-}
-
-#[test]
-fn time15_4() {
-    test_times(Format::new(Type::Time, 15, 4).unwrap(), "time15.4.txt");
-}
-
-#[test]
-fn time15_5() {
-    test_times(Format::new(Type::Time, 15, 5).unwrap(), "time15.5.txt");
-}
-
-#[test]
-fn time15_6() {
-    test_times(Format::new(Type::Time, 15, 6).unwrap(), "time15.6.txt");
-}
-
-#[test]
-fn mtime5() {
-    test_times(Format::new(Type::MTime, 5, 0).unwrap(), "mtime5.txt");
-}
-
-#[test]
-fn mtime6() {
-    test_times(Format::new(Type::MTime, 6, 0).unwrap(), "mtime6.txt");
-}
-
-#[test]
-fn mtime7() {
-    test_times(Format::new(Type::MTime, 7, 0).unwrap(), "mtime7.txt");
-}
-
-#[test]
-fn mtime7_1() {
-    test_times(Format::new(Type::MTime, 7, 1).unwrap(), "mtime7.1.txt");
-}
-
-#[test]
-fn mtime8() {
-    test_times(Format::new(Type::MTime, 8, 0).unwrap(), "mtime8.txt");
-}
-
-#[test]
-fn mtime8_1() {
-    test_times(Format::new(Type::MTime, 8, 1).unwrap(), "mtime8.1.txt");
-}
-
-#[test]
-fn mtime8_2() {
-    test_times(Format::new(Type::MTime, 8, 2).unwrap(), "mtime8.2.txt");
-}
-
-#[test]
-fn mtime9() {
-    test_times(Format::new(Type::MTime, 9, 0).unwrap(), "mtime9.txt");
-}
-
-#[test]
-fn mtime9_1() {
-    test_times(Format::new(Type::MTime, 9, 1).unwrap(), "mtime9.1.txt");
-}
-
-#[test]
-fn mtime9_2() {
-    test_times(Format::new(Type::MTime, 9, 2).unwrap(), "mtime9.2.txt");
-}
-
-#[test]
-fn mtime9_3() {
-    test_times(Format::new(Type::MTime, 9, 3).unwrap(), "mtime9.3.txt");
-}
-
-#[test]
-fn mtime10() {
-    test_times(Format::new(Type::MTime, 10, 0).unwrap(), "mtime10.txt");
-}
-
-#[test]
-fn mtime10_1() {
-    test_times(Format::new(Type::MTime, 10, 1).unwrap(), "mtime10.1.txt");
-}
-
-#[test]
-fn mtime10_2() {
-    test_times(Format::new(Type::MTime, 10, 2).unwrap(), "mtime10.2.txt");
-}
-
-#[test]
-fn mtime10_3() {
-    test_times(Format::new(Type::MTime, 10, 3).unwrap(), "mtime10.3.txt");
-}
-
-#[test]
-fn mtime10_4() {
-    test_times(Format::new(Type::MTime, 10, 4).unwrap(), "mtime10.4.txt");
-}
-
-#[test]
-fn mtime11() {
-    test_times(Format::new(Type::MTime, 11, 0).unwrap(), "mtime11.txt");
-}
-
-#[test]
-fn mtime11_1() {
-    test_times(Format::new(Type::MTime, 11, 1).unwrap(), "mtime11.1.txt");
-}
-
-#[test]
-fn mtime11_2() {
-    test_times(Format::new(Type::MTime, 11, 2).unwrap(), "mtime11.2.txt");
-}
-
-#[test]
-fn mtime11_3() {
-    test_times(Format::new(Type::MTime, 11, 3).unwrap(), "mtime11.3.txt");
-}
-
-#[test]
-fn mtime11_4() {
-    test_times(Format::new(Type::MTime, 11, 4).unwrap(), "mtime11.4.txt");
-}
-
-#[test]
-fn mtime11_5() {
-    test_times(Format::new(Type::MTime, 11, 5).unwrap(), "mtime11.5.txt");
-}
-
-#[test]
-fn mtime12_5() {
-    test_times(Format::new(Type::MTime, 12, 5).unwrap(), "mtime12.5.txt");
-}
-
-#[test]
-fn mtime13_5() {
-    test_times(Format::new(Type::MTime, 13, 5).unwrap(), "mtime13.5.txt");
-}
-
-#[test]
-fn mtime14_5() {
-    test_times(Format::new(Type::MTime, 14, 5).unwrap(), "mtime14.5.txt");
-}
-
-#[test]
-fn mtime15_5() {
-    test_times(Format::new(Type::MTime, 15, 5).unwrap(), "mtime15.5.txt");
-}
-
-#[test]
-fn mtime16_5() {
-    test_times(Format::new(Type::MTime, 16, 5).unwrap(), "mtime16.5.txt");
-}
-
-#[test]
-fn dtime8() {
-    test_times(Format::new(Type::DTime, 8, 0).unwrap(), "dtime8.txt");
-}
-
-#[test]
-fn dtime9() {
-    test_times(Format::new(Type::DTime, 9, 0).unwrap(), "dtime9.txt");
-}
-
-#[test]
-fn dtime10() {
-    test_times(Format::new(Type::DTime, 10, 0).unwrap(), "dtime10.txt");
-}
-
-#[test]
-fn dtime11() {
-    test_times(Format::new(Type::DTime, 11, 0).unwrap(), "dtime11.txt");
-}
-
-#[test]
-fn dtime12() {
-    test_times(Format::new(Type::DTime, 12, 0).unwrap(), "dtime12.txt");
-}
-
-#[test]
-fn dtime13() {
-    test_times(Format::new(Type::DTime, 13, 0).unwrap(), "dtime13.txt");
-}
-
-#[test]
-fn dtime13_1() {
-    test_times(Format::new(Type::DTime, 13, 1).unwrap(), "dtime13.1.txt");
-}
-
-#[test]
-fn dtime14() {
-    test_times(Format::new(Type::DTime, 14, 0).unwrap(), "dtime14.txt");
-}
-
-#[test]
-fn dtime14_1() {
-    test_times(Format::new(Type::DTime, 14, 1).unwrap(), "dtime14.1.txt");
-}
-
-#[test]
-fn dtime14_2() {
-    test_times(Format::new(Type::DTime, 14, 2).unwrap(), "dtime14.2.txt");
-}
-
-#[test]
-fn dtime15() {
-    test_times(Format::new(Type::DTime, 15, 0).unwrap(), "dtime15.txt");
-}
-
-#[test]
-fn dtime15_1() {
-    test_times(Format::new(Type::DTime, 15, 1).unwrap(), "dtime15.1.txt");
-}
-
-#[test]
-fn dtime15_2() {
-    test_times(Format::new(Type::DTime, 15, 2).unwrap(), "dtime15.2.txt");
-}
-
-#[test]
-fn dtime15_3() {
-    test_times(Format::new(Type::DTime, 15, 3).unwrap(), "dtime15.3.txt");
-}
-
-#[test]
-fn dtime16() {
-    test_times(Format::new(Type::DTime, 16, 0).unwrap(), "dtime16.txt");
-}
-
-#[test]
-fn dtime16_1() {
-    test_times(Format::new(Type::DTime, 16, 1).unwrap(), "dtime16.1.txt");
-}
-
-#[test]
-fn dtime16_2() {
-    test_times(Format::new(Type::DTime, 16, 2).unwrap(), "dtime16.2.txt");
-}
-
-#[test]
-fn dtime16_3() {
-    test_times(Format::new(Type::DTime, 16, 3).unwrap(), "dtime16.3.txt");
-}
-
-#[test]
-fn dtime16_4() {
-    test_times(Format::new(Type::DTime, 16, 4).unwrap(), "dtime16.4.txt");
-}
-
-#[test]
-fn dtime17() {
-    test_times(Format::new(Type::DTime, 17, 0).unwrap(), "dtime17.txt");
-}
-
-#[test]
-fn dtime17_1() {
-    test_times(Format::new(Type::DTime, 17, 1).unwrap(), "dtime17.1.txt");
-}
-
-#[test]
-fn dtime17_2() {
-    test_times(Format::new(Type::DTime, 17, 2).unwrap(), "dtime17.2.txt");
-}
-
-#[test]
-fn dtime17_3() {
-    test_times(Format::new(Type::DTime, 17, 3).unwrap(), "dtime17.3.txt");
-}
-
-#[test]
-fn dtime17_4() {
-    test_times(Format::new(Type::DTime, 17, 4).unwrap(), "dtime17.4.txt");
-}
-
-#[test]
-fn dtime17_5() {
-    test_times(Format::new(Type::DTime, 17, 5).unwrap(), "dtime17.5.txt");
-}
-
-#[test]
-fn dtime18() {
-    test_times(Format::new(Type::DTime, 18, 0).unwrap(), "dtime18.txt");
-}
-
-#[test]
-fn dtime18_1() {
-    test_times(Format::new(Type::DTime, 18, 1).unwrap(), "dtime18.1.txt");
-}
-
-#[test]
-fn dtime18_2() {
-    test_times(Format::new(Type::DTime, 18, 2).unwrap(), "dtime18.2.txt");
-}
-
-#[test]
-fn dtime18_3() {
-    test_times(Format::new(Type::DTime, 18, 3).unwrap(), "dtime18.3.txt");
-}
-
-#[test]
-fn dtime18_4() {
-    test_times(Format::new(Type::DTime, 18, 4).unwrap(), "dtime18.4.txt");
-}
-
-#[test]
-fn dtime18_5() {
-    test_times(Format::new(Type::DTime, 18, 5).unwrap(), "dtime18.5.txt");
-}
-
-#[test]
-fn dtime18_6() {
-    test_times(Format::new(Type::DTime, 18, 6).unwrap(), "dtime18.6.txt");
-}
diff --git a/rust/pspp/src/format/display/tests.rs b/rust/pspp/src/format/display/tests.rs
new file mode 100644 (file)
index 0000000..9ddd304
--- /dev/null
@@ -0,0 +1,1757 @@
+// PSPP - a program for statistical analysis.
+// Copyright (C) 2025 Free Software Foundation, Inc.
+//
+// This program is free software: you can redistribute it and/or modify it under
+// the terms of the GNU General Public License as published by the Free Software
+// Foundation, either version 3 of the License, or (at your option) any later
+// version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+// FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+// details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program.  If not, see <http://www.gnu.org/licenses/>.
+
+use std::{fmt::Write, fs::File, io::BufRead, path::Path};
+
+use binrw::{io::BufReader, Endian};
+use encoding_rs::UTF_8;
+use itertools::Itertools;
+use smallstr::SmallString;
+use smallvec::SmallVec;
+
+use crate::{
+    data::{ByteString, Datum, WithEncoding},
+    format::{AbstractFormat, Epoch, Format, Settings, Type, UncheckedFormat, CC},
+    lex::{scan::StringScanner, segment::Syntax, Punct, Token},
+    settings::EndianSettings,
+};
+
+fn test(name: &str) {
+    let filename = Path::new("src/format/testdata/display").join(name);
+    let input = BufReader::new(File::open(&filename).unwrap());
+    let settings = Settings::default()
+        .with_cc(CC::A, ",,,".parse().unwrap())
+        .with_cc(CC::B, "-,[[[,]]],-".parse().unwrap())
+        .with_cc(CC::C, "((,[,],))".parse().unwrap())
+        .with_cc(CC::D, ",XXX,,-".parse().unwrap())
+        .with_cc(CC::E, ",,YYY,-".parse().unwrap());
+    let endian = EndianSettings::new(Endian::Big);
+    let mut value = Some(0.0);
+    let mut value_name = String::new();
+    for (line, line_number) in input.lines().map(|r| r.unwrap()).zip(1..) {
+        let line = line.trim();
+        let tokens = StringScanner::new(line, Syntax::Interactive, true)
+            .unwrapped()
+            .collect::<Vec<_>>();
+        match &tokens[0] {
+            Token::Number(number) => {
+                value = if let Some(Token::Punct(Punct::Exp)) = tokens.get(1) {
+                    assert_eq!(tokens.len(), 3);
+                    let exponent = tokens[2].as_number().unwrap();
+                    Some(number.powf(exponent))
+                } else {
+                    assert_eq!(tokens.len(), 1);
+                    Some(*number)
+                };
+                value_name = String::from(line);
+            }
+            Token::End => {
+                value = None;
+                value_name = String::from(line);
+            }
+            Token::Id(id) => {
+                let format: UncheckedFormat =
+                    id.0.as_str()
+                        .parse::<AbstractFormat>()
+                        .unwrap()
+                        .try_into()
+                        .unwrap();
+                let format: Format = format.try_into().unwrap();
+                assert_eq!(tokens.get(1), Some(&Token::Punct(Punct::Colon)));
+                let expected = tokens[2].as_string().unwrap();
+                let actual = Datum::<WithEncoding<ByteString>>::Number(value)
+                    .display(format)
+                    .with_settings(&settings)
+                    .with_endian(endian)
+                    .to_string();
+                assert_eq!(
+                    expected,
+                    &actual,
+                    "{}:{line_number}: Error formatting {value_name} as {format}",
+                    filename.display()
+                );
+            }
+            _ => panic!(),
+        }
+    }
+}
+
+#[test]
+fn comma() {
+    test("comma.txt");
+}
+
+#[test]
+fn dot() {
+    test("dot.txt");
+}
+
+#[test]
+fn dollar() {
+    test("dollar.txt");
+}
+
+#[test]
+fn pct() {
+    test("pct.txt");
+}
+
+#[test]
+fn e() {
+    test("e.txt");
+}
+
+#[test]
+fn f() {
+    test("f.txt");
+}
+
+#[test]
+fn n() {
+    test("n.txt");
+}
+
+#[test]
+fn z() {
+    test("z.txt");
+}
+
+#[test]
+fn cca() {
+    test("cca.txt");
+}
+
+#[test]
+fn ccb() {
+    test("ccb.txt");
+}
+
+#[test]
+fn ccc() {
+    test("ccc.txt");
+}
+
+#[test]
+fn ccd() {
+    test("ccd.txt");
+}
+
+#[test]
+fn cce() {
+    test("cce.txt");
+}
+
+#[test]
+fn pibhex() {
+    test("pibhex.txt");
+}
+
+#[test]
+fn rbhex() {
+    test("rbhex.txt");
+}
+
+#[test]
+fn leading_zeros() {
+    struct Test {
+        with_leading_zero: Settings,
+        without_leading_zero: Settings,
+    }
+
+    impl Test {
+        fn new() -> Self {
+            Self {
+                without_leading_zero: Settings::default(),
+                with_leading_zero: Settings::default().with_leading_zero(true),
+            }
+        }
+
+        fn test_with_settings(value: f64, expected: [&str; 2], settings: &Settings) {
+            let value = Datum::<WithEncoding<ByteString>>::from(value);
+            for (expected, d) in expected.into_iter().zip([2, 1].into_iter()) {
+                assert_eq!(
+                    &value
+                        .display(Format::new(Type::F, 5, d).unwrap())
+                        .with_settings(settings)
+                        .to_string(),
+                    expected
+                );
+            }
+        }
+        fn test(&self, value: f64, without: [&str; 2], with: [&str; 2]) {
+            Self::test_with_settings(value, without, &self.without_leading_zero);
+            Self::test_with_settings(value, with, &self.with_leading_zero);
+        }
+    }
+    let test = Test::new();
+    test.test(0.5, ["  .50", "   .5"], [" 0.50", "  0.5"]);
+    test.test(0.99, ["  .99", "  1.0"], [" 0.99", "  1.0"]);
+    test.test(0.01, ["  .01", "   .0"], [" 0.01", "  0.0"]);
+    test.test(0.0, ["  .00", "   .0"], [" 0.00", "  0.0"]);
+    test.test(-0.0, ["  .00", "   .0"], [" 0.00", "  0.0"]);
+    test.test(-0.5, [" -.50", "  -.5"], ["-0.50", " -0.5"]);
+    test.test(-0.99, [" -.99", " -1.0"], ["-0.99", " -1.0"]);
+    test.test(-0.01, [" -.01", "   .0"], ["-0.01", "  0.0"]);
+}
+
+#[test]
+fn non_ascii_cc() {
+    fn test(settings: &Settings, value: f64, expected: &str) {
+        assert_eq!(
+            &Datum::<WithEncoding<ByteString>>::from(value)
+                .display(Format::new(Type::CC(CC::A), 10, 2).unwrap())
+                .with_settings(settings)
+                .to_string(),
+            expected
+        );
+    }
+
+    let settings = Settings::default().with_cc(CC::A, "«,¥,€,»".parse().unwrap());
+    test(&settings, 1.0, "   ¥1.00€ ");
+    test(&settings, -1.0, "  «¥1.00€»");
+    test(&settings, 1.5, "   ¥1.50€ ");
+    test(&settings, -1.5, "  «¥1.50€»");
+    test(&settings, 0.75, "    ¥.75€ ");
+    test(&settings, 1.5e10, " ¥2E+010€ ");
+    test(&settings, -1.5e10, "«¥2E+010€»");
+}
+
+fn test_binhex(name: &str) {
+    let filename = Path::new("src/format/testdata/display").join(name);
+    let input = BufReader::new(File::open(&filename).unwrap());
+    let mut value = None;
+    let mut value_name = String::new();
+
+    let endian = EndianSettings::new(Endian::Big);
+    for (line, line_number) in input.lines().map(|r| r.unwrap()).zip(1..) {
+        let line = line.trim();
+        let tokens = StringScanner::new(line, Syntax::Interactive, true)
+            .unwrapped()
+            .collect::<Vec<_>>();
+        match &tokens[0] {
+            Token::Number(number) => {
+                value = Some(*number);
+                value_name = String::from(line);
+            }
+            Token::End => {
+                value = None;
+                value_name = String::from(line);
+            }
+            Token::Id(id) => {
+                let format: UncheckedFormat =
+                    id.0.as_str()
+                        .parse::<AbstractFormat>()
+                        .unwrap()
+                        .try_into()
+                        .unwrap();
+                let format: Format = format.try_into().unwrap();
+                assert_eq!(tokens.get(1), Some(&Token::Punct(Punct::Colon)));
+                let expected = tokens[2].as_string().unwrap();
+                let mut actual = SmallVec::<[u8; 16]>::new();
+                Datum::<WithEncoding<ByteString>>::Number(value)
+                    .display(format)
+                    .with_endian(endian)
+                    .write(&mut actual, UTF_8)
+                    .unwrap();
+                let mut actual_s = SmallString::<[u8; 32]>::new();
+                for b in actual {
+                    write!(&mut actual_s, "{:02x}", b).unwrap();
+                }
+                assert_eq!(
+                    expected,
+                    &*actual_s,
+                    "{}:{line_number}: Error formatting {value_name} as {format}",
+                    filename.display()
+                );
+            }
+            _ => panic!(),
+        }
+    }
+}
+
+#[test]
+fn p() {
+    test_binhex("p.txt");
+}
+
+#[test]
+fn pk() {
+    test_binhex("pk.txt");
+}
+
+#[test]
+fn ib() {
+    test_binhex("ib.txt");
+}
+
+#[test]
+fn pib() {
+    test_binhex("pib.txt");
+}
+
+#[test]
+fn rb() {
+    test_binhex("rb.txt");
+}
+
+fn test_dates(format: Format, expect: &[&str]) {
+    let settings = Settings::default().with_epoch(Epoch(1930));
+    let parser = Type::DateTime.parser(UTF_8).with_settings(&settings);
+    static INPUTS: &[&str; 20] = &[
+        "10-6-1648 0:0:0",
+        "30-6-1680 4:50:38.12301",
+        "24-7-1716 12:31:35.23453",
+        "19-6-1768 12:47:53.34505",
+        "2-8-1819 1:26:0.45615",
+        "27-3-1839 20:58:11.56677",
+        "19-4-1903 7:36:5.18964",
+        "25-8-1929 15:43:49.83132",
+        "29-9-1941 4:25:9.01293",
+        "19-4-1943 6:49:27.52375",
+        "7-10-1943 2:57:52.01565",
+        "17-3-1992 16:45:44.86529",
+        "25-2-1996 21:30:57.82047",
+        "29-9-41 4:25:9.15395",
+        "19-4-43 6:49:27.10533",
+        "7-10-43 2:57:52.48229",
+        "17-3-92 16:45:44.65827",
+        "25-2-96 21:30:57.58219",
+        "10-11-2038 22:30:4.18347",
+        "18-7-2094 1:56:51.59319",
+    ];
+    assert_eq!(expect.len(), INPUTS.len());
+    for (input, expect) in INPUTS.iter().copied().zip_eq(expect.iter().copied()) {
+        let value = parser.parse(input).unwrap().with_encoding(UTF_8);
+        let formatted = value.display(format).with_settings(&settings).to_string();
+        assert_eq!(&formatted, expect);
+    }
+}
+
+#[test]
+fn date9() {
+    test_dates(
+        Format::new(Type::Date, 9, 0).unwrap(),
+        &[
+            "*********",
+            "*********",
+            "*********",
+            "*********",
+            "*********",
+            "*********",
+            "*********",
+            "*********",
+            "29-SEP-41",
+            "19-APR-43",
+            "07-OCT-43",
+            "17-MAR-92",
+            "25-FEB-96",
+            "29-SEP-41",
+            "19-APR-43",
+            "07-OCT-43",
+            "17-MAR-92",
+            "25-FEB-96",
+            "*********",
+            "*********",
+        ],
+    );
+}
+
+#[test]
+fn date11() {
+    test_dates(
+        Format::new(Type::Date, 11, 0).unwrap(),
+        &[
+            "10-JUN-1648",
+            "30-JUN-1680",
+            "24-JUL-1716",
+            "19-JUN-1768",
+            "02-AUG-1819",
+            "27-MAR-1839",
+            "19-APR-1903",
+            "25-AUG-1929",
+            "29-SEP-1941",
+            "19-APR-1943",
+            "07-OCT-1943",
+            "17-MAR-1992",
+            "25-FEB-1996",
+            "29-SEP-1941",
+            "19-APR-1943",
+            "07-OCT-1943",
+            "17-MAR-1992",
+            "25-FEB-1996",
+            "10-NOV-2038",
+            "18-JUL-2094",
+        ],
+    );
+}
+
+#[test]
+fn adate8() {
+    test_dates(
+        Format::new(Type::ADate, 8, 0).unwrap(),
+        &[
+            "********", "********", "********", "********", "********", "********", "********",
+            "********", "09/29/41", "04/19/43", "10/07/43", "03/17/92", "02/25/96", "09/29/41",
+            "04/19/43", "10/07/43", "03/17/92", "02/25/96", "********", "********",
+        ],
+    );
+}
+
+#[test]
+fn adate10() {
+    test_dates(
+        Format::new(Type::ADate, 10, 0).unwrap(),
+        &[
+            "06/10/1648",
+            "06/30/1680",
+            "07/24/1716",
+            "06/19/1768",
+            "08/02/1819",
+            "03/27/1839",
+            "04/19/1903",
+            "08/25/1929",
+            "09/29/1941",
+            "04/19/1943",
+            "10/07/1943",
+            "03/17/1992",
+            "02/25/1996",
+            "09/29/1941",
+            "04/19/1943",
+            "10/07/1943",
+            "03/17/1992",
+            "02/25/1996",
+            "11/10/2038",
+            "07/18/2094",
+        ],
+    );
+}
+
+#[test]
+fn edate8() {
+    test_dates(
+        Format::new(Type::EDate, 8, 0).unwrap(),
+        &[
+            "********", "********", "********", "********", "********", "********", "********",
+            "********", "29.09.41", "19.04.43", "07.10.43", "17.03.92", "25.02.96", "29.09.41",
+            "19.04.43", "07.10.43", "17.03.92", "25.02.96", "********", "********",
+        ],
+    );
+}
+
+#[test]
+fn edate10() {
+    test_dates(
+        Format::new(Type::EDate, 10, 0).unwrap(),
+        &[
+            "10.06.1648",
+            "30.06.1680",
+            "24.07.1716",
+            "19.06.1768",
+            "02.08.1819",
+            "27.03.1839",
+            "19.04.1903",
+            "25.08.1929",
+            "29.09.1941",
+            "19.04.1943",
+            "07.10.1943",
+            "17.03.1992",
+            "25.02.1996",
+            "29.09.1941",
+            "19.04.1943",
+            "07.10.1943",
+            "17.03.1992",
+            "25.02.1996",
+            "10.11.2038",
+            "18.07.2094",
+        ],
+    );
+}
+
+#[test]
+fn jdate5() {
+    test_dates(
+        Format::new(Type::JDate, 5, 0).unwrap(),
+        &[
+            "*****", "*****", "*****", "*****", "*****", "*****", "*****", "*****", "41272",
+            "43109", "43280", "92077", "96056", "41272", "43109", "43280", "92077", "96056",
+            "*****", "*****",
+        ],
+    );
+}
+
+#[test]
+fn jdate7() {
+    test_dates(
+        Format::new(Type::JDate, 7, 0).unwrap(),
+        &[
+            "1648162", "1680182", "1716206", "1768171", "1819214", "1839086", "1903109", "1929237",
+            "1941272", "1943109", "1943280", "1992077", "1996056", "1941272", "1943109", "1943280",
+            "1992077", "1996056", "2038314", "2094199",
+        ],
+    );
+}
+
+#[test]
+fn sdate8() {
+    test_dates(
+        Format::new(Type::SDate, 8, 0).unwrap(),
+        &[
+            "********", "********", "********", "********", "********", "********", "********",
+            "********", "41/09/29", "43/04/19", "43/10/07", "92/03/17", "96/02/25", "41/09/29",
+            "43/04/19", "43/10/07", "92/03/17", "96/02/25", "********", "********",
+        ],
+    );
+}
+
+#[test]
+fn sdate10() {
+    test_dates(
+        Format::new(Type::SDate, 10, 0).unwrap(),
+        &[
+            "1648/06/10",
+            "1680/06/30",
+            "1716/07/24",
+            "1768/06/19",
+            "1819/08/02",
+            "1839/03/27",
+            "1903/04/19",
+            "1929/08/25",
+            "1941/09/29",
+            "1943/04/19",
+            "1943/10/07",
+            "1992/03/17",
+            "1996/02/25",
+            "1941/09/29",
+            "1943/04/19",
+            "1943/10/07",
+            "1992/03/17",
+            "1996/02/25",
+            "2038/11/10",
+            "2094/07/18",
+        ],
+    );
+}
+
+#[test]
+fn qyr6() {
+    test_dates(
+        Format::new(Type::QYr, 6, 0).unwrap(),
+        &[
+            "******", "******", "******", "******", "******", "******", "******", "******",
+            "3 Q 41", "2 Q 43", "4 Q 43", "1 Q 92", "1 Q 96", "3 Q 41", "2 Q 43", "4 Q 43",
+            "1 Q 92", "1 Q 96", "******", "******",
+        ],
+    );
+}
+
+#[test]
+fn qyr8() {
+    test_dates(
+        Format::new(Type::QYr, 8, 0).unwrap(),
+        &[
+            "2 Q 1648", "2 Q 1680", "3 Q 1716", "2 Q 1768", "3 Q 1819", "1 Q 1839", "2 Q 1903",
+            "3 Q 1929", "3 Q 1941", "2 Q 1943", "4 Q 1943", "1 Q 1992", "1 Q 1996", "3 Q 1941",
+            "2 Q 1943", "4 Q 1943", "1 Q 1992", "1 Q 1996", "4 Q 2038", "3 Q 2094",
+        ],
+    );
+}
+
+#[test]
+fn moyr6() {
+    test_dates(
+        Format::new(Type::MoYr, 6, 0).unwrap(),
+        &[
+            "******", "******", "******", "******", "******", "******", "******", "******",
+            "SEP 41", "APR 43", "OCT 43", "MAR 92", "FEB 96", "SEP 41", "APR 43", "OCT 43",
+            "MAR 92", "FEB 96", "******", "******",
+        ],
+    );
+}
+
+#[test]
+fn moyr8() {
+    test_dates(
+        Format::new(Type::MoYr, 8, 0).unwrap(),
+        &[
+            "JUN 1648", "JUN 1680", "JUL 1716", "JUN 1768", "AUG 1819", "MAR 1839", "APR 1903",
+            "AUG 1929", "SEP 1941", "APR 1943", "OCT 1943", "MAR 1992", "FEB 1996", "SEP 1941",
+            "APR 1943", "OCT 1943", "MAR 1992", "FEB 1996", "NOV 2038", "JUL 2094",
+        ],
+    );
+}
+
+#[test]
+fn wkyr8() {
+    test_dates(
+        Format::new(Type::WkYr, 8, 0).unwrap(),
+        &[
+            "********", "********", "********", "********", "********", "********", "********",
+            "********", "39 WK 41", "16 WK 43", "40 WK 43", "11 WK 92", " 8 WK 96", "39 WK 41",
+            "16 WK 43", "40 WK 43", "11 WK 92", " 8 WK 96", "********", "********",
+        ],
+    );
+}
+
+#[test]
+fn wkyr10() {
+    test_dates(
+        Format::new(Type::WkYr, 10, 0).unwrap(),
+        &[
+            "24 WK 1648",
+            "26 WK 1680",
+            "30 WK 1716",
+            "25 WK 1768",
+            "31 WK 1819",
+            "13 WK 1839",
+            "16 WK 1903",
+            "34 WK 1929",
+            "39 WK 1941",
+            "16 WK 1943",
+            "40 WK 1943",
+            "11 WK 1992",
+            " 8 WK 1996",
+            "39 WK 1941",
+            "16 WK 1943",
+            "40 WK 1943",
+            "11 WK 1992",
+            " 8 WK 1996",
+            "45 WK 2038",
+            "29 WK 2094",
+        ],
+    );
+}
+
+#[test]
+fn datetime17() {
+    test_dates(
+        Format::new(Type::DateTime, 17, 0).unwrap(),
+        &[
+            "10-JUN-1648 00:00",
+            "30-JUN-1680 04:50",
+            "24-JUL-1716 12:31",
+            "19-JUN-1768 12:47",
+            "02-AUG-1819 01:26",
+            "27-MAR-1839 20:58",
+            "19-APR-1903 07:36",
+            "25-AUG-1929 15:43",
+            "29-SEP-1941 04:25",
+            "19-APR-1943 06:49",
+            "07-OCT-1943 02:57",
+            "17-MAR-1992 16:45",
+            "25-FEB-1996 21:30",
+            "29-SEP-1941 04:25",
+            "19-APR-1943 06:49",
+            "07-OCT-1943 02:57",
+            "17-MAR-1992 16:45",
+            "25-FEB-1996 21:30",
+            "10-NOV-2038 22:30",
+            "18-JUL-2094 01:56",
+        ],
+    );
+}
+
+#[test]
+fn datetime18() {
+    test_dates(
+        Format::new(Type::DateTime, 18, 0).unwrap(),
+        &[
+            " 10-JUN-1648 00:00",
+            " 30-JUN-1680 04:50",
+            " 24-JUL-1716 12:31",
+            " 19-JUN-1768 12:47",
+            " 02-AUG-1819 01:26",
+            " 27-MAR-1839 20:58",
+            " 19-APR-1903 07:36",
+            " 25-AUG-1929 15:43",
+            " 29-SEP-1941 04:25",
+            " 19-APR-1943 06:49",
+            " 07-OCT-1943 02:57",
+            " 17-MAR-1992 16:45",
+            " 25-FEB-1996 21:30",
+            " 29-SEP-1941 04:25",
+            " 19-APR-1943 06:49",
+            " 07-OCT-1943 02:57",
+            " 17-MAR-1992 16:45",
+            " 25-FEB-1996 21:30",
+            " 10-NOV-2038 22:30",
+            " 18-JUL-2094 01:56",
+        ],
+    );
+}
+
+#[test]
+fn datetime19() {
+    test_dates(
+        Format::new(Type::DateTime, 19, 0).unwrap(),
+        &[
+            "  10-JUN-1648 00:00",
+            "  30-JUN-1680 04:50",
+            "  24-JUL-1716 12:31",
+            "  19-JUN-1768 12:47",
+            "  02-AUG-1819 01:26",
+            "  27-MAR-1839 20:58",
+            "  19-APR-1903 07:36",
+            "  25-AUG-1929 15:43",
+            "  29-SEP-1941 04:25",
+            "  19-APR-1943 06:49",
+            "  07-OCT-1943 02:57",
+            "  17-MAR-1992 16:45",
+            "  25-FEB-1996 21:30",
+            "  29-SEP-1941 04:25",
+            "  19-APR-1943 06:49",
+            "  07-OCT-1943 02:57",
+            "  17-MAR-1992 16:45",
+            "  25-FEB-1996 21:30",
+            "  10-NOV-2038 22:30",
+            "  18-JUL-2094 01:56",
+        ],
+    );
+}
+
+#[test]
+fn datetime20() {
+    test_dates(
+        Format::new(Type::DateTime, 20, 0).unwrap(),
+        &[
+            "10-JUN-1648 00:00:00",
+            "30-JUN-1680 04:50:38",
+            "24-JUL-1716 12:31:35",
+            "19-JUN-1768 12:47:53",
+            "02-AUG-1819 01:26:00",
+            "27-MAR-1839 20:58:11",
+            "19-APR-1903 07:36:05",
+            "25-AUG-1929 15:43:49",
+            "29-SEP-1941 04:25:09",
+            "19-APR-1943 06:49:27",
+            "07-OCT-1943 02:57:52",
+            "17-MAR-1992 16:45:44",
+            "25-FEB-1996 21:30:57",
+            "29-SEP-1941 04:25:09",
+            "19-APR-1943 06:49:27",
+            "07-OCT-1943 02:57:52",
+            "17-MAR-1992 16:45:44",
+            "25-FEB-1996 21:30:57",
+            "10-NOV-2038 22:30:04",
+            "18-JUL-2094 01:56:51",
+        ],
+    );
+}
+
+#[test]
+fn datetime21() {
+    test_dates(
+        Format::new(Type::DateTime, 21, 0).unwrap(),
+        &[
+            " 10-JUN-1648 00:00:00",
+            " 30-JUN-1680 04:50:38",
+            " 24-JUL-1716 12:31:35",
+            " 19-JUN-1768 12:47:53",
+            " 02-AUG-1819 01:26:00",
+            " 27-MAR-1839 20:58:11",
+            " 19-APR-1903 07:36:05",
+            " 25-AUG-1929 15:43:49",
+            " 29-SEP-1941 04:25:09",
+            " 19-APR-1943 06:49:27",
+            " 07-OCT-1943 02:57:52",
+            " 17-MAR-1992 16:45:44",
+            " 25-FEB-1996 21:30:57",
+            " 29-SEP-1941 04:25:09",
+            " 19-APR-1943 06:49:27",
+            " 07-OCT-1943 02:57:52",
+            " 17-MAR-1992 16:45:44",
+            " 25-FEB-1996 21:30:57",
+            " 10-NOV-2038 22:30:04",
+            " 18-JUL-2094 01:56:51",
+        ],
+    );
+}
+
+#[test]
+fn datetime22() {
+    test_dates(
+        Format::new(Type::DateTime, 22, 0).unwrap(),
+        &[
+            "  10-JUN-1648 00:00:00",
+            "  30-JUN-1680 04:50:38",
+            "  24-JUL-1716 12:31:35",
+            "  19-JUN-1768 12:47:53",
+            "  02-AUG-1819 01:26:00",
+            "  27-MAR-1839 20:58:11",
+            "  19-APR-1903 07:36:05",
+            "  25-AUG-1929 15:43:49",
+            "  29-SEP-1941 04:25:09",
+            "  19-APR-1943 06:49:27",
+            "  07-OCT-1943 02:57:52",
+            "  17-MAR-1992 16:45:44",
+            "  25-FEB-1996 21:30:57",
+            "  29-SEP-1941 04:25:09",
+            "  19-APR-1943 06:49:27",
+            "  07-OCT-1943 02:57:52",
+            "  17-MAR-1992 16:45:44",
+            "  25-FEB-1996 21:30:57",
+            "  10-NOV-2038 22:30:04",
+            "  18-JUL-2094 01:56:51",
+        ],
+    );
+}
+
+#[test]
+fn datetime22_1() {
+    test_dates(
+        Format::new(Type::DateTime, 22, 1).unwrap(),
+        &[
+            "10-JUN-1648 00:00:00.0",
+            "30-JUN-1680 04:50:38.1",
+            "24-JUL-1716 12:31:35.2",
+            "19-JUN-1768 12:47:53.3",
+            "02-AUG-1819 01:26:00.5",
+            "27-MAR-1839 20:58:11.6",
+            "19-APR-1903 07:36:05.2",
+            "25-AUG-1929 15:43:49.8",
+            "29-SEP-1941 04:25:09.0",
+            "19-APR-1943 06:49:27.5",
+            "07-OCT-1943 02:57:52.0",
+            "17-MAR-1992 16:45:44.9",
+            "25-FEB-1996 21:30:57.8",
+            "29-SEP-1941 04:25:09.2",
+            "19-APR-1943 06:49:27.1",
+            "07-OCT-1943 02:57:52.5",
+            "17-MAR-1992 16:45:44.7",
+            "25-FEB-1996 21:30:57.6",
+            "10-NOV-2038 22:30:04.2",
+            "18-JUL-2094 01:56:51.6",
+        ],
+    );
+}
+
+#[test]
+fn datetime23_2() {
+    test_dates(
+        Format::new(Type::DateTime, 23, 2).unwrap(),
+        &[
+            "10-JUN-1648 00:00:00.00",
+            "30-JUN-1680 04:50:38.12",
+            "24-JUL-1716 12:31:35.23",
+            "19-JUN-1768 12:47:53.35",
+            "02-AUG-1819 01:26:00.46",
+            "27-MAR-1839 20:58:11.57",
+            "19-APR-1903 07:36:05.19",
+            "25-AUG-1929 15:43:49.83",
+            "29-SEP-1941 04:25:09.01",
+            "19-APR-1943 06:49:27.52",
+            "07-OCT-1943 02:57:52.02",
+            "17-MAR-1992 16:45:44.87",
+            "25-FEB-1996 21:30:57.82",
+            "29-SEP-1941 04:25:09.15",
+            "19-APR-1943 06:49:27.11",
+            "07-OCT-1943 02:57:52.48",
+            "17-MAR-1992 16:45:44.66",
+            "25-FEB-1996 21:30:57.58",
+            "10-NOV-2038 22:30:04.18",
+            "18-JUL-2094 01:56:51.59",
+        ],
+    );
+}
+
+#[test]
+fn datetime24_3() {
+    test_dates(
+        Format::new(Type::DateTime, 24, 3).unwrap(),
+        &[
+            "10-JUN-1648 00:00:00.000",
+            "30-JUN-1680 04:50:38.123",
+            "24-JUL-1716 12:31:35.235",
+            "19-JUN-1768 12:47:53.345",
+            "02-AUG-1819 01:26:00.456",
+            "27-MAR-1839 20:58:11.567",
+            "19-APR-1903 07:36:05.190",
+            "25-AUG-1929 15:43:49.831",
+            "29-SEP-1941 04:25:09.013",
+            "19-APR-1943 06:49:27.524",
+            "07-OCT-1943 02:57:52.016",
+            "17-MAR-1992 16:45:44.865",
+            "25-FEB-1996 21:30:57.820",
+            "29-SEP-1941 04:25:09.154",
+            "19-APR-1943 06:49:27.105",
+            "07-OCT-1943 02:57:52.482",
+            "17-MAR-1992 16:45:44.658",
+            "25-FEB-1996 21:30:57.582",
+            "10-NOV-2038 22:30:04.183",
+            "18-JUL-2094 01:56:51.593",
+        ],
+    );
+}
+
+#[test]
+fn datetime25_4() {
+    test_dates(
+        Format::new(Type::DateTime, 25, 4).unwrap(),
+        &[
+            "10-JUN-1648 00:00:00.0000",
+            "30-JUN-1680 04:50:38.1230",
+            "24-JUL-1716 12:31:35.2345",
+            "19-JUN-1768 12:47:53.3450",
+            "02-AUG-1819 01:26:00.4562",
+            "27-MAR-1839 20:58:11.5668",
+            "19-APR-1903 07:36:05.1896",
+            "25-AUG-1929 15:43:49.8313",
+            "29-SEP-1941 04:25:09.0129",
+            "19-APR-1943 06:49:27.5238",
+            "07-OCT-1943 02:57:52.0156",
+            "17-MAR-1992 16:45:44.8653",
+            "25-FEB-1996 21:30:57.8205",
+            "29-SEP-1941 04:25:09.1539",
+            "19-APR-1943 06:49:27.1053",
+            "07-OCT-1943 02:57:52.4823",
+            "17-MAR-1992 16:45:44.6583",
+            "25-FEB-1996 21:30:57.5822",
+            "10-NOV-2038 22:30:04.1835",
+            "18-JUL-2094 01:56:51.5932",
+        ],
+    );
+}
+
+#[test]
+fn datetime26_5() {
+    test_dates(
+        Format::new(Type::DateTime, 26, 5).unwrap(),
+        &[
+            "10-JUN-1648 00:00:00.00000",
+            "30-JUN-1680 04:50:38.12301",
+            "24-JUL-1716 12:31:35.23453",
+            "19-JUN-1768 12:47:53.34505",
+            "02-AUG-1819 01:26:00.45615",
+            "27-MAR-1839 20:58:11.56677",
+            "19-APR-1903 07:36:05.18964",
+            "25-AUG-1929 15:43:49.83132",
+            "29-SEP-1941 04:25:09.01293",
+            "19-APR-1943 06:49:27.52375",
+            "07-OCT-1943 02:57:52.01565",
+            "17-MAR-1992 16:45:44.86529",
+            "25-FEB-1996 21:30:57.82047",
+            "29-SEP-1941 04:25:09.15395",
+            "19-APR-1943 06:49:27.10533",
+            "07-OCT-1943 02:57:52.48229",
+            "17-MAR-1992 16:45:44.65827",
+            "25-FEB-1996 21:30:57.58219",
+            "10-NOV-2038 22:30:04.18347",
+            "18-JUL-2094 01:56:51.59319",
+        ],
+    );
+}
+
+#[test]
+fn ymdhms16() {
+    test_dates(
+        Format::new(Type::YmdHms, 16, 0).unwrap(),
+        &[
+            "1648-06-10 00:00",
+            "1680-06-30 04:50",
+            "1716-07-24 12:31",
+            "1768-06-19 12:47",
+            "1819-08-02 01:26",
+            "1839-03-27 20:58",
+            "1903-04-19 07:36",
+            "1929-08-25 15:43",
+            "1941-09-29 04:25",
+            "1943-04-19 06:49",
+            "1943-10-07 02:57",
+            "1992-03-17 16:45",
+            "1996-02-25 21:30",
+            "1941-09-29 04:25",
+            "1943-04-19 06:49",
+            "1943-10-07 02:57",
+            "1992-03-17 16:45",
+            "1996-02-25 21:30",
+            "2038-11-10 22:30",
+            "2094-07-18 01:56",
+        ],
+    );
+}
+
+#[test]
+fn ymdhms17() {
+    test_dates(
+        Format::new(Type::YmdHms, 17, 0).unwrap(),
+        &[
+            " 1648-06-10 00:00",
+            " 1680-06-30 04:50",
+            " 1716-07-24 12:31",
+            " 1768-06-19 12:47",
+            " 1819-08-02 01:26",
+            " 1839-03-27 20:58",
+            " 1903-04-19 07:36",
+            " 1929-08-25 15:43",
+            " 1941-09-29 04:25",
+            " 1943-04-19 06:49",
+            " 1943-10-07 02:57",
+            " 1992-03-17 16:45",
+            " 1996-02-25 21:30",
+            " 1941-09-29 04:25",
+            " 1943-04-19 06:49",
+            " 1943-10-07 02:57",
+            " 1992-03-17 16:45",
+            " 1996-02-25 21:30",
+            " 2038-11-10 22:30",
+            " 2094-07-18 01:56",
+        ],
+    );
+}
+
+#[test]
+fn ymdhms18() {
+    test_dates(
+        Format::new(Type::YmdHms, 18, 0).unwrap(),
+        &[
+            "  1648-06-10 00:00",
+            "  1680-06-30 04:50",
+            "  1716-07-24 12:31",
+            "  1768-06-19 12:47",
+            "  1819-08-02 01:26",
+            "  1839-03-27 20:58",
+            "  1903-04-19 07:36",
+            "  1929-08-25 15:43",
+            "  1941-09-29 04:25",
+            "  1943-04-19 06:49",
+            "  1943-10-07 02:57",
+            "  1992-03-17 16:45",
+            "  1996-02-25 21:30",
+            "  1941-09-29 04:25",
+            "  1943-04-19 06:49",
+            "  1943-10-07 02:57",
+            "  1992-03-17 16:45",
+            "  1996-02-25 21:30",
+            "  2038-11-10 22:30",
+            "  2094-07-18 01:56",
+        ],
+    );
+}
+
+#[test]
+fn ymdhms19() {
+    test_dates(
+        Format::new(Type::YmdHms, 19, 0).unwrap(),
+        &[
+            "1648-06-10 00:00:00",
+            "1680-06-30 04:50:38",
+            "1716-07-24 12:31:35",
+            "1768-06-19 12:47:53",
+            "1819-08-02 01:26:00",
+            "1839-03-27 20:58:11",
+            "1903-04-19 07:36:05",
+            "1929-08-25 15:43:49",
+            "1941-09-29 04:25:09",
+            "1943-04-19 06:49:27",
+            "1943-10-07 02:57:52",
+            "1992-03-17 16:45:44",
+            "1996-02-25 21:30:57",
+            "1941-09-29 04:25:09",
+            "1943-04-19 06:49:27",
+            "1943-10-07 02:57:52",
+            "1992-03-17 16:45:44",
+            "1996-02-25 21:30:57",
+            "2038-11-10 22:30:04",
+            "2094-07-18 01:56:51",
+        ],
+    );
+}
+
+#[test]
+fn ymdhms20() {
+    test_dates(
+        Format::new(Type::YmdHms, 20, 0).unwrap(),
+        &[
+            " 1648-06-10 00:00:00",
+            " 1680-06-30 04:50:38",
+            " 1716-07-24 12:31:35",
+            " 1768-06-19 12:47:53",
+            " 1819-08-02 01:26:00",
+            " 1839-03-27 20:58:11",
+            " 1903-04-19 07:36:05",
+            " 1929-08-25 15:43:49",
+            " 1941-09-29 04:25:09",
+            " 1943-04-19 06:49:27",
+            " 1943-10-07 02:57:52",
+            " 1992-03-17 16:45:44",
+            " 1996-02-25 21:30:57",
+            " 1941-09-29 04:25:09",
+            " 1943-04-19 06:49:27",
+            " 1943-10-07 02:57:52",
+            " 1992-03-17 16:45:44",
+            " 1996-02-25 21:30:57",
+            " 2038-11-10 22:30:04",
+            " 2094-07-18 01:56:51",
+        ],
+    );
+}
+
+#[test]
+fn ymdhms21() {
+    test_dates(
+        Format::new(Type::YmdHms, 21, 0).unwrap(),
+        &[
+            "  1648-06-10 00:00:00",
+            "  1680-06-30 04:50:38",
+            "  1716-07-24 12:31:35",
+            "  1768-06-19 12:47:53",
+            "  1819-08-02 01:26:00",
+            "  1839-03-27 20:58:11",
+            "  1903-04-19 07:36:05",
+            "  1929-08-25 15:43:49",
+            "  1941-09-29 04:25:09",
+            "  1943-04-19 06:49:27",
+            "  1943-10-07 02:57:52",
+            "  1992-03-17 16:45:44",
+            "  1996-02-25 21:30:57",
+            "  1941-09-29 04:25:09",
+            "  1943-04-19 06:49:27",
+            "  1943-10-07 02:57:52",
+            "  1992-03-17 16:45:44",
+            "  1996-02-25 21:30:57",
+            "  2038-11-10 22:30:04",
+            "  2094-07-18 01:56:51",
+        ],
+    );
+}
+
+#[test]
+fn ymdhms21_1() {
+    test_dates(
+        Format::new(Type::YmdHms, 21, 1).unwrap(),
+        &[
+            "1648-06-10 00:00:00.0",
+            "1680-06-30 04:50:38.1",
+            "1716-07-24 12:31:35.2",
+            "1768-06-19 12:47:53.3",
+            "1819-08-02 01:26:00.5",
+            "1839-03-27 20:58:11.6",
+            "1903-04-19 07:36:05.2",
+            "1929-08-25 15:43:49.8",
+            "1941-09-29 04:25:09.0",
+            "1943-04-19 06:49:27.5",
+            "1943-10-07 02:57:52.0",
+            "1992-03-17 16:45:44.9",
+            "1996-02-25 21:30:57.8",
+            "1941-09-29 04:25:09.2",
+            "1943-04-19 06:49:27.1",
+            "1943-10-07 02:57:52.5",
+            "1992-03-17 16:45:44.7",
+            "1996-02-25 21:30:57.6",
+            "2038-11-10 22:30:04.2",
+            "2094-07-18 01:56:51.6",
+        ],
+    );
+}
+
+#[test]
+fn ymdhms22_2() {
+    test_dates(
+        Format::new(Type::YmdHms, 22, 2).unwrap(),
+        &[
+            "1648-06-10 00:00:00.00",
+            "1680-06-30 04:50:38.12",
+            "1716-07-24 12:31:35.23",
+            "1768-06-19 12:47:53.35",
+            "1819-08-02 01:26:00.46",
+            "1839-03-27 20:58:11.57",
+            "1903-04-19 07:36:05.19",
+            "1929-08-25 15:43:49.83",
+            "1941-09-29 04:25:09.01",
+            "1943-04-19 06:49:27.52",
+            "1943-10-07 02:57:52.02",
+            "1992-03-17 16:45:44.87",
+            "1996-02-25 21:30:57.82",
+            "1941-09-29 04:25:09.15",
+            "1943-04-19 06:49:27.11",
+            "1943-10-07 02:57:52.48",
+            "1992-03-17 16:45:44.66",
+            "1996-02-25 21:30:57.58",
+            "2038-11-10 22:30:04.18",
+            "2094-07-18 01:56:51.59",
+        ],
+    );
+}
+
+#[test]
+fn ymdhms23_3() {
+    test_dates(
+        Format::new(Type::YmdHms, 23, 3).unwrap(),
+        &[
+            "1648-06-10 00:00:00.000",
+            "1680-06-30 04:50:38.123",
+            "1716-07-24 12:31:35.235",
+            "1768-06-19 12:47:53.345",
+            "1819-08-02 01:26:00.456",
+            "1839-03-27 20:58:11.567",
+            "1903-04-19 07:36:05.190",
+            "1929-08-25 15:43:49.831",
+            "1941-09-29 04:25:09.013",
+            "1943-04-19 06:49:27.524",
+            "1943-10-07 02:57:52.016",
+            "1992-03-17 16:45:44.865",
+            "1996-02-25 21:30:57.820",
+            "1941-09-29 04:25:09.154",
+            "1943-04-19 06:49:27.105",
+            "1943-10-07 02:57:52.482",
+            "1992-03-17 16:45:44.658",
+            "1996-02-25 21:30:57.582",
+            "2038-11-10 22:30:04.183",
+            "2094-07-18 01:56:51.593",
+        ],
+    );
+}
+
+#[test]
+fn ymdhms24_4() {
+    test_dates(
+        Format::new(Type::YmdHms, 24, 4).unwrap(),
+        &[
+            "1648-06-10 00:00:00.0000",
+            "1680-06-30 04:50:38.1230",
+            "1716-07-24 12:31:35.2345",
+            "1768-06-19 12:47:53.3450",
+            "1819-08-02 01:26:00.4562",
+            "1839-03-27 20:58:11.5668",
+            "1903-04-19 07:36:05.1896",
+            "1929-08-25 15:43:49.8313",
+            "1941-09-29 04:25:09.0129",
+            "1943-04-19 06:49:27.5238",
+            "1943-10-07 02:57:52.0156",
+            "1992-03-17 16:45:44.8653",
+            "1996-02-25 21:30:57.8205",
+            "1941-09-29 04:25:09.1539",
+            "1943-04-19 06:49:27.1053",
+            "1943-10-07 02:57:52.4823",
+            "1992-03-17 16:45:44.6583",
+            "1996-02-25 21:30:57.5822",
+            "2038-11-10 22:30:04.1835",
+            "2094-07-18 01:56:51.5932",
+        ],
+    );
+}
+
+#[test]
+fn ymdhms25_5() {
+    test_dates(
+        Format::new(Type::YmdHms, 25, 5).unwrap(),
+        &[
+            "1648-06-10 00:00:00.00000",
+            "1680-06-30 04:50:38.12301",
+            "1716-07-24 12:31:35.23453",
+            "1768-06-19 12:47:53.34505",
+            "1819-08-02 01:26:00.45615",
+            "1839-03-27 20:58:11.56677",
+            "1903-04-19 07:36:05.18964",
+            "1929-08-25 15:43:49.83132",
+            "1941-09-29 04:25:09.01293",
+            "1943-04-19 06:49:27.52375",
+            "1943-10-07 02:57:52.01565",
+            "1992-03-17 16:45:44.86529",
+            "1996-02-25 21:30:57.82047",
+            "1941-09-29 04:25:09.15395",
+            "1943-04-19 06:49:27.10533",
+            "1943-10-07 02:57:52.48229",
+            "1992-03-17 16:45:44.65827",
+            "1996-02-25 21:30:57.58219",
+            "2038-11-10 22:30:04.18347",
+            "2094-07-18 01:56:51.59319",
+        ],
+    );
+}
+
+fn test_times(format: Format, name: &str) {
+    let directory = Path::new("src/format/testdata/display");
+    let input_filename = directory.join("time-input.txt");
+    let input = BufReader::new(File::open(&input_filename).unwrap());
+
+    let output_filename = directory.join(name);
+    let output = BufReader::new(File::open(&output_filename).unwrap());
+
+    let parser = Type::DTime.parser(UTF_8);
+    for ((input, expect), line_number) in input
+        .lines()
+        .map(|r| r.unwrap())
+        .zip_eq(output.lines().map(|r| r.unwrap()))
+        .zip(1..)
+    {
+        let formatted = parser
+            .parse(input)
+            .unwrap()
+            .with_encoding(UTF_8)
+            .display(format)
+            .to_string();
+        assert!(
+                formatted == expect,
+                "formatting {}:{line_number} as {format}:\n  actual: {formatted:?}\nexpected: {expect:?}",
+                input_filename.display()
+            );
+    }
+}
+
+#[test]
+fn time5() {
+    test_times(Format::new(Type::Time, 5, 0).unwrap(), "time5.txt");
+}
+
+#[test]
+fn time6() {
+    test_times(Format::new(Type::Time, 6, 0).unwrap(), "time6.txt");
+}
+
+#[test]
+fn time7() {
+    test_times(Format::new(Type::Time, 7, 0).unwrap(), "time7.txt");
+}
+
+#[test]
+fn time8() {
+    test_times(Format::new(Type::Time, 8, 0).unwrap(), "time8.txt");
+}
+
+#[test]
+fn time9() {
+    test_times(Format::new(Type::Time, 9, 0).unwrap(), "time9.txt");
+}
+
+#[test]
+fn time10() {
+    test_times(Format::new(Type::Time, 10, 0).unwrap(), "time10.txt");
+}
+
+#[test]
+fn time10_1() {
+    test_times(Format::new(Type::Time, 10, 1).unwrap(), "time10.1.txt");
+}
+
+#[test]
+fn time11() {
+    test_times(Format::new(Type::Time, 11, 0).unwrap(), "time11.txt");
+}
+
+#[test]
+fn time11_1() {
+    test_times(Format::new(Type::Time, 11, 1).unwrap(), "time11.1.txt");
+}
+
+#[test]
+fn time11_2() {
+    test_times(Format::new(Type::Time, 11, 2).unwrap(), "time11.2.txt");
+}
+
+#[test]
+fn time12() {
+    test_times(Format::new(Type::Time, 12, 0).unwrap(), "time12.txt");
+}
+
+#[test]
+fn time12_1() {
+    test_times(Format::new(Type::Time, 12, 1).unwrap(), "time12.1.txt");
+}
+
+#[test]
+fn time12_2() {
+    test_times(Format::new(Type::Time, 12, 2).unwrap(), "time12.2.txt");
+}
+
+#[test]
+fn time12_3() {
+    test_times(Format::new(Type::Time, 12, 3).unwrap(), "time12.3.txt");
+}
+
+#[test]
+fn time13() {
+    test_times(Format::new(Type::Time, 13, 0).unwrap(), "time13.txt");
+}
+
+#[test]
+fn time13_1() {
+    test_times(Format::new(Type::Time, 13, 1).unwrap(), "time13.1.txt");
+}
+
+#[test]
+fn time13_2() {
+    test_times(Format::new(Type::Time, 13, 2).unwrap(), "time13.2.txt");
+}
+
+#[test]
+fn time13_3() {
+    test_times(Format::new(Type::Time, 13, 3).unwrap(), "time13.3.txt");
+}
+
+#[test]
+fn time13_4() {
+    test_times(Format::new(Type::Time, 13, 4).unwrap(), "time13.4.txt");
+}
+
+#[test]
+fn time14() {
+    test_times(Format::new(Type::Time, 14, 0).unwrap(), "time14.txt");
+}
+
+#[test]
+fn time14_1() {
+    test_times(Format::new(Type::Time, 14, 1).unwrap(), "time14.1.txt");
+}
+
+#[test]
+fn time14_2() {
+    test_times(Format::new(Type::Time, 14, 2).unwrap(), "time14.2.txt");
+}
+
+#[test]
+fn time14_3() {
+    test_times(Format::new(Type::Time, 14, 3).unwrap(), "time14.3.txt");
+}
+
+#[test]
+fn time14_4() {
+    test_times(Format::new(Type::Time, 14, 4).unwrap(), "time14.4.txt");
+}
+
+#[test]
+fn time14_5() {
+    test_times(Format::new(Type::Time, 14, 5).unwrap(), "time14.5.txt");
+}
+
+#[test]
+fn time15() {
+    test_times(Format::new(Type::Time, 15, 0).unwrap(), "time15.txt");
+}
+
+#[test]
+fn time15_1() {
+    test_times(Format::new(Type::Time, 15, 1).unwrap(), "time15.1.txt");
+}
+
+#[test]
+fn time15_2() {
+    test_times(Format::new(Type::Time, 15, 2).unwrap(), "time15.2.txt");
+}
+
+#[test]
+fn time15_3() {
+    test_times(Format::new(Type::Time, 15, 3).unwrap(), "time15.3.txt");
+}
+
+#[test]
+fn time15_4() {
+    test_times(Format::new(Type::Time, 15, 4).unwrap(), "time15.4.txt");
+}
+
+#[test]
+fn time15_5() {
+    test_times(Format::new(Type::Time, 15, 5).unwrap(), "time15.5.txt");
+}
+
+#[test]
+fn time15_6() {
+    test_times(Format::new(Type::Time, 15, 6).unwrap(), "time15.6.txt");
+}
+
+#[test]
+fn mtime5() {
+    test_times(Format::new(Type::MTime, 5, 0).unwrap(), "mtime5.txt");
+}
+
+#[test]
+fn mtime6() {
+    test_times(Format::new(Type::MTime, 6, 0).unwrap(), "mtime6.txt");
+}
+
+#[test]
+fn mtime7() {
+    test_times(Format::new(Type::MTime, 7, 0).unwrap(), "mtime7.txt");
+}
+
+#[test]
+fn mtime7_1() {
+    test_times(Format::new(Type::MTime, 7, 1).unwrap(), "mtime7.1.txt");
+}
+
+#[test]
+fn mtime8() {
+    test_times(Format::new(Type::MTime, 8, 0).unwrap(), "mtime8.txt");
+}
+
+#[test]
+fn mtime8_1() {
+    test_times(Format::new(Type::MTime, 8, 1).unwrap(), "mtime8.1.txt");
+}
+
+#[test]
+fn mtime8_2() {
+    test_times(Format::new(Type::MTime, 8, 2).unwrap(), "mtime8.2.txt");
+}
+
+#[test]
+fn mtime9() {
+    test_times(Format::new(Type::MTime, 9, 0).unwrap(), "mtime9.txt");
+}
+
+#[test]
+fn mtime9_1() {
+    test_times(Format::new(Type::MTime, 9, 1).unwrap(), "mtime9.1.txt");
+}
+
+#[test]
+fn mtime9_2() {
+    test_times(Format::new(Type::MTime, 9, 2).unwrap(), "mtime9.2.txt");
+}
+
+#[test]
+fn mtime9_3() {
+    test_times(Format::new(Type::MTime, 9, 3).unwrap(), "mtime9.3.txt");
+}
+
+#[test]
+fn mtime10() {
+    test_times(Format::new(Type::MTime, 10, 0).unwrap(), "mtime10.txt");
+}
+
+#[test]
+fn mtime10_1() {
+    test_times(Format::new(Type::MTime, 10, 1).unwrap(), "mtime10.1.txt");
+}
+
+#[test]
+fn mtime10_2() {
+    test_times(Format::new(Type::MTime, 10, 2).unwrap(), "mtime10.2.txt");
+}
+
+#[test]
+fn mtime10_3() {
+    test_times(Format::new(Type::MTime, 10, 3).unwrap(), "mtime10.3.txt");
+}
+
+#[test]
+fn mtime10_4() {
+    test_times(Format::new(Type::MTime, 10, 4).unwrap(), "mtime10.4.txt");
+}
+
+#[test]
+fn mtime11() {
+    test_times(Format::new(Type::MTime, 11, 0).unwrap(), "mtime11.txt");
+}
+
+#[test]
+fn mtime11_1() {
+    test_times(Format::new(Type::MTime, 11, 1).unwrap(), "mtime11.1.txt");
+}
+
+#[test]
+fn mtime11_2() {
+    test_times(Format::new(Type::MTime, 11, 2).unwrap(), "mtime11.2.txt");
+}
+
+#[test]
+fn mtime11_3() {
+    test_times(Format::new(Type::MTime, 11, 3).unwrap(), "mtime11.3.txt");
+}
+
+#[test]
+fn mtime11_4() {
+    test_times(Format::new(Type::MTime, 11, 4).unwrap(), "mtime11.4.txt");
+}
+
+#[test]
+fn mtime11_5() {
+    test_times(Format::new(Type::MTime, 11, 5).unwrap(), "mtime11.5.txt");
+}
+
+#[test]
+fn mtime12_5() {
+    test_times(Format::new(Type::MTime, 12, 5).unwrap(), "mtime12.5.txt");
+}
+
+#[test]
+fn mtime13_5() {
+    test_times(Format::new(Type::MTime, 13, 5).unwrap(), "mtime13.5.txt");
+}
+
+#[test]
+fn mtime14_5() {
+    test_times(Format::new(Type::MTime, 14, 5).unwrap(), "mtime14.5.txt");
+}
+
+#[test]
+fn mtime15_5() {
+    test_times(Format::new(Type::MTime, 15, 5).unwrap(), "mtime15.5.txt");
+}
+
+#[test]
+fn mtime16_5() {
+    test_times(Format::new(Type::MTime, 16, 5).unwrap(), "mtime16.5.txt");
+}
+
+#[test]
+fn dtime8() {
+    test_times(Format::new(Type::DTime, 8, 0).unwrap(), "dtime8.txt");
+}
+
+#[test]
+fn dtime9() {
+    test_times(Format::new(Type::DTime, 9, 0).unwrap(), "dtime9.txt");
+}
+
+#[test]
+fn dtime10() {
+    test_times(Format::new(Type::DTime, 10, 0).unwrap(), "dtime10.txt");
+}
+
+#[test]
+fn dtime11() {
+    test_times(Format::new(Type::DTime, 11, 0).unwrap(), "dtime11.txt");
+}
+
+#[test]
+fn dtime12() {
+    test_times(Format::new(Type::DTime, 12, 0).unwrap(), "dtime12.txt");
+}
+
+#[test]
+fn dtime13() {
+    test_times(Format::new(Type::DTime, 13, 0).unwrap(), "dtime13.txt");
+}
+
+#[test]
+fn dtime13_1() {
+    test_times(Format::new(Type::DTime, 13, 1).unwrap(), "dtime13.1.txt");
+}
+
+#[test]
+fn dtime14() {
+    test_times(Format::new(Type::DTime, 14, 0).unwrap(), "dtime14.txt");
+}
+
+#[test]
+fn dtime14_1() {
+    test_times(Format::new(Type::DTime, 14, 1).unwrap(), "dtime14.1.txt");
+}
+
+#[test]
+fn dtime14_2() {
+    test_times(Format::new(Type::DTime, 14, 2).unwrap(), "dtime14.2.txt");
+}
+
+#[test]
+fn dtime15() {
+    test_times(Format::new(Type::DTime, 15, 0).unwrap(), "dtime15.txt");
+}
+
+#[test]
+fn dtime15_1() {
+    test_times(Format::new(Type::DTime, 15, 1).unwrap(), "dtime15.1.txt");
+}
+
+#[test]
+fn dtime15_2() {
+    test_times(Format::new(Type::DTime, 15, 2).unwrap(), "dtime15.2.txt");
+}
+
+#[test]
+fn dtime15_3() {
+    test_times(Format::new(Type::DTime, 15, 3).unwrap(), "dtime15.3.txt");
+}
+
+#[test]
+fn dtime16() {
+    test_times(Format::new(Type::DTime, 16, 0).unwrap(), "dtime16.txt");
+}
+
+#[test]
+fn dtime16_1() {
+    test_times(Format::new(Type::DTime, 16, 1).unwrap(), "dtime16.1.txt");
+}
+
+#[test]
+fn dtime16_2() {
+    test_times(Format::new(Type::DTime, 16, 2).unwrap(), "dtime16.2.txt");
+}
+
+#[test]
+fn dtime16_3() {
+    test_times(Format::new(Type::DTime, 16, 3).unwrap(), "dtime16.3.txt");
+}
+
+#[test]
+fn dtime16_4() {
+    test_times(Format::new(Type::DTime, 16, 4).unwrap(), "dtime16.4.txt");
+}
+
+#[test]
+fn dtime17() {
+    test_times(Format::new(Type::DTime, 17, 0).unwrap(), "dtime17.txt");
+}
+
+#[test]
+fn dtime17_1() {
+    test_times(Format::new(Type::DTime, 17, 1).unwrap(), "dtime17.1.txt");
+}
+
+#[test]
+fn dtime17_2() {
+    test_times(Format::new(Type::DTime, 17, 2).unwrap(), "dtime17.2.txt");
+}
+
+#[test]
+fn dtime17_3() {
+    test_times(Format::new(Type::DTime, 17, 3).unwrap(), "dtime17.3.txt");
+}
+
+#[test]
+fn dtime17_4() {
+    test_times(Format::new(Type::DTime, 17, 4).unwrap(), "dtime17.4.txt");
+}
+
+#[test]
+fn dtime17_5() {
+    test_times(Format::new(Type::DTime, 17, 5).unwrap(), "dtime17.5.txt");
+}
+
+#[test]
+fn dtime18() {
+    test_times(Format::new(Type::DTime, 18, 0).unwrap(), "dtime18.txt");
+}
+
+#[test]
+fn dtime18_1() {
+    test_times(Format::new(Type::DTime, 18, 1).unwrap(), "dtime18.1.txt");
+}
+
+#[test]
+fn dtime18_2() {
+    test_times(Format::new(Type::DTime, 18, 2).unwrap(), "dtime18.2.txt");
+}
+
+#[test]
+fn dtime18_3() {
+    test_times(Format::new(Type::DTime, 18, 3).unwrap(), "dtime18.3.txt");
+}
+
+#[test]
+fn dtime18_4() {
+    test_times(Format::new(Type::DTime, 18, 4).unwrap(), "dtime18.4.txt");
+}
+
+#[test]
+fn dtime18_5() {
+    test_times(Format::new(Type::DTime, 18, 5).unwrap(), "dtime18.5.txt");
+}
+
+#[test]
+fn dtime18_6() {
+    test_times(Format::new(Type::DTime, 18, 6).unwrap(), "dtime18.6.txt");
+}
index f6a795ed2a86719013e724603e2a52211e222d5f..2161e3dc0a6c8d3e43b05602e4734b83b4eddf2f 100644 (file)
@@ -910,7 +910,7 @@ fn nibble(b: u8) -> Result<u128, ParseErrorKind> {
 }
 
 #[cfg(test)]
-mod test {
+mod tests {
     use std::{
         fs::File,
         io::{BufRead, BufReader},
index fcb1bc341676e0cd25f0e3152e9a116123468aad..4a3d2193f3abbaa34908092189d1a9bc2874ce6d 100644 (file)
@@ -479,4 +479,4 @@ impl Iterator for StringScanner<'_> {
 }
 
 #[cfg(test)]
-mod test;
+mod tests;
diff --git a/rust/pspp/src/lex/scan/test.rs b/rust/pspp/src/lex/scan/test.rs
deleted file mode 100644 (file)
index 6f0e582..0000000
+++ /dev/null
@@ -1,1036 +0,0 @@
-// PSPP - a program for statistical analysis.
-// Copyright (C) 2025 Free Software Foundation, Inc.
-//
-// This program is free software: you can redistribute it and/or modify it under
-// the terms of the GNU General Public License as published by the Free Software
-// Foundation, either version 3 of the License, or (at your option) any later
-// version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-// FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
-// details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program.  If not, see <http://www.gnu.org/licenses/>.
-
-use crate::{
-    identifier::Identifier,
-    lex::{
-        segment::Syntax,
-        token::{Punct, Token},
-    },
-};
-
-use super::{ScanError, StringScanner};
-
-fn print_token(token: &Token) {
-    match token {
-        Token::Id(s) => print!("Token::Id(String::from({s:?}))"),
-        Token::Number(number) => print!("Token::Number({number:?})"),
-        Token::String(s) => print!("Token::String(String::from({s:?}))"),
-        Token::End => print!("Token::EndCommand"),
-        Token::Punct(punct) => print!("Token::Punct(Punct::{punct:?})"),
-    }
-}
-
-#[track_caller]
-fn check_scan(input: &str, mode: Syntax, expected: &[Result<Token, ScanError>]) {
-    let tokens = StringScanner::new(input, mode, false).collect::<Vec<_>>();
-
-    if tokens != expected {
-        for token in &tokens {
-            match token {
-                Ok(token) => {
-                    print!("Ok(");
-                    print_token(token);
-                    print!(")");
-                }
-                Err(error) => print!("Err(ScanError::{error:?})"),
-            }
-            println!(",");
-        }
-
-        eprintln!("tokens differ from expected:");
-        let difference = diff::slice(expected, &tokens);
-        for result in difference {
-            match result {
-                diff::Result::Left(left) => eprintln!("-{left:?}"),
-                diff::Result::Both(left, _right) => eprintln!(" {left:?}"),
-                diff::Result::Right(right) => eprintln!("+{right:?}"),
-            }
-        }
-        panic!();
-    }
-}
-
-#[test]
-fn test_identifiers() {
-    check_scan(
-        r#"a aB i5 $x @efg @@. !abcd !* !*a #.# .x _z.
-abcd. abcd.
-QRSTUV./* end of line comment */
-QrStUv./* end of line comment */ 
-WXYZ. /* unterminated end of line comment
-�. /* U+FFFD is not valid in an identifier
-"#,
-        Syntax::Auto,
-        &[
-            Ok(Token::Id(Identifier::new("a").unwrap())),
-            Ok(Token::Id(Identifier::new("aB").unwrap())),
-            Ok(Token::Id(Identifier::new("i5").unwrap())),
-            Ok(Token::Id(Identifier::new("$x").unwrap())),
-            Ok(Token::Id(Identifier::new("@efg").unwrap())),
-            Ok(Token::Id(Identifier::new("@@.").unwrap())),
-            Ok(Token::Id(Identifier::new("!abcd").unwrap())),
-            Ok(Token::Punct(Punct::BangAsterisk)),
-            Ok(Token::Punct(Punct::BangAsterisk)),
-            Ok(Token::Id(Identifier::new("a").unwrap())),
-            Ok(Token::Id(Identifier::new("#.#").unwrap())),
-            Ok(Token::Punct(Punct::Dot)),
-            Ok(Token::Id(Identifier::new("x").unwrap())),
-            Ok(Token::Punct(Punct::Underscore)),
-            Ok(Token::Id(Identifier::new("z").unwrap())),
-            Ok(Token::End),
-            Ok(Token::Id(Identifier::new("abcd.").unwrap())),
-            Ok(Token::Id(Identifier::new("abcd").unwrap())),
-            Ok(Token::End),
-            Ok(Token::Id(Identifier::new("QRSTUV").unwrap())),
-            Ok(Token::End),
-            Ok(Token::Id(Identifier::new("QrStUv").unwrap())),
-            Ok(Token::End),
-            Ok(Token::Id(Identifier::new("WXYZ").unwrap())),
-            Ok(Token::End),
-            Err(ScanError::UnexpectedChar('�')),
-            Ok(Token::End),
-        ],
-    );
-}
-
-#[test]
-fn test_reserved_words() {
-    check_scan(
-        r#"and or not eq ge gt le lt ne all by to with
-AND OR NOT EQ GE GT LE LT NE ALL BY TO WITH
-andx orx notx eqx gex gtx lex ltx nex allx byx tox withx
-and. with.
-"#,
-        Syntax::Auto,
-        &[
-            Ok(Token::Punct(Punct::And)),
-            Ok(Token::Punct(Punct::Or)),
-            Ok(Token::Punct(Punct::Not)),
-            Ok(Token::Punct(Punct::Eq)),
-            Ok(Token::Punct(Punct::Ge)),
-            Ok(Token::Punct(Punct::Gt)),
-            Ok(Token::Punct(Punct::Le)),
-            Ok(Token::Punct(Punct::Lt)),
-            Ok(Token::Punct(Punct::Ne)),
-            Ok(Token::Punct(Punct::All)),
-            Ok(Token::Punct(Punct::By)),
-            Ok(Token::Punct(Punct::To)),
-            Ok(Token::Punct(Punct::With)),
-            Ok(Token::Punct(Punct::And)),
-            Ok(Token::Punct(Punct::Or)),
-            Ok(Token::Punct(Punct::Not)),
-            Ok(Token::Punct(Punct::Eq)),
-            Ok(Token::Punct(Punct::Ge)),
-            Ok(Token::Punct(Punct::Gt)),
-            Ok(Token::Punct(Punct::Le)),
-            Ok(Token::Punct(Punct::Lt)),
-            Ok(Token::Punct(Punct::Ne)),
-            Ok(Token::Punct(Punct::All)),
-            Ok(Token::Punct(Punct::By)),
-            Ok(Token::Punct(Punct::To)),
-            Ok(Token::Punct(Punct::With)),
-            Ok(Token::Id(Identifier::new("andx").unwrap())),
-            Ok(Token::Id(Identifier::new("orx").unwrap())),
-            Ok(Token::Id(Identifier::new("notx").unwrap())),
-            Ok(Token::Id(Identifier::new("eqx").unwrap())),
-            Ok(Token::Id(Identifier::new("gex").unwrap())),
-            Ok(Token::Id(Identifier::new("gtx").unwrap())),
-            Ok(Token::Id(Identifier::new("lex").unwrap())),
-            Ok(Token::Id(Identifier::new("ltx").unwrap())),
-            Ok(Token::Id(Identifier::new("nex").unwrap())),
-            Ok(Token::Id(Identifier::new("allx").unwrap())),
-            Ok(Token::Id(Identifier::new("byx").unwrap())),
-            Ok(Token::Id(Identifier::new("tox").unwrap())),
-            Ok(Token::Id(Identifier::new("withx").unwrap())),
-            Ok(Token::Id(Identifier::new("and.").unwrap())),
-            Ok(Token::Punct(Punct::With)),
-            Ok(Token::End),
-        ],
-    );
-}
-
-#[test]
-fn test_punctuation() {
-    check_scan(
-        r#"~ & | = >= > <= < ~= <> ( ) , - + * / [ ] **
-~&|=>=><=<~=<>(),-+*/[]**
-% : ; ? _ ` { } ~
-"#,
-        Syntax::Auto,
-        &[
-            Ok(Token::Punct(Punct::Not)),
-            Ok(Token::Punct(Punct::And)),
-            Ok(Token::Punct(Punct::Or)),
-            Ok(Token::Punct(Punct::Equals)),
-            Ok(Token::Punct(Punct::Ge)),
-            Ok(Token::Punct(Punct::Gt)),
-            Ok(Token::Punct(Punct::Le)),
-            Ok(Token::Punct(Punct::Lt)),
-            Ok(Token::Punct(Punct::Ne)),
-            Ok(Token::Punct(Punct::Ne)),
-            Ok(Token::Punct(Punct::LParen)),
-            Ok(Token::Punct(Punct::RParen)),
-            Ok(Token::Punct(Punct::Comma)),
-            Ok(Token::Punct(Punct::Dash)),
-            Ok(Token::Punct(Punct::Plus)),
-            Ok(Token::Punct(Punct::Asterisk)),
-            Ok(Token::Punct(Punct::Slash)),
-            Ok(Token::Punct(Punct::LSquare)),
-            Ok(Token::Punct(Punct::RSquare)),
-            Ok(Token::Punct(Punct::Exp)),
-            Ok(Token::Punct(Punct::Not)),
-            Ok(Token::Punct(Punct::And)),
-            Ok(Token::Punct(Punct::Or)),
-            Ok(Token::Punct(Punct::Equals)),
-            Ok(Token::Punct(Punct::Ge)),
-            Ok(Token::Punct(Punct::Gt)),
-            Ok(Token::Punct(Punct::Le)),
-            Ok(Token::Punct(Punct::Lt)),
-            Ok(Token::Punct(Punct::Ne)),
-            Ok(Token::Punct(Punct::Ne)),
-            Ok(Token::Punct(Punct::LParen)),
-            Ok(Token::Punct(Punct::RParen)),
-            Ok(Token::Punct(Punct::Comma)),
-            Ok(Token::Punct(Punct::Dash)),
-            Ok(Token::Punct(Punct::Plus)),
-            Ok(Token::Punct(Punct::Asterisk)),
-            Ok(Token::Punct(Punct::Slash)),
-            Ok(Token::Punct(Punct::LSquare)),
-            Ok(Token::Punct(Punct::RSquare)),
-            Ok(Token::Punct(Punct::Exp)),
-            Ok(Token::Punct(Punct::Percent)),
-            Ok(Token::Punct(Punct::Colon)),
-            Ok(Token::Punct(Punct::Semicolon)),
-            Ok(Token::Punct(Punct::Question)),
-            Ok(Token::Punct(Punct::Underscore)),
-            Ok(Token::Punct(Punct::Backtick)),
-            Ok(Token::Punct(Punct::LCurly)),
-            Ok(Token::Punct(Punct::RCurly)),
-            Ok(Token::Punct(Punct::Not)),
-        ],
-    );
-}
-
-#[test]
-fn test_positive_numbers() {
-    check_scan(
-        r#"0 1 01 001. 1.
-123. /* comment 1 */ /* comment 2 */
-.1 0.1 00.1 00.10
-5e1 6E-1 7e+1 6E+01 6e-03
-.3E1 .4e-1 .5E+1 .6e+01 .7E-03
-1.23e1 45.6E-1 78.9e+1 99.9E+01 11.2e-03
-. 1e e1 1e+ 1e-
-"#,
-        Syntax::Auto,
-        &[
-            Ok(Token::Number(0.0)),
-            Ok(Token::Number(1.0)),
-            Ok(Token::Number(1.0)),
-            Ok(Token::Number(1.0)),
-            Ok(Token::Number(1.0)),
-            Ok(Token::End),
-            Ok(Token::Number(123.0)),
-            Ok(Token::End),
-            Ok(Token::End),
-            Ok(Token::Number(1.0)),
-            Ok(Token::Number(0.1)),
-            Ok(Token::Number(0.1)),
-            Ok(Token::Number(0.1)),
-            Ok(Token::Number(50.0)),
-            Ok(Token::Number(0.6)),
-            Ok(Token::Number(70.0)),
-            Ok(Token::Number(60.0)),
-            Ok(Token::Number(0.006)),
-            Ok(Token::End),
-            Ok(Token::Number(30.0)),
-            Ok(Token::Number(0.04)),
-            Ok(Token::Number(5.0)),
-            Ok(Token::Number(6.0)),
-            Ok(Token::Number(0.0007)),
-            Ok(Token::Number(12.3)),
-            Ok(Token::Number(4.56)),
-            Ok(Token::Number(789.0)),
-            Ok(Token::Number(999.0)),
-            Ok(Token::Number(0.0112)),
-            Ok(Token::End),
-            Err(ScanError::ExpectedExponent(String::from("1e"))),
-            Ok(Token::Id(Identifier::new("e1").unwrap())),
-            Err(ScanError::ExpectedExponent(String::from("1e+"))),
-            Err(ScanError::ExpectedExponent(String::from("1e-"))),
-        ],
-    );
-}
-
-#[test]
-fn test_negative_numbers() {
-    check_scan(
-        r#" -0 -1 -01 -001. -1.
- -123. /* comment 1 */ /* comment 2 */
- -.1 -0.1 -00.1 -00.10
- -5e1 -6E-1 -7e+1 -6E+01 -6e-03
- -.3E1 -.4e-1 -.5E+1 -.6e+01 -.7E-03
- -1.23e1 -45.6E-1 -78.9e+1 -99.9E+01 -11.2e-03
- -/**/1
- -. -1e -e1 -1e+ -1e- -1.
-"#,
-        Syntax::Auto,
-        &[
-            Ok(Token::Number(-0.0)),
-            Ok(Token::Number(-1.0)),
-            Ok(Token::Number(-1.0)),
-            Ok(Token::Number(-1.0)),
-            Ok(Token::Number(-1.0)),
-            Ok(Token::End),
-            Ok(Token::Number(-123.0)),
-            Ok(Token::End),
-            Ok(Token::Number(-0.1)),
-            Ok(Token::Number(-0.1)),
-            Ok(Token::Number(-0.1)),
-            Ok(Token::Number(-0.1)),
-            Ok(Token::Number(-50.0)),
-            Ok(Token::Number(-0.6)),
-            Ok(Token::Number(-70.0)),
-            Ok(Token::Number(-60.0)),
-            Ok(Token::Number(-0.006)),
-            Ok(Token::Number(-3.0)),
-            Ok(Token::Number(-0.04)),
-            Ok(Token::Number(-5.0)),
-            Ok(Token::Number(-6.0)),
-            Ok(Token::Number(-0.0007)),
-            Ok(Token::Number(-12.3)),
-            Ok(Token::Number(-4.56)),
-            Ok(Token::Number(-789.0)),
-            Ok(Token::Number(-999.0)),
-            Ok(Token::Number(-0.0112)),
-            Ok(Token::Number(-1.0)),
-            Ok(Token::Punct(Punct::Dash)),
-            Ok(Token::Punct(Punct::Dot)),
-            Err(ScanError::ExpectedExponent(String::from("-1e"))),
-            Ok(Token::Punct(Punct::Dash)),
-            Ok(Token::Id(Identifier::new("e1").unwrap())),
-            Err(ScanError::ExpectedExponent(String::from("-1e+"))),
-            Err(ScanError::ExpectedExponent(String::from("-1e-"))),
-            Ok(Token::Number(-1.0)),
-            Ok(Token::End),
-        ],
-    );
-}
-
-#[test]
-fn test_strings() {
-    check_scan(
-        r#"'x' "y" 'abc'
-'Don''t' "Can't" 'Won''t'
-"""quoted""" '"quoted"'
-'' "" '''' """"
-'missing end quote
-"missing double quote
-'x' + "y"
-+ 'z' +
-'a' /* abc */ + "b" /*
-+ 'c' +/* */"d"/* */+'e'
-'foo'
-+          /* special case: + in column 0 would ordinarily start a new command
-'bar'
-'foo'
- +
-'bar'
-'foo'
-+
-
-'bar'
-
-+
-x"4142"+'5152'
-"4142"+
-x'5152'
-x"4142"
-+u'304a'
-"�あいうえお"
-"abc"+U"FFFD"+u'3048'+"xyz"
-"#,
-        Syntax::Auto,
-        &[
-            Ok(Token::String(String::from("x"))),
-            Ok(Token::String(String::from("y"))),
-            Ok(Token::String(String::from("abc"))),
-            Ok(Token::String(String::from("Don't"))),
-            Ok(Token::String(String::from("Can't"))),
-            Ok(Token::String(String::from("Won't"))),
-            Ok(Token::String(String::from("\"quoted\""))),
-            Ok(Token::String(String::from("\"quoted\""))),
-            Ok(Token::String(String::from(""))),
-            Ok(Token::String(String::from(""))),
-            Ok(Token::String(String::from("'"))),
-            Ok(Token::String(String::from("\""))),
-            Err(ScanError::ExpectedQuote),
-            Err(ScanError::ExpectedQuote),
-            Ok(Token::String(String::from("xyzabcde"))),
-            Ok(Token::String(String::from("foobar"))),
-            Ok(Token::String(String::from("foobar"))),
-            Ok(Token::String(String::from("foo"))),
-            Ok(Token::Punct(Punct::Plus)),
-            Ok(Token::End),
-            Ok(Token::String(String::from("bar"))),
-            Ok(Token::End),
-            Ok(Token::Punct(Punct::Plus)),
-            Ok(Token::String(String::from("AB5152"))),
-            Ok(Token::String(String::from("4142QR"))),
-            Ok(Token::String(String::from("ABお"))),
-            Ok(Token::String(String::from("�あいうえお"))),
-            Ok(Token::String(String::from("abc�えxyz"))),
-        ],
-    );
-}
-
-#[test]
-fn test_shbang() {
-    check_scan(
-        r#"#! /usr/bin/pspp
-#! /usr/bin/pspp
-"#,
-        Syntax::Auto,
-        &[
-            Ok(Token::Id(Identifier::new("#").unwrap())),
-            Ok(Token::Punct(Punct::Bang)),
-            Ok(Token::Punct(Punct::Slash)),
-            Ok(Token::Id(Identifier::new("usr").unwrap())),
-            Ok(Token::Punct(Punct::Slash)),
-            Ok(Token::Id(Identifier::new("bin").unwrap())),
-            Ok(Token::Punct(Punct::Slash)),
-            Ok(Token::Id(Identifier::new("pspp").unwrap())),
-        ],
-    );
-}
-
-#[test]
-fn test_comments() {
-    check_scan(
-        r#"* Comment commands "don't
-have to contain valid tokens.
-
-** Check ambiguity with ** token.
-****************.
-
-comment keyword works too.
-COMM also.
-com is ambiguous with COMPUTE.
-
-   * Comment need not start at left margin.
-
-* Comment ends with blank line
-
-next command.
-
-"#,
-        Syntax::Auto,
-        &[
-            Ok(Token::End),
-            Ok(Token::End),
-            Ok(Token::End),
-            Ok(Token::End),
-            Ok(Token::End),
-            Ok(Token::End),
-            Ok(Token::End),
-            Ok(Token::Id(Identifier::new("com").unwrap())),
-            Ok(Token::Id(Identifier::new("is").unwrap())),
-            Ok(Token::Id(Identifier::new("ambiguous").unwrap())),
-            Ok(Token::Punct(Punct::With)),
-            Ok(Token::Id(Identifier::new("COMPUTE").unwrap())),
-            Ok(Token::End),
-            Ok(Token::End),
-            Ok(Token::End),
-            Ok(Token::End),
-            Ok(Token::End),
-            Ok(Token::Id(Identifier::new("next").unwrap())),
-            Ok(Token::Id(Identifier::new("command").unwrap())),
-            Ok(Token::End),
-            Ok(Token::End),
-        ],
-    );
-}
-
-#[test]
-fn test_document() {
-    check_scan(
-        r#"DOCUMENT one line.
-DOC more
-    than
-        one
-            line.
-docu
-first.paragraph
-isn't parsed as tokens
-
-second paragraph.
-"#,
-        Syntax::Auto,
-        &[
-            Ok(Token::Id(Identifier::new("DOCUMENT").unwrap())),
-            Ok(Token::String(String::from("DOCUMENT one line."))),
-            Ok(Token::End),
-            Ok(Token::End),
-            Ok(Token::Id(Identifier::new("DOCUMENT").unwrap())),
-            Ok(Token::String(String::from("DOC more"))),
-            Ok(Token::String(String::from("    than"))),
-            Ok(Token::String(String::from("        one"))),
-            Ok(Token::String(String::from("            line."))),
-            Ok(Token::End),
-            Ok(Token::End),
-            Ok(Token::Id(Identifier::new("DOCUMENT").unwrap())),
-            Ok(Token::String(String::from("docu"))),
-            Ok(Token::String(String::from("first.paragraph"))),
-            Ok(Token::String(String::from("isn't parsed as tokens"))),
-            Ok(Token::String(String::from(""))),
-            Ok(Token::String(String::from("second paragraph."))),
-            Ok(Token::End),
-            Ok(Token::End),
-        ],
-    );
-}
-
-#[test]
-fn test_file_label() {
-    check_scan(
-        r#"FIL label isn't quoted.
-FILE
-  lab 'is quoted'.
-FILE /*
-/**/  lab not quoted here either
-
-"#,
-        Syntax::Auto,
-        &[
-            Ok(Token::Id(Identifier::new("FIL").unwrap())),
-            Ok(Token::Id(Identifier::new("label").unwrap())),
-            Ok(Token::String(String::from("isn't quoted"))),
-            Ok(Token::End),
-            Ok(Token::Id(Identifier::new("FILE").unwrap())),
-            Ok(Token::Id(Identifier::new("lab").unwrap())),
-            Ok(Token::String(String::from("is quoted"))),
-            Ok(Token::End),
-            Ok(Token::Id(Identifier::new("FILE").unwrap())),
-            Ok(Token::Id(Identifier::new("lab").unwrap())),
-            Ok(Token::String(String::from("not quoted here either"))),
-            Ok(Token::End),
-        ],
-    );
-}
-
-#[test]
-fn test_begin_data() {
-    check_scan(
-        r#"begin data.
-123
-xxx
-end data.
-
-BEG /**/ DAT /*
-5 6 7 /* x
-
-end  data
-end data
-.
-"#,
-        Syntax::Auto,
-        &[
-            Ok(Token::Id(Identifier::new("begin").unwrap())),
-            Ok(Token::Id(Identifier::new("data").unwrap())),
-            Ok(Token::End),
-            Ok(Token::String(String::from("123"))),
-            Ok(Token::String(String::from("xxx"))),
-            Ok(Token::Id(Identifier::new("end").unwrap())),
-            Ok(Token::Id(Identifier::new("data").unwrap())),
-            Ok(Token::End),
-            Ok(Token::End),
-            Ok(Token::Id(Identifier::new("BEG").unwrap())),
-            Ok(Token::Id(Identifier::new("DAT").unwrap())),
-            Ok(Token::String(String::from("5 6 7 /* x"))),
-            Ok(Token::String(String::from(""))),
-            Ok(Token::String(String::from("end  data"))),
-            Ok(Token::Id(Identifier::new("end").unwrap())),
-            Ok(Token::Id(Identifier::new("data").unwrap())),
-            Ok(Token::End),
-        ],
-    );
-}
-
-#[test]
-fn test_do_repeat() {
-    check_scan(
-        r#"do repeat x=a b c
-          y=d e f.
-  do repeat a=1 thru 5.
-another command.
-second command
-+ third command.
-end /* x */ /* y */ repeat print.
-end
- repeat.
-"#,
-        Syntax::Auto,
-        &[
-            Ok(Token::Id(Identifier::new("do").unwrap())),
-            Ok(Token::Id(Identifier::new("repeat").unwrap())),
-            Ok(Token::Id(Identifier::new("x").unwrap())),
-            Ok(Token::Punct(Punct::Equals)),
-            Ok(Token::Id(Identifier::new("a").unwrap())),
-            Ok(Token::Id(Identifier::new("b").unwrap())),
-            Ok(Token::Id(Identifier::new("c").unwrap())),
-            Ok(Token::Id(Identifier::new("y").unwrap())),
-            Ok(Token::Punct(Punct::Equals)),
-            Ok(Token::Id(Identifier::new("d").unwrap())),
-            Ok(Token::Id(Identifier::new("e").unwrap())),
-            Ok(Token::Id(Identifier::new("f").unwrap())),
-            Ok(Token::End),
-            Ok(Token::String(String::from("  do repeat a=1 thru 5."))),
-            Ok(Token::String(String::from("another command."))),
-            Ok(Token::String(String::from("second command"))),
-            Ok(Token::String(String::from("+ third command."))),
-            Ok(Token::String(String::from(
-                "end /* x */ /* y */ repeat print.",
-            ))),
-            Ok(Token::Id(Identifier::new("end").unwrap())),
-            Ok(Token::Id(Identifier::new("repeat").unwrap())),
-            Ok(Token::End),
-        ],
-    );
-}
-
-#[test]
-fn test_do_repeat_batch() {
-    check_scan(
-        r#"do repeat x=a b c
-          y=d e f
-do repeat a=1 thru 5
-another command
-second command
-+ third command
-end /* x */ /* y */ repeat print
-end
- repeat
-do
-  repeat #a=1
-
-  inner command
-end repeat
-"#,
-        Syntax::Batch,
-        &[
-            Ok(Token::Id(Identifier::new("do").unwrap())),
-            Ok(Token::Id(Identifier::new("repeat").unwrap())),
-            Ok(Token::Id(Identifier::new("x").unwrap())),
-            Ok(Token::Punct(Punct::Equals)),
-            Ok(Token::Id(Identifier::new("a").unwrap())),
-            Ok(Token::Id(Identifier::new("b").unwrap())),
-            Ok(Token::Id(Identifier::new("c").unwrap())),
-            Ok(Token::Id(Identifier::new("y").unwrap())),
-            Ok(Token::Punct(Punct::Equals)),
-            Ok(Token::Id(Identifier::new("d").unwrap())),
-            Ok(Token::Id(Identifier::new("e").unwrap())),
-            Ok(Token::Id(Identifier::new("f").unwrap())),
-            Ok(Token::End),
-            Ok(Token::String(String::from("do repeat a=1 thru 5"))),
-            Ok(Token::String(String::from("another command"))),
-            Ok(Token::String(String::from("second command"))),
-            Ok(Token::String(String::from("+ third command"))),
-            Ok(Token::String(String::from(
-                "end /* x */ /* y */ repeat print",
-            ))),
-            Ok(Token::Id(Identifier::new("end").unwrap())),
-            Ok(Token::Id(Identifier::new("repeat").unwrap())),
-            Ok(Token::End),
-            Ok(Token::Id(Identifier::new("do").unwrap())),
-            Ok(Token::Id(Identifier::new("repeat").unwrap())),
-            Ok(Token::Id(Identifier::new("#a").unwrap())),
-            Ok(Token::Punct(Punct::Equals)),
-            Ok(Token::Number(1.0)),
-            Ok(Token::End),
-            Ok(Token::String(String::from("  inner command"))),
-            Ok(Token::Id(Identifier::new("end").unwrap())),
-            Ok(Token::Id(Identifier::new("repeat").unwrap())),
-        ],
-    );
-}
-
-#[test]
-fn test_batch_mode() {
-    check_scan(
-        r#"first command
-     another line of first command
-+  second command
-third command
-
-fourth command.
-   fifth command.
-"#,
-        Syntax::Batch,
-        &[
-            Ok(Token::Id(Identifier::new("first").unwrap())),
-            Ok(Token::Id(Identifier::new("command").unwrap())),
-            Ok(Token::Id(Identifier::new("another").unwrap())),
-            Ok(Token::Id(Identifier::new("line").unwrap())),
-            Ok(Token::Id(Identifier::new("of").unwrap())),
-            Ok(Token::Id(Identifier::new("first").unwrap())),
-            Ok(Token::Id(Identifier::new("command").unwrap())),
-            Ok(Token::End),
-            Ok(Token::Id(Identifier::new("second").unwrap())),
-            Ok(Token::Id(Identifier::new("command").unwrap())),
-            Ok(Token::End),
-            Ok(Token::Id(Identifier::new("third").unwrap())),
-            Ok(Token::Id(Identifier::new("command").unwrap())),
-            Ok(Token::End),
-            Ok(Token::Id(Identifier::new("fourth").unwrap())),
-            Ok(Token::Id(Identifier::new("command").unwrap())),
-            Ok(Token::End),
-            Ok(Token::Id(Identifier::new("fifth").unwrap())),
-            Ok(Token::Id(Identifier::new("command").unwrap())),
-            Ok(Token::End),
-        ],
-    );
-}
-
-mod define {
-    use crate::{
-        identifier::Identifier,
-        lex::{
-            segment::Syntax,
-            token::{Punct, Token},
-        },
-    };
-
-    use super::check_scan;
-
-    #[test]
-    fn test_simple() {
-        check_scan(
-            r#"define !macro1()
-var1 var2 var3
-!enddefine.
-"#,
-            Syntax::Auto,
-            &[
-                Ok(Token::Id(Identifier::new("define").unwrap())),
-                Ok(Token::String(String::from("!macro1"))),
-                Ok(Token::Punct(Punct::LParen)),
-                Ok(Token::Punct(Punct::RParen)),
-                Ok(Token::String(String::from("var1 var2 var3"))),
-                Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
-                Ok(Token::End),
-            ],
-        );
-    }
-
-    #[test]
-    fn test_no_newline_after_parentheses() {
-        check_scan(
-            r#"define !macro1() var1 var2 var3
-!enddefine.
-"#,
-            Syntax::Auto,
-            &[
-                Ok(Token::Id(Identifier::new("define").unwrap())),
-                Ok(Token::String(String::from("!macro1"))),
-                Ok(Token::Punct(Punct::LParen)),
-                Ok(Token::Punct(Punct::RParen)),
-                Ok(Token::String(String::from(" var1 var2 var3"))),
-                Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
-                Ok(Token::End),
-            ],
-        );
-    }
-
-    #[test]
-    fn test_no_newline_before_enddefine() {
-        check_scan(
-            r#"define !macro1()
-var1 var2 var3!enddefine.
-"#,
-            Syntax::Auto,
-            &[
-                Ok(Token::Id(Identifier::new("define").unwrap())),
-                Ok(Token::String(String::from("!macro1"))),
-                Ok(Token::Punct(Punct::LParen)),
-                Ok(Token::Punct(Punct::RParen)),
-                Ok(Token::String(String::from("var1 var2 var3"))),
-                Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
-                Ok(Token::End),
-            ],
-        );
-    }
-
-    #[test]
-    fn test_all_on_one_line() {
-        check_scan(
-            r#"define !macro1()var1 var2 var3!enddefine.
-"#,
-            Syntax::Auto,
-            &[
-                Ok(Token::Id(Identifier::new("define").unwrap())),
-                Ok(Token::String(String::from("!macro1"))),
-                Ok(Token::Punct(Punct::LParen)),
-                Ok(Token::Punct(Punct::RParen)),
-                Ok(Token::String(String::from("var1 var2 var3"))),
-                Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
-                Ok(Token::End),
-            ],
-        );
-    }
-
-    #[test]
-    fn test_empty() {
-        check_scan(
-            r#"define !macro1()
-!enddefine.
-"#,
-            Syntax::Auto,
-            &[
-                Ok(Token::Id(Identifier::new("define").unwrap())),
-                Ok(Token::String(String::from("!macro1"))),
-                Ok(Token::Punct(Punct::LParen)),
-                Ok(Token::Punct(Punct::RParen)),
-                Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
-                Ok(Token::End),
-            ],
-        );
-    }
-
-    #[test]
-    fn test_blank_lines() {
-        check_scan(
-            r#"define !macro1()
-
-
-!enddefine.
-"#,
-            Syntax::Auto,
-            &[
-                Ok(Token::Id(Identifier::new("define").unwrap())),
-                Ok(Token::String(String::from("!macro1"))),
-                Ok(Token::Punct(Punct::LParen)),
-                Ok(Token::Punct(Punct::RParen)),
-                Ok(Token::String(String::from(""))),
-                Ok(Token::String(String::from(""))),
-                Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
-                Ok(Token::End),
-            ],
-        );
-    }
-
-    #[test]
-    fn test_arguments() {
-        check_scan(
-            r#"define !macro1(a(), b(), c())
-!enddefine.
-"#,
-            Syntax::Auto,
-            &[
-                Ok(Token::Id(Identifier::new("define").unwrap())),
-                Ok(Token::String(String::from("!macro1"))),
-                Ok(Token::Punct(Punct::LParen)),
-                Ok(Token::Id(Identifier::new("a").unwrap())),
-                Ok(Token::Punct(Punct::LParen)),
-                Ok(Token::Punct(Punct::RParen)),
-                Ok(Token::Punct(Punct::Comma)),
-                Ok(Token::Id(Identifier::new("b").unwrap())),
-                Ok(Token::Punct(Punct::LParen)),
-                Ok(Token::Punct(Punct::RParen)),
-                Ok(Token::Punct(Punct::Comma)),
-                Ok(Token::Id(Identifier::new("c").unwrap())),
-                Ok(Token::Punct(Punct::LParen)),
-                Ok(Token::Punct(Punct::RParen)),
-                Ok(Token::Punct(Punct::RParen)),
-                Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
-                Ok(Token::End),
-            ],
-        );
-    }
-
-    #[test]
-    fn test_multiline_arguments() {
-        check_scan(
-            r#"define !macro1(
-  a(), b(
-  ),
-  c()
-)
-!enddefine.
-"#,
-            Syntax::Auto,
-            &[
-                Ok(Token::Id(Identifier::new("define").unwrap())),
-                Ok(Token::String(String::from("!macro1"))),
-                Ok(Token::Punct(Punct::LParen)),
-                Ok(Token::Id(Identifier::new("a").unwrap())),
-                Ok(Token::Punct(Punct::LParen)),
-                Ok(Token::Punct(Punct::RParen)),
-                Ok(Token::Punct(Punct::Comma)),
-                Ok(Token::Id(Identifier::new("b").unwrap())),
-                Ok(Token::Punct(Punct::LParen)),
-                Ok(Token::Punct(Punct::RParen)),
-                Ok(Token::Punct(Punct::Comma)),
-                Ok(Token::Id(Identifier::new("c").unwrap())),
-                Ok(Token::Punct(Punct::LParen)),
-                Ok(Token::Punct(Punct::RParen)),
-                Ok(Token::Punct(Punct::RParen)),
-                Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
-                Ok(Token::End),
-            ],
-        );
-    }
-
-    #[test]
-    fn test_arguments_start_on_second_line() {
-        check_scan(
-            r#"define !macro1
-(x,y,z
-)
-content 1
-content 2
-!enddefine.
-"#,
-            Syntax::Auto,
-            &[
-                Ok(Token::Id(Identifier::new("define").unwrap())),
-                Ok(Token::String(String::from("!macro1"))),
-                Ok(Token::Punct(Punct::LParen)),
-                Ok(Token::Id(Identifier::new("x").unwrap())),
-                Ok(Token::Punct(Punct::Comma)),
-                Ok(Token::Id(Identifier::new("y").unwrap())),
-                Ok(Token::Punct(Punct::Comma)),
-                Ok(Token::Id(Identifier::new("z").unwrap())),
-                Ok(Token::Punct(Punct::RParen)),
-                Ok(Token::String(String::from("content 1"))),
-                Ok(Token::String(String::from("content 2"))),
-                Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
-                Ok(Token::End),
-            ],
-        );
-    }
-
-    #[test]
-    fn test_early_end_of_command_1() {
-        check_scan(
-            r#"define !macro1.
-data list /x 1.
-"#,
-            Syntax::Auto,
-            &[
-                Ok(Token::Id(Identifier::new("define").unwrap())),
-                Ok(Token::String(String::from("!macro1"))),
-                Ok(Token::End),
-                Ok(Token::Id(Identifier::new("data").unwrap())),
-                Ok(Token::Id(Identifier::new("list").unwrap())),
-                Ok(Token::Punct(Punct::Slash)),
-                Ok(Token::Id(Identifier::new("x").unwrap())),
-                Ok(Token::Number(1.0)),
-                Ok(Token::End),
-            ],
-        );
-    }
-
-    #[test]
-    fn test_early_end_of_command_2() {
-        check_scan(
-            r#"define !macro1
-x.
-data list /x 1.
-"#,
-            Syntax::Auto,
-            &[
-                Ok(Token::Id(Identifier::new("define").unwrap())),
-                Ok(Token::String(String::from("!macro1"))),
-                Ok(Token::Id(Identifier::new("x").unwrap())),
-                Ok(Token::End),
-                Ok(Token::Id(Identifier::new("data").unwrap())),
-                Ok(Token::Id(Identifier::new("list").unwrap())),
-                Ok(Token::Punct(Punct::Slash)),
-                Ok(Token::Id(Identifier::new("x").unwrap())),
-                Ok(Token::Number(1.0)),
-                Ok(Token::End),
-            ],
-        );
-    }
-
-    #[test]
-    fn test_early_end_of_command_3() {
-        check_scan(
-            r#"define !macro1(.
-x.
-data list /x 1.
-"#,
-            Syntax::Auto,
-            &[
-                Ok(Token::Id(Identifier::new("define").unwrap())),
-                Ok(Token::String(String::from("!macro1"))),
-                Ok(Token::Punct(Punct::LParen)),
-                Ok(Token::End),
-                Ok(Token::Id(Identifier::new("x").unwrap())),
-                Ok(Token::End),
-                Ok(Token::Id(Identifier::new("data").unwrap())),
-                Ok(Token::Id(Identifier::new("list").unwrap())),
-                Ok(Token::Punct(Punct::Slash)),
-                Ok(Token::Id(Identifier::new("x").unwrap())),
-                Ok(Token::Number(1.0)),
-                Ok(Token::End),
-            ],
-        );
-    }
-
-    #[test]
-    fn test_early_end_of_command_4() {
-        // Notice the command terminator at the end of the DEFINE command,
-        // which should not be there and ends it early.
-        check_scan(
-            r#"define !macro1.
-data list /x 1.
-"#,
-            Syntax::Auto,
-            &[
-                Ok(Token::Id(Identifier::new("define").unwrap())),
-                Ok(Token::String(String::from("!macro1"))),
-                Ok(Token::End),
-                Ok(Token::Id(Identifier::new("data").unwrap())),
-                Ok(Token::Id(Identifier::new("list").unwrap())),
-                Ok(Token::Punct(Punct::Slash)),
-                Ok(Token::Id(Identifier::new("x").unwrap())),
-                Ok(Token::Number(1.0)),
-                Ok(Token::End),
-            ],
-        );
-    }
-
-    #[test]
-    fn test_missing_enddefine() {
-        check_scan(
-            r#"define !macro1()
-content line 1
-content line 2
-"#,
-            Syntax::Auto,
-            &[
-                Ok(Token::Id(Identifier::new("define").unwrap())),
-                Ok(Token::String(String::from("!macro1"))),
-                Ok(Token::Punct(Punct::LParen)),
-                Ok(Token::Punct(Punct::RParen)),
-                Ok(Token::String(String::from("content line 1"))),
-                Ok(Token::String(String::from("content line 2"))),
-            ],
-        );
-    }
-}
diff --git a/rust/pspp/src/lex/scan/tests.rs b/rust/pspp/src/lex/scan/tests.rs
new file mode 100644 (file)
index 0000000..6f0e582
--- /dev/null
@@ -0,0 +1,1036 @@
+// PSPP - a program for statistical analysis.
+// Copyright (C) 2025 Free Software Foundation, Inc.
+//
+// This program is free software: you can redistribute it and/or modify it under
+// the terms of the GNU General Public License as published by the Free Software
+// Foundation, either version 3 of the License, or (at your option) any later
+// version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+// FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+// details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program.  If not, see <http://www.gnu.org/licenses/>.
+
+use crate::{
+    identifier::Identifier,
+    lex::{
+        segment::Syntax,
+        token::{Punct, Token},
+    },
+};
+
+use super::{ScanError, StringScanner};
+
+fn print_token(token: &Token) {
+    match token {
+        Token::Id(s) => print!("Token::Id(String::from({s:?}))"),
+        Token::Number(number) => print!("Token::Number({number:?})"),
+        Token::String(s) => print!("Token::String(String::from({s:?}))"),
+        Token::End => print!("Token::EndCommand"),
+        Token::Punct(punct) => print!("Token::Punct(Punct::{punct:?})"),
+    }
+}
+
+#[track_caller]
+fn check_scan(input: &str, mode: Syntax, expected: &[Result<Token, ScanError>]) {
+    let tokens = StringScanner::new(input, mode, false).collect::<Vec<_>>();
+
+    if tokens != expected {
+        for token in &tokens {
+            match token {
+                Ok(token) => {
+                    print!("Ok(");
+                    print_token(token);
+                    print!(")");
+                }
+                Err(error) => print!("Err(ScanError::{error:?})"),
+            }
+            println!(",");
+        }
+
+        eprintln!("tokens differ from expected:");
+        let difference = diff::slice(expected, &tokens);
+        for result in difference {
+            match result {
+                diff::Result::Left(left) => eprintln!("-{left:?}"),
+                diff::Result::Both(left, _right) => eprintln!(" {left:?}"),
+                diff::Result::Right(right) => eprintln!("+{right:?}"),
+            }
+        }
+        panic!();
+    }
+}
+
+#[test]
+fn test_identifiers() {
+    check_scan(
+        r#"a aB i5 $x @efg @@. !abcd !* !*a #.# .x _z.
+abcd. abcd.
+QRSTUV./* end of line comment */
+QrStUv./* end of line comment */ 
+WXYZ. /* unterminated end of line comment
+�. /* U+FFFD is not valid in an identifier
+"#,
+        Syntax::Auto,
+        &[
+            Ok(Token::Id(Identifier::new("a").unwrap())),
+            Ok(Token::Id(Identifier::new("aB").unwrap())),
+            Ok(Token::Id(Identifier::new("i5").unwrap())),
+            Ok(Token::Id(Identifier::new("$x").unwrap())),
+            Ok(Token::Id(Identifier::new("@efg").unwrap())),
+            Ok(Token::Id(Identifier::new("@@.").unwrap())),
+            Ok(Token::Id(Identifier::new("!abcd").unwrap())),
+            Ok(Token::Punct(Punct::BangAsterisk)),
+            Ok(Token::Punct(Punct::BangAsterisk)),
+            Ok(Token::Id(Identifier::new("a").unwrap())),
+            Ok(Token::Id(Identifier::new("#.#").unwrap())),
+            Ok(Token::Punct(Punct::Dot)),
+            Ok(Token::Id(Identifier::new("x").unwrap())),
+            Ok(Token::Punct(Punct::Underscore)),
+            Ok(Token::Id(Identifier::new("z").unwrap())),
+            Ok(Token::End),
+            Ok(Token::Id(Identifier::new("abcd.").unwrap())),
+            Ok(Token::Id(Identifier::new("abcd").unwrap())),
+            Ok(Token::End),
+            Ok(Token::Id(Identifier::new("QRSTUV").unwrap())),
+            Ok(Token::End),
+            Ok(Token::Id(Identifier::new("QrStUv").unwrap())),
+            Ok(Token::End),
+            Ok(Token::Id(Identifier::new("WXYZ").unwrap())),
+            Ok(Token::End),
+            Err(ScanError::UnexpectedChar('�')),
+            Ok(Token::End),
+        ],
+    );
+}
+
+#[test]
+fn test_reserved_words() {
+    check_scan(
+        r#"and or not eq ge gt le lt ne all by to with
+AND OR NOT EQ GE GT LE LT NE ALL BY TO WITH
+andx orx notx eqx gex gtx lex ltx nex allx byx tox withx
+and. with.
+"#,
+        Syntax::Auto,
+        &[
+            Ok(Token::Punct(Punct::And)),
+            Ok(Token::Punct(Punct::Or)),
+            Ok(Token::Punct(Punct::Not)),
+            Ok(Token::Punct(Punct::Eq)),
+            Ok(Token::Punct(Punct::Ge)),
+            Ok(Token::Punct(Punct::Gt)),
+            Ok(Token::Punct(Punct::Le)),
+            Ok(Token::Punct(Punct::Lt)),
+            Ok(Token::Punct(Punct::Ne)),
+            Ok(Token::Punct(Punct::All)),
+            Ok(Token::Punct(Punct::By)),
+            Ok(Token::Punct(Punct::To)),
+            Ok(Token::Punct(Punct::With)),
+            Ok(Token::Punct(Punct::And)),
+            Ok(Token::Punct(Punct::Or)),
+            Ok(Token::Punct(Punct::Not)),
+            Ok(Token::Punct(Punct::Eq)),
+            Ok(Token::Punct(Punct::Ge)),
+            Ok(Token::Punct(Punct::Gt)),
+            Ok(Token::Punct(Punct::Le)),
+            Ok(Token::Punct(Punct::Lt)),
+            Ok(Token::Punct(Punct::Ne)),
+            Ok(Token::Punct(Punct::All)),
+            Ok(Token::Punct(Punct::By)),
+            Ok(Token::Punct(Punct::To)),
+            Ok(Token::Punct(Punct::With)),
+            Ok(Token::Id(Identifier::new("andx").unwrap())),
+            Ok(Token::Id(Identifier::new("orx").unwrap())),
+            Ok(Token::Id(Identifier::new("notx").unwrap())),
+            Ok(Token::Id(Identifier::new("eqx").unwrap())),
+            Ok(Token::Id(Identifier::new("gex").unwrap())),
+            Ok(Token::Id(Identifier::new("gtx").unwrap())),
+            Ok(Token::Id(Identifier::new("lex").unwrap())),
+            Ok(Token::Id(Identifier::new("ltx").unwrap())),
+            Ok(Token::Id(Identifier::new("nex").unwrap())),
+            Ok(Token::Id(Identifier::new("allx").unwrap())),
+            Ok(Token::Id(Identifier::new("byx").unwrap())),
+            Ok(Token::Id(Identifier::new("tox").unwrap())),
+            Ok(Token::Id(Identifier::new("withx").unwrap())),
+            Ok(Token::Id(Identifier::new("and.").unwrap())),
+            Ok(Token::Punct(Punct::With)),
+            Ok(Token::End),
+        ],
+    );
+}
+
+#[test]
+fn test_punctuation() {
+    check_scan(
+        r#"~ & | = >= > <= < ~= <> ( ) , - + * / [ ] **
+~&|=>=><=<~=<>(),-+*/[]**
+% : ; ? _ ` { } ~
+"#,
+        Syntax::Auto,
+        &[
+            Ok(Token::Punct(Punct::Not)),
+            Ok(Token::Punct(Punct::And)),
+            Ok(Token::Punct(Punct::Or)),
+            Ok(Token::Punct(Punct::Equals)),
+            Ok(Token::Punct(Punct::Ge)),
+            Ok(Token::Punct(Punct::Gt)),
+            Ok(Token::Punct(Punct::Le)),
+            Ok(Token::Punct(Punct::Lt)),
+            Ok(Token::Punct(Punct::Ne)),
+            Ok(Token::Punct(Punct::Ne)),
+            Ok(Token::Punct(Punct::LParen)),
+            Ok(Token::Punct(Punct::RParen)),
+            Ok(Token::Punct(Punct::Comma)),
+            Ok(Token::Punct(Punct::Dash)),
+            Ok(Token::Punct(Punct::Plus)),
+            Ok(Token::Punct(Punct::Asterisk)),
+            Ok(Token::Punct(Punct::Slash)),
+            Ok(Token::Punct(Punct::LSquare)),
+            Ok(Token::Punct(Punct::RSquare)),
+            Ok(Token::Punct(Punct::Exp)),
+            Ok(Token::Punct(Punct::Not)),
+            Ok(Token::Punct(Punct::And)),
+            Ok(Token::Punct(Punct::Or)),
+            Ok(Token::Punct(Punct::Equals)),
+            Ok(Token::Punct(Punct::Ge)),
+            Ok(Token::Punct(Punct::Gt)),
+            Ok(Token::Punct(Punct::Le)),
+            Ok(Token::Punct(Punct::Lt)),
+            Ok(Token::Punct(Punct::Ne)),
+            Ok(Token::Punct(Punct::Ne)),
+            Ok(Token::Punct(Punct::LParen)),
+            Ok(Token::Punct(Punct::RParen)),
+            Ok(Token::Punct(Punct::Comma)),
+            Ok(Token::Punct(Punct::Dash)),
+            Ok(Token::Punct(Punct::Plus)),
+            Ok(Token::Punct(Punct::Asterisk)),
+            Ok(Token::Punct(Punct::Slash)),
+            Ok(Token::Punct(Punct::LSquare)),
+            Ok(Token::Punct(Punct::RSquare)),
+            Ok(Token::Punct(Punct::Exp)),
+            Ok(Token::Punct(Punct::Percent)),
+            Ok(Token::Punct(Punct::Colon)),
+            Ok(Token::Punct(Punct::Semicolon)),
+            Ok(Token::Punct(Punct::Question)),
+            Ok(Token::Punct(Punct::Underscore)),
+            Ok(Token::Punct(Punct::Backtick)),
+            Ok(Token::Punct(Punct::LCurly)),
+            Ok(Token::Punct(Punct::RCurly)),
+            Ok(Token::Punct(Punct::Not)),
+        ],
+    );
+}
+
+#[test]
+fn test_positive_numbers() {
+    check_scan(
+        r#"0 1 01 001. 1.
+123. /* comment 1 */ /* comment 2 */
+.1 0.1 00.1 00.10
+5e1 6E-1 7e+1 6E+01 6e-03
+.3E1 .4e-1 .5E+1 .6e+01 .7E-03
+1.23e1 45.6E-1 78.9e+1 99.9E+01 11.2e-03
+. 1e e1 1e+ 1e-
+"#,
+        Syntax::Auto,
+        &[
+            Ok(Token::Number(0.0)),
+            Ok(Token::Number(1.0)),
+            Ok(Token::Number(1.0)),
+            Ok(Token::Number(1.0)),
+            Ok(Token::Number(1.0)),
+            Ok(Token::End),
+            Ok(Token::Number(123.0)),
+            Ok(Token::End),
+            Ok(Token::End),
+            Ok(Token::Number(1.0)),
+            Ok(Token::Number(0.1)),
+            Ok(Token::Number(0.1)),
+            Ok(Token::Number(0.1)),
+            Ok(Token::Number(50.0)),
+            Ok(Token::Number(0.6)),
+            Ok(Token::Number(70.0)),
+            Ok(Token::Number(60.0)),
+            Ok(Token::Number(0.006)),
+            Ok(Token::End),
+            Ok(Token::Number(30.0)),
+            Ok(Token::Number(0.04)),
+            Ok(Token::Number(5.0)),
+            Ok(Token::Number(6.0)),
+            Ok(Token::Number(0.0007)),
+            Ok(Token::Number(12.3)),
+            Ok(Token::Number(4.56)),
+            Ok(Token::Number(789.0)),
+            Ok(Token::Number(999.0)),
+            Ok(Token::Number(0.0112)),
+            Ok(Token::End),
+            Err(ScanError::ExpectedExponent(String::from("1e"))),
+            Ok(Token::Id(Identifier::new("e1").unwrap())),
+            Err(ScanError::ExpectedExponent(String::from("1e+"))),
+            Err(ScanError::ExpectedExponent(String::from("1e-"))),
+        ],
+    );
+}
+
+#[test]
+fn test_negative_numbers() {
+    check_scan(
+        r#" -0 -1 -01 -001. -1.
+ -123. /* comment 1 */ /* comment 2 */
+ -.1 -0.1 -00.1 -00.10
+ -5e1 -6E-1 -7e+1 -6E+01 -6e-03
+ -.3E1 -.4e-1 -.5E+1 -.6e+01 -.7E-03
+ -1.23e1 -45.6E-1 -78.9e+1 -99.9E+01 -11.2e-03
+ -/**/1
+ -. -1e -e1 -1e+ -1e- -1.
+"#,
+        Syntax::Auto,
+        &[
+            Ok(Token::Number(-0.0)),
+            Ok(Token::Number(-1.0)),
+            Ok(Token::Number(-1.0)),
+            Ok(Token::Number(-1.0)),
+            Ok(Token::Number(-1.0)),
+            Ok(Token::End),
+            Ok(Token::Number(-123.0)),
+            Ok(Token::End),
+            Ok(Token::Number(-0.1)),
+            Ok(Token::Number(-0.1)),
+            Ok(Token::Number(-0.1)),
+            Ok(Token::Number(-0.1)),
+            Ok(Token::Number(-50.0)),
+            Ok(Token::Number(-0.6)),
+            Ok(Token::Number(-70.0)),
+            Ok(Token::Number(-60.0)),
+            Ok(Token::Number(-0.006)),
+            Ok(Token::Number(-3.0)),
+            Ok(Token::Number(-0.04)),
+            Ok(Token::Number(-5.0)),
+            Ok(Token::Number(-6.0)),
+            Ok(Token::Number(-0.0007)),
+            Ok(Token::Number(-12.3)),
+            Ok(Token::Number(-4.56)),
+            Ok(Token::Number(-789.0)),
+            Ok(Token::Number(-999.0)),
+            Ok(Token::Number(-0.0112)),
+            Ok(Token::Number(-1.0)),
+            Ok(Token::Punct(Punct::Dash)),
+            Ok(Token::Punct(Punct::Dot)),
+            Err(ScanError::ExpectedExponent(String::from("-1e"))),
+            Ok(Token::Punct(Punct::Dash)),
+            Ok(Token::Id(Identifier::new("e1").unwrap())),
+            Err(ScanError::ExpectedExponent(String::from("-1e+"))),
+            Err(ScanError::ExpectedExponent(String::from("-1e-"))),
+            Ok(Token::Number(-1.0)),
+            Ok(Token::End),
+        ],
+    );
+}
+
+#[test]
+fn test_strings() {
+    check_scan(
+        r#"'x' "y" 'abc'
+'Don''t' "Can't" 'Won''t'
+"""quoted""" '"quoted"'
+'' "" '''' """"
+'missing end quote
+"missing double quote
+'x' + "y"
++ 'z' +
+'a' /* abc */ + "b" /*
++ 'c' +/* */"d"/* */+'e'
+'foo'
++          /* special case: + in column 0 would ordinarily start a new command
+'bar'
+'foo'
+ +
+'bar'
+'foo'
++
+
+'bar'
+
++
+x"4142"+'5152'
+"4142"+
+x'5152'
+x"4142"
++u'304a'
+"�あいうえお"
+"abc"+U"FFFD"+u'3048'+"xyz"
+"#,
+        Syntax::Auto,
+        &[
+            Ok(Token::String(String::from("x"))),
+            Ok(Token::String(String::from("y"))),
+            Ok(Token::String(String::from("abc"))),
+            Ok(Token::String(String::from("Don't"))),
+            Ok(Token::String(String::from("Can't"))),
+            Ok(Token::String(String::from("Won't"))),
+            Ok(Token::String(String::from("\"quoted\""))),
+            Ok(Token::String(String::from("\"quoted\""))),
+            Ok(Token::String(String::from(""))),
+            Ok(Token::String(String::from(""))),
+            Ok(Token::String(String::from("'"))),
+            Ok(Token::String(String::from("\""))),
+            Err(ScanError::ExpectedQuote),
+            Err(ScanError::ExpectedQuote),
+            Ok(Token::String(String::from("xyzabcde"))),
+            Ok(Token::String(String::from("foobar"))),
+            Ok(Token::String(String::from("foobar"))),
+            Ok(Token::String(String::from("foo"))),
+            Ok(Token::Punct(Punct::Plus)),
+            Ok(Token::End),
+            Ok(Token::String(String::from("bar"))),
+            Ok(Token::End),
+            Ok(Token::Punct(Punct::Plus)),
+            Ok(Token::String(String::from("AB5152"))),
+            Ok(Token::String(String::from("4142QR"))),
+            Ok(Token::String(String::from("ABお"))),
+            Ok(Token::String(String::from("�あいうえお"))),
+            Ok(Token::String(String::from("abc�えxyz"))),
+        ],
+    );
+}
+
+#[test]
+fn test_shbang() {
+    check_scan(
+        r#"#! /usr/bin/pspp
+#! /usr/bin/pspp
+"#,
+        Syntax::Auto,
+        &[
+            Ok(Token::Id(Identifier::new("#").unwrap())),
+            Ok(Token::Punct(Punct::Bang)),
+            Ok(Token::Punct(Punct::Slash)),
+            Ok(Token::Id(Identifier::new("usr").unwrap())),
+            Ok(Token::Punct(Punct::Slash)),
+            Ok(Token::Id(Identifier::new("bin").unwrap())),
+            Ok(Token::Punct(Punct::Slash)),
+            Ok(Token::Id(Identifier::new("pspp").unwrap())),
+        ],
+    );
+}
+
+#[test]
+fn test_comments() {
+    check_scan(
+        r#"* Comment commands "don't
+have to contain valid tokens.
+
+** Check ambiguity with ** token.
+****************.
+
+comment keyword works too.
+COMM also.
+com is ambiguous with COMPUTE.
+
+   * Comment need not start at left margin.
+
+* Comment ends with blank line
+
+next command.
+
+"#,
+        Syntax::Auto,
+        &[
+            Ok(Token::End),
+            Ok(Token::End),
+            Ok(Token::End),
+            Ok(Token::End),
+            Ok(Token::End),
+            Ok(Token::End),
+            Ok(Token::End),
+            Ok(Token::Id(Identifier::new("com").unwrap())),
+            Ok(Token::Id(Identifier::new("is").unwrap())),
+            Ok(Token::Id(Identifier::new("ambiguous").unwrap())),
+            Ok(Token::Punct(Punct::With)),
+            Ok(Token::Id(Identifier::new("COMPUTE").unwrap())),
+            Ok(Token::End),
+            Ok(Token::End),
+            Ok(Token::End),
+            Ok(Token::End),
+            Ok(Token::End),
+            Ok(Token::Id(Identifier::new("next").unwrap())),
+            Ok(Token::Id(Identifier::new("command").unwrap())),
+            Ok(Token::End),
+            Ok(Token::End),
+        ],
+    );
+}
+
+#[test]
+fn test_document() {
+    check_scan(
+        r#"DOCUMENT one line.
+DOC more
+    than
+        one
+            line.
+docu
+first.paragraph
+isn't parsed as tokens
+
+second paragraph.
+"#,
+        Syntax::Auto,
+        &[
+            Ok(Token::Id(Identifier::new("DOCUMENT").unwrap())),
+            Ok(Token::String(String::from("DOCUMENT one line."))),
+            Ok(Token::End),
+            Ok(Token::End),
+            Ok(Token::Id(Identifier::new("DOCUMENT").unwrap())),
+            Ok(Token::String(String::from("DOC more"))),
+            Ok(Token::String(String::from("    than"))),
+            Ok(Token::String(String::from("        one"))),
+            Ok(Token::String(String::from("            line."))),
+            Ok(Token::End),
+            Ok(Token::End),
+            Ok(Token::Id(Identifier::new("DOCUMENT").unwrap())),
+            Ok(Token::String(String::from("docu"))),
+            Ok(Token::String(String::from("first.paragraph"))),
+            Ok(Token::String(String::from("isn't parsed as tokens"))),
+            Ok(Token::String(String::from(""))),
+            Ok(Token::String(String::from("second paragraph."))),
+            Ok(Token::End),
+            Ok(Token::End),
+        ],
+    );
+}
+
+#[test]
+fn test_file_label() {
+    check_scan(
+        r#"FIL label isn't quoted.
+FILE
+  lab 'is quoted'.
+FILE /*
+/**/  lab not quoted here either
+
+"#,
+        Syntax::Auto,
+        &[
+            Ok(Token::Id(Identifier::new("FIL").unwrap())),
+            Ok(Token::Id(Identifier::new("label").unwrap())),
+            Ok(Token::String(String::from("isn't quoted"))),
+            Ok(Token::End),
+            Ok(Token::Id(Identifier::new("FILE").unwrap())),
+            Ok(Token::Id(Identifier::new("lab").unwrap())),
+            Ok(Token::String(String::from("is quoted"))),
+            Ok(Token::End),
+            Ok(Token::Id(Identifier::new("FILE").unwrap())),
+            Ok(Token::Id(Identifier::new("lab").unwrap())),
+            Ok(Token::String(String::from("not quoted here either"))),
+            Ok(Token::End),
+        ],
+    );
+}
+
+#[test]
+fn test_begin_data() {
+    check_scan(
+        r#"begin data.
+123
+xxx
+end data.
+
+BEG /**/ DAT /*
+5 6 7 /* x
+
+end  data
+end data
+.
+"#,
+        Syntax::Auto,
+        &[
+            Ok(Token::Id(Identifier::new("begin").unwrap())),
+            Ok(Token::Id(Identifier::new("data").unwrap())),
+            Ok(Token::End),
+            Ok(Token::String(String::from("123"))),
+            Ok(Token::String(String::from("xxx"))),
+            Ok(Token::Id(Identifier::new("end").unwrap())),
+            Ok(Token::Id(Identifier::new("data").unwrap())),
+            Ok(Token::End),
+            Ok(Token::End),
+            Ok(Token::Id(Identifier::new("BEG").unwrap())),
+            Ok(Token::Id(Identifier::new("DAT").unwrap())),
+            Ok(Token::String(String::from("5 6 7 /* x"))),
+            Ok(Token::String(String::from(""))),
+            Ok(Token::String(String::from("end  data"))),
+            Ok(Token::Id(Identifier::new("end").unwrap())),
+            Ok(Token::Id(Identifier::new("data").unwrap())),
+            Ok(Token::End),
+        ],
+    );
+}
+
+#[test]
+fn test_do_repeat() {
+    check_scan(
+        r#"do repeat x=a b c
+          y=d e f.
+  do repeat a=1 thru 5.
+another command.
+second command
++ third command.
+end /* x */ /* y */ repeat print.
+end
+ repeat.
+"#,
+        Syntax::Auto,
+        &[
+            Ok(Token::Id(Identifier::new("do").unwrap())),
+            Ok(Token::Id(Identifier::new("repeat").unwrap())),
+            Ok(Token::Id(Identifier::new("x").unwrap())),
+            Ok(Token::Punct(Punct::Equals)),
+            Ok(Token::Id(Identifier::new("a").unwrap())),
+            Ok(Token::Id(Identifier::new("b").unwrap())),
+            Ok(Token::Id(Identifier::new("c").unwrap())),
+            Ok(Token::Id(Identifier::new("y").unwrap())),
+            Ok(Token::Punct(Punct::Equals)),
+            Ok(Token::Id(Identifier::new("d").unwrap())),
+            Ok(Token::Id(Identifier::new("e").unwrap())),
+            Ok(Token::Id(Identifier::new("f").unwrap())),
+            Ok(Token::End),
+            Ok(Token::String(String::from("  do repeat a=1 thru 5."))),
+            Ok(Token::String(String::from("another command."))),
+            Ok(Token::String(String::from("second command"))),
+            Ok(Token::String(String::from("+ third command."))),
+            Ok(Token::String(String::from(
+                "end /* x */ /* y */ repeat print.",
+            ))),
+            Ok(Token::Id(Identifier::new("end").unwrap())),
+            Ok(Token::Id(Identifier::new("repeat").unwrap())),
+            Ok(Token::End),
+        ],
+    );
+}
+
+#[test]
+fn test_do_repeat_batch() {
+    check_scan(
+        r#"do repeat x=a b c
+          y=d e f
+do repeat a=1 thru 5
+another command
+second command
++ third command
+end /* x */ /* y */ repeat print
+end
+ repeat
+do
+  repeat #a=1
+
+  inner command
+end repeat
+"#,
+        Syntax::Batch,
+        &[
+            Ok(Token::Id(Identifier::new("do").unwrap())),
+            Ok(Token::Id(Identifier::new("repeat").unwrap())),
+            Ok(Token::Id(Identifier::new("x").unwrap())),
+            Ok(Token::Punct(Punct::Equals)),
+            Ok(Token::Id(Identifier::new("a").unwrap())),
+            Ok(Token::Id(Identifier::new("b").unwrap())),
+            Ok(Token::Id(Identifier::new("c").unwrap())),
+            Ok(Token::Id(Identifier::new("y").unwrap())),
+            Ok(Token::Punct(Punct::Equals)),
+            Ok(Token::Id(Identifier::new("d").unwrap())),
+            Ok(Token::Id(Identifier::new("e").unwrap())),
+            Ok(Token::Id(Identifier::new("f").unwrap())),
+            Ok(Token::End),
+            Ok(Token::String(String::from("do repeat a=1 thru 5"))),
+            Ok(Token::String(String::from("another command"))),
+            Ok(Token::String(String::from("second command"))),
+            Ok(Token::String(String::from("+ third command"))),
+            Ok(Token::String(String::from(
+                "end /* x */ /* y */ repeat print",
+            ))),
+            Ok(Token::Id(Identifier::new("end").unwrap())),
+            Ok(Token::Id(Identifier::new("repeat").unwrap())),
+            Ok(Token::End),
+            Ok(Token::Id(Identifier::new("do").unwrap())),
+            Ok(Token::Id(Identifier::new("repeat").unwrap())),
+            Ok(Token::Id(Identifier::new("#a").unwrap())),
+            Ok(Token::Punct(Punct::Equals)),
+            Ok(Token::Number(1.0)),
+            Ok(Token::End),
+            Ok(Token::String(String::from("  inner command"))),
+            Ok(Token::Id(Identifier::new("end").unwrap())),
+            Ok(Token::Id(Identifier::new("repeat").unwrap())),
+        ],
+    );
+}
+
+#[test]
+fn test_batch_mode() {
+    check_scan(
+        r#"first command
+     another line of first command
++  second command
+third command
+
+fourth command.
+   fifth command.
+"#,
+        Syntax::Batch,
+        &[
+            Ok(Token::Id(Identifier::new("first").unwrap())),
+            Ok(Token::Id(Identifier::new("command").unwrap())),
+            Ok(Token::Id(Identifier::new("another").unwrap())),
+            Ok(Token::Id(Identifier::new("line").unwrap())),
+            Ok(Token::Id(Identifier::new("of").unwrap())),
+            Ok(Token::Id(Identifier::new("first").unwrap())),
+            Ok(Token::Id(Identifier::new("command").unwrap())),
+            Ok(Token::End),
+            Ok(Token::Id(Identifier::new("second").unwrap())),
+            Ok(Token::Id(Identifier::new("command").unwrap())),
+            Ok(Token::End),
+            Ok(Token::Id(Identifier::new("third").unwrap())),
+            Ok(Token::Id(Identifier::new("command").unwrap())),
+            Ok(Token::End),
+            Ok(Token::Id(Identifier::new("fourth").unwrap())),
+            Ok(Token::Id(Identifier::new("command").unwrap())),
+            Ok(Token::End),
+            Ok(Token::Id(Identifier::new("fifth").unwrap())),
+            Ok(Token::Id(Identifier::new("command").unwrap())),
+            Ok(Token::End),
+        ],
+    );
+}
+
+mod define {
+    use crate::{
+        identifier::Identifier,
+        lex::{
+            segment::Syntax,
+            token::{Punct, Token},
+        },
+    };
+
+    use super::check_scan;
+
+    #[test]
+    fn test_simple() {
+        check_scan(
+            r#"define !macro1()
+var1 var2 var3
+!enddefine.
+"#,
+            Syntax::Auto,
+            &[
+                Ok(Token::Id(Identifier::new("define").unwrap())),
+                Ok(Token::String(String::from("!macro1"))),
+                Ok(Token::Punct(Punct::LParen)),
+                Ok(Token::Punct(Punct::RParen)),
+                Ok(Token::String(String::from("var1 var2 var3"))),
+                Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
+                Ok(Token::End),
+            ],
+        );
+    }
+
+    #[test]
+    fn test_no_newline_after_parentheses() {
+        check_scan(
+            r#"define !macro1() var1 var2 var3
+!enddefine.
+"#,
+            Syntax::Auto,
+            &[
+                Ok(Token::Id(Identifier::new("define").unwrap())),
+                Ok(Token::String(String::from("!macro1"))),
+                Ok(Token::Punct(Punct::LParen)),
+                Ok(Token::Punct(Punct::RParen)),
+                Ok(Token::String(String::from(" var1 var2 var3"))),
+                Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
+                Ok(Token::End),
+            ],
+        );
+    }
+
+    #[test]
+    fn test_no_newline_before_enddefine() {
+        check_scan(
+            r#"define !macro1()
+var1 var2 var3!enddefine.
+"#,
+            Syntax::Auto,
+            &[
+                Ok(Token::Id(Identifier::new("define").unwrap())),
+                Ok(Token::String(String::from("!macro1"))),
+                Ok(Token::Punct(Punct::LParen)),
+                Ok(Token::Punct(Punct::RParen)),
+                Ok(Token::String(String::from("var1 var2 var3"))),
+                Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
+                Ok(Token::End),
+            ],
+        );
+    }
+
+    #[test]
+    fn test_all_on_one_line() {
+        check_scan(
+            r#"define !macro1()var1 var2 var3!enddefine.
+"#,
+            Syntax::Auto,
+            &[
+                Ok(Token::Id(Identifier::new("define").unwrap())),
+                Ok(Token::String(String::from("!macro1"))),
+                Ok(Token::Punct(Punct::LParen)),
+                Ok(Token::Punct(Punct::RParen)),
+                Ok(Token::String(String::from("var1 var2 var3"))),
+                Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
+                Ok(Token::End),
+            ],
+        );
+    }
+
+    #[test]
+    fn test_empty() {
+        check_scan(
+            r#"define !macro1()
+!enddefine.
+"#,
+            Syntax::Auto,
+            &[
+                Ok(Token::Id(Identifier::new("define").unwrap())),
+                Ok(Token::String(String::from("!macro1"))),
+                Ok(Token::Punct(Punct::LParen)),
+                Ok(Token::Punct(Punct::RParen)),
+                Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
+                Ok(Token::End),
+            ],
+        );
+    }
+
+    #[test]
+    fn test_blank_lines() {
+        check_scan(
+            r#"define !macro1()
+
+
+!enddefine.
+"#,
+            Syntax::Auto,
+            &[
+                Ok(Token::Id(Identifier::new("define").unwrap())),
+                Ok(Token::String(String::from("!macro1"))),
+                Ok(Token::Punct(Punct::LParen)),
+                Ok(Token::Punct(Punct::RParen)),
+                Ok(Token::String(String::from(""))),
+                Ok(Token::String(String::from(""))),
+                Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
+                Ok(Token::End),
+            ],
+        );
+    }
+
+    #[test]
+    fn test_arguments() {
+        check_scan(
+            r#"define !macro1(a(), b(), c())
+!enddefine.
+"#,
+            Syntax::Auto,
+            &[
+                Ok(Token::Id(Identifier::new("define").unwrap())),
+                Ok(Token::String(String::from("!macro1"))),
+                Ok(Token::Punct(Punct::LParen)),
+                Ok(Token::Id(Identifier::new("a").unwrap())),
+                Ok(Token::Punct(Punct::LParen)),
+                Ok(Token::Punct(Punct::RParen)),
+                Ok(Token::Punct(Punct::Comma)),
+                Ok(Token::Id(Identifier::new("b").unwrap())),
+                Ok(Token::Punct(Punct::LParen)),
+                Ok(Token::Punct(Punct::RParen)),
+                Ok(Token::Punct(Punct::Comma)),
+                Ok(Token::Id(Identifier::new("c").unwrap())),
+                Ok(Token::Punct(Punct::LParen)),
+                Ok(Token::Punct(Punct::RParen)),
+                Ok(Token::Punct(Punct::RParen)),
+                Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
+                Ok(Token::End),
+            ],
+        );
+    }
+
+    #[test]
+    fn test_multiline_arguments() {
+        check_scan(
+            r#"define !macro1(
+  a(), b(
+  ),
+  c()
+)
+!enddefine.
+"#,
+            Syntax::Auto,
+            &[
+                Ok(Token::Id(Identifier::new("define").unwrap())),
+                Ok(Token::String(String::from("!macro1"))),
+                Ok(Token::Punct(Punct::LParen)),
+                Ok(Token::Id(Identifier::new("a").unwrap())),
+                Ok(Token::Punct(Punct::LParen)),
+                Ok(Token::Punct(Punct::RParen)),
+                Ok(Token::Punct(Punct::Comma)),
+                Ok(Token::Id(Identifier::new("b").unwrap())),
+                Ok(Token::Punct(Punct::LParen)),
+                Ok(Token::Punct(Punct::RParen)),
+                Ok(Token::Punct(Punct::Comma)),
+                Ok(Token::Id(Identifier::new("c").unwrap())),
+                Ok(Token::Punct(Punct::LParen)),
+                Ok(Token::Punct(Punct::RParen)),
+                Ok(Token::Punct(Punct::RParen)),
+                Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
+                Ok(Token::End),
+            ],
+        );
+    }
+
+    #[test]
+    fn test_arguments_start_on_second_line() {
+        check_scan(
+            r#"define !macro1
+(x,y,z
+)
+content 1
+content 2
+!enddefine.
+"#,
+            Syntax::Auto,
+            &[
+                Ok(Token::Id(Identifier::new("define").unwrap())),
+                Ok(Token::String(String::from("!macro1"))),
+                Ok(Token::Punct(Punct::LParen)),
+                Ok(Token::Id(Identifier::new("x").unwrap())),
+                Ok(Token::Punct(Punct::Comma)),
+                Ok(Token::Id(Identifier::new("y").unwrap())),
+                Ok(Token::Punct(Punct::Comma)),
+                Ok(Token::Id(Identifier::new("z").unwrap())),
+                Ok(Token::Punct(Punct::RParen)),
+                Ok(Token::String(String::from("content 1"))),
+                Ok(Token::String(String::from("content 2"))),
+                Ok(Token::Id(Identifier::new("!enddefine").unwrap())),
+                Ok(Token::End),
+            ],
+        );
+    }
+
+    #[test]
+    fn test_early_end_of_command_1() {
+        check_scan(
+            r#"define !macro1.
+data list /x 1.
+"#,
+            Syntax::Auto,
+            &[
+                Ok(Token::Id(Identifier::new("define").unwrap())),
+                Ok(Token::String(String::from("!macro1"))),
+                Ok(Token::End),
+                Ok(Token::Id(Identifier::new("data").unwrap())),
+                Ok(Token::Id(Identifier::new("list").unwrap())),
+                Ok(Token::Punct(Punct::Slash)),
+                Ok(Token::Id(Identifier::new("x").unwrap())),
+                Ok(Token::Number(1.0)),
+                Ok(Token::End),
+            ],
+        );
+    }
+
+    #[test]
+    fn test_early_end_of_command_2() {
+        check_scan(
+            r#"define !macro1
+x.
+data list /x 1.
+"#,
+            Syntax::Auto,
+            &[
+                Ok(Token::Id(Identifier::new("define").unwrap())),
+                Ok(Token::String(String::from("!macro1"))),
+                Ok(Token::Id(Identifier::new("x").unwrap())),
+                Ok(Token::End),
+                Ok(Token::Id(Identifier::new("data").unwrap())),
+                Ok(Token::Id(Identifier::new("list").unwrap())),
+                Ok(Token::Punct(Punct::Slash)),
+                Ok(Token::Id(Identifier::new("x").unwrap())),
+                Ok(Token::Number(1.0)),
+                Ok(Token::End),
+            ],
+        );
+    }
+
+    #[test]
+    fn test_early_end_of_command_3() {
+        check_scan(
+            r#"define !macro1(.
+x.
+data list /x 1.
+"#,
+            Syntax::Auto,
+            &[
+                Ok(Token::Id(Identifier::new("define").unwrap())),
+                Ok(Token::String(String::from("!macro1"))),
+                Ok(Token::Punct(Punct::LParen)),
+                Ok(Token::End),
+                Ok(Token::Id(Identifier::new("x").unwrap())),
+                Ok(Token::End),
+                Ok(Token::Id(Identifier::new("data").unwrap())),
+                Ok(Token::Id(Identifier::new("list").unwrap())),
+                Ok(Token::Punct(Punct::Slash)),
+                Ok(Token::Id(Identifier::new("x").unwrap())),
+                Ok(Token::Number(1.0)),
+                Ok(Token::End),
+            ],
+        );
+    }
+
+    #[test]
+    fn test_early_end_of_command_4() {
+        // Notice the command terminator at the end of the DEFINE command,
+        // which should not be there and ends it early.
+        check_scan(
+            r#"define !macro1.
+data list /x 1.
+"#,
+            Syntax::Auto,
+            &[
+                Ok(Token::Id(Identifier::new("define").unwrap())),
+                Ok(Token::String(String::from("!macro1"))),
+                Ok(Token::End),
+                Ok(Token::Id(Identifier::new("data").unwrap())),
+                Ok(Token::Id(Identifier::new("list").unwrap())),
+                Ok(Token::Punct(Punct::Slash)),
+                Ok(Token::Id(Identifier::new("x").unwrap())),
+                Ok(Token::Number(1.0)),
+                Ok(Token::End),
+            ],
+        );
+    }
+
+    #[test]
+    fn test_missing_enddefine() {
+        check_scan(
+            r#"define !macro1()
+content line 1
+content line 2
+"#,
+            Syntax::Auto,
+            &[
+                Ok(Token::Id(Identifier::new("define").unwrap())),
+                Ok(Token::String(String::from("!macro1"))),
+                Ok(Token::Punct(Punct::LParen)),
+                Ok(Token::Punct(Punct::RParen)),
+                Ok(Token::String(String::from("content line 1"))),
+                Ok(Token::String(String::from("content line 2"))),
+            ],
+        );
+    }
+}
index 5a568692b5030aa94f9e9cdf84101ba28963ac5a..27313a71840ae611da8b9c297775f0e8adc6478e 100644 (file)
@@ -1439,4 +1439,4 @@ fn strip_prefix_ignore_ascii_case<'a>(line: &'a str, pattern: &str) -> Option<&'
 }
 
 #[cfg(test)]
-mod test;
+mod tests;
diff --git a/rust/pspp/src/lex/segment/test.rs b/rust/pspp/src/lex/segment/test.rs
deleted file mode 100644 (file)
index 12b6591..0000000
+++ /dev/null
@@ -1,2148 +0,0 @@
-// PSPP - a program for statistical analysis.
-// Copyright (C) 2025 Free Software Foundation, Inc.
-//
-// This program is free software: you can redistribute it and/or modify it under
-// the terms of the GNU General Public License as published by the Free Software
-// Foundation, either version 3 of the License, or (at your option) any later
-// version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-// FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
-// details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program.  If not, see <http://www.gnu.org/licenses/>.
-
-use crate::prompt::PromptStyle;
-
-use super::{Segment, Segmenter, Syntax};
-
-fn push_segment(
-    segmenter: &mut Segmenter,
-    input: &str,
-    one_byte: bool,
-) -> Option<(usize, Segment)> {
-    if one_byte {
-        for len in input.char_indices().map(|(pos, _c)| pos) {
-            if let Ok(result) = segmenter.push(&input[..len], false) {
-                return result;
-            }
-        }
-    }
-    segmenter.push(input, true).unwrap()
-}
-
-fn _check_segmentation(
-    mut input: &str,
-    mode: Syntax,
-    expect_segments: &[(Segment, &str)],
-    expect_prompts: &[PromptStyle],
-    one_byte: bool,
-) {
-    let mut segments = Vec::with_capacity(expect_segments.len());
-    let mut prompts = Vec::new();
-    let mut segmenter = Segmenter::new(mode, false);
-    while let Some((seg_len, seg_type)) = push_segment(&mut segmenter, input, one_byte) {
-        let (token, rest) = input.split_at(seg_len);
-        segments.push((seg_type, token));
-        if let Segment::Newline = seg_type {
-            prompts.push(segmenter.prompt());
-        }
-        input = rest;
-    }
-
-    if segments != expect_segments {
-        eprintln!("segments differ from expected:");
-        let difference = diff::slice(expect_segments, &segments);
-        for result in difference {
-            match result {
-                diff::Result::Left(left) => eprintln!("-{left:?}"),
-                diff::Result::Both(left, _right) => eprintln!(" {left:?}"),
-                diff::Result::Right(right) => eprintln!("+{right:?}"),
-            }
-        }
-        panic!();
-    }
-
-    if prompts != expect_prompts {
-        eprintln!("prompts differ from expected:");
-        let difference = diff::slice(expect_prompts, &prompts);
-        for result in difference {
-            match result {
-                diff::Result::Left(left) => eprintln!("-{left:?}"),
-                diff::Result::Both(left, _right) => eprintln!(" {left:?}"),
-                diff::Result::Right(right) => eprintln!("+{right:?}"),
-            }
-        }
-        panic!();
-    }
-}
-
-fn check_segmentation(
-    input: &str,
-    mode: Syntax,
-    expect_segments: &[(Segment, &str)],
-    expect_prompts: &[PromptStyle],
-) {
-    for (one_byte, one_byte_name) in [(false, "full-string"), (true, "byte-by-byte")] {
-        println!("running {one_byte_name} segmentation test with LF newlines...");
-        _check_segmentation(input, mode, expect_segments, expect_prompts, one_byte);
-
-        println!("running {one_byte_name} segmentation test with CRLF newlines...");
-        _check_segmentation(
-            &input.replace('\n', "\r\n"),
-            mode,
-            &expect_segments
-                .iter()
-                .map(|(segment, s)| match *segment {
-                    Segment::Newline => (Segment::Newline, "\r\n"),
-                    _ => (*segment, *s),
-                })
-                .collect::<Vec<_>>(),
-            expect_prompts,
-            one_byte,
-        );
-
-        if let Some(input) = input.strip_suffix('\n') {
-            println!("running {one_byte_name} segmentation test without final newline...");
-            let mut expect_segments: Vec<_> = expect_segments.to_vec();
-            assert_eq!(expect_segments.pop(), Some((Segment::Newline, "\n")));
-            while let Some((Segment::SeparateCommands | Segment::EndCommand, "")) =
-                expect_segments.last()
-            {
-                expect_segments.pop();
-            }
-            _check_segmentation(
-                input,
-                mode,
-                &expect_segments,
-                &expect_prompts[..expect_prompts.len() - 1],
-                one_byte,
-            );
-        }
-    }
-}
-
-#[allow(dead_code)]
-fn print_segmentation(mut input: &str) {
-    let mut segmenter = Segmenter::new(Syntax::Interactive, false);
-    while let Some((seg_len, seg_type)) = segmenter.push(input, true).unwrap() {
-        let (token, rest) = input.split_at(seg_len);
-        print!("{seg_type:?} {token:?}");
-        if let Segment::Newline = seg_type {
-            print!(" ({:?})", segmenter.prompt())
-        }
-        println!();
-        input = rest;
-    }
-}
-
-#[test]
-fn test_identifiers() {
-    check_segmentation(
-        r#"a ab abc abcd !abcd
-A AB ABC ABCD !ABCD
-aB aBC aBcD !aBcD
-$x $y $z !$z
-grève Ângstrom poté
-#a #b #c ## #d !#d
-@efg @ @@. @#@ !@ 
-## # #12345 #.#
-f@#_.#6
-GhIjK
-.x 1y _z
-!abc abc!
-"#,
-        Syntax::Auto,
-        &[
-            (Segment::Identifier, "a"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "ab"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "abc"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "abcd"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "!abcd"),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "A"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "AB"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "ABC"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "ABCD"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "!ABCD"),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "aB"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "aBC"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "aBcD"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "!aBcD"),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "$x"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "$y"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "$z"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "!$z"),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "grève"),
-            (Segment::Spaces, "\u{00a0}"),
-            (Segment::Identifier, "Ângstrom"),
-            (Segment::Spaces, "\u{00a0}"),
-            (Segment::Identifier, "poté"),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "#a"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "#b"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "#c"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "##"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "#d"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "!#d"),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "@efg"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "@"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "@@."),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "@#@"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "!@"),
-            (Segment::Spaces, " "),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "##"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "#"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "#12345"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "#.#"),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "f@#_.#6"),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "GhIjK"),
-            (Segment::Newline, "\n"),
-            (Segment::StartCommand, "."),
-            (Segment::Identifier, "x"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "1"),
-            (Segment::Identifier, "y"),
-            (Segment::Spaces, " "),
-            (Segment::Punct, "_"),
-            (Segment::Identifier, "z"),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "!abc"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "abc"),
-            (Segment::Punct, "!"),
-            (Segment::Newline, "\n"),
-        ],
-        &[
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-        ],
-    );
-}
-
-#[test]
-fn test_identifiers_ending_in_dot() {
-    check_segmentation(
-        r#"abcd. abcd.
-ABCD. ABCD.
-aBcD. aBcD. 
-$y. $z. あいうえお.
-#c. #d..
-@@. @@....
-#.#.
-#abcd.
-.
-. 
-LMNOP. 
-QRSTUV./* end of line comment */
-qrstuv. /* end of line comment */
-QrStUv./* end of line comment */ 
-wxyz./* unterminated end of line comment
-WXYZ. /* unterminated end of line comment
-WxYz./* unterminated end of line comment 
-"#,
-        Syntax::Auto,
-        &[
-            (Segment::Identifier, "abcd."),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "abcd"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "ABCD."),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "ABCD"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "aBcD."),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "aBcD"),
-            (Segment::EndCommand, "."),
-            (Segment::Spaces, " "),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "$y."),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "$z."),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "あいうえお"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "#c."),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "#d."),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "@@."),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "@@..."),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "#.#"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "#abcd"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::StartCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::StartCommand, "."),
-            (Segment::Spaces, " "),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "LMNOP"),
-            (Segment::EndCommand, "."),
-            (Segment::Spaces, " "),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "QRSTUV"),
-            (Segment::EndCommand, "."),
-            (Segment::Comment, "/* end of line comment */"),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "qrstuv"),
-            (Segment::EndCommand, "."),
-            (Segment::Spaces, " "),
-            (Segment::Comment, "/* end of line comment */"),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "QrStUv"),
-            (Segment::EndCommand, "."),
-            (Segment::Comment, "/* end of line comment */"),
-            (Segment::Spaces, " "),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "wxyz"),
-            (Segment::EndCommand, "."),
-            (Segment::Comment, "/* unterminated end of line comment"),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "WXYZ"),
-            (Segment::EndCommand, "."),
-            (Segment::Spaces, " "),
-            (Segment::Comment, "/* unterminated end of line comment"),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "WxYz"),
-            (Segment::EndCommand, "."),
-            (Segment::Comment, "/* unterminated end of line comment "),
-            (Segment::Newline, "\n"),
-        ],
-        &[
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::First,
-        ],
-    );
-}
-
-#[test]
-fn test_reserved_words() {
-    check_segmentation(
-        r#"and or not eq ge gt le lt ne all by to with
-AND OR NOT EQ GE GT LE LT NE ALL BY TO WITH
-andx orx notx eqx gex gtx lex ltx nex allx byx tox withx
-and. with.
-"#,
-        Syntax::Auto,
-        &[
-            (Segment::Identifier, "and"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "or"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "not"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "eq"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "ge"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "gt"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "le"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "lt"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "ne"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "all"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "by"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "to"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "with"),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "AND"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "OR"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "NOT"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "EQ"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "GE"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "GT"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "LE"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "LT"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "NE"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "ALL"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "BY"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "TO"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "WITH"),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "andx"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "orx"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "notx"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "eqx"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "gex"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "gtx"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "lex"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "ltx"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "nex"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "allx"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "byx"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "tox"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "withx"),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "and."),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "with"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-        ],
-        &[
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::First,
-        ],
-    );
-}
-
-#[test]
-fn test_punctuation() {
-    check_segmentation(
-        r#"~ & | = >= > <= < ~= <> ( ) , - + * / [ ] **
-~&|=>=><=<~=<>(),-+*/[]**!*
-% : ; ? _ ` { } ~ !*
-"#,
-        Syntax::Auto,
-        &[
-            (Segment::Punct, "~"),
-            (Segment::Spaces, " "),
-            (Segment::Punct, "&"),
-            (Segment::Spaces, " "),
-            (Segment::Punct, "|"),
-            (Segment::Spaces, " "),
-            (Segment::Punct, "="),
-            (Segment::Spaces, " "),
-            (Segment::Punct, ">="),
-            (Segment::Spaces, " "),
-            (Segment::Punct, ">"),
-            (Segment::Spaces, " "),
-            (Segment::Punct, "<="),
-            (Segment::Spaces, " "),
-            (Segment::Punct, "<"),
-            (Segment::Spaces, " "),
-            (Segment::Punct, "~="),
-            (Segment::Spaces, " "),
-            (Segment::Punct, "<>"),
-            (Segment::Spaces, " "),
-            (Segment::Punct, "("),
-            (Segment::Spaces, " "),
-            (Segment::Punct, ")"),
-            (Segment::Spaces, " "),
-            (Segment::Punct, ","),
-            (Segment::Spaces, " "),
-            (Segment::Punct, "-"),
-            (Segment::Spaces, " "),
-            (Segment::Punct, "+"),
-            (Segment::Spaces, " "),
-            (Segment::Punct, "*"),
-            (Segment::Spaces, " "),
-            (Segment::Punct, "/"),
-            (Segment::Spaces, " "),
-            (Segment::Punct, "["),
-            (Segment::Spaces, " "),
-            (Segment::Punct, "]"),
-            (Segment::Spaces, " "),
-            (Segment::Punct, "**"),
-            (Segment::Newline, "\n"),
-            (Segment::Punct, "~"),
-            (Segment::Punct, "&"),
-            (Segment::Punct, "|"),
-            (Segment::Punct, "="),
-            (Segment::Punct, ">="),
-            (Segment::Punct, ">"),
-            (Segment::Punct, "<="),
-            (Segment::Punct, "<"),
-            (Segment::Punct, "~="),
-            (Segment::Punct, "<>"),
-            (Segment::Punct, "("),
-            (Segment::Punct, ")"),
-            (Segment::Punct, ","),
-            (Segment::Punct, "-"),
-            (Segment::Punct, "+"),
-            (Segment::Punct, "*"),
-            (Segment::Punct, "/"),
-            (Segment::Punct, "["),
-            (Segment::Punct, "]"),
-            (Segment::Punct, "**"),
-            (Segment::Punct, "!*"),
-            (Segment::Newline, "\n"),
-            (Segment::Punct, "%"),
-            (Segment::Spaces, " "),
-            (Segment::Punct, ":"),
-            (Segment::Spaces, " "),
-            (Segment::Punct, ";"),
-            (Segment::Spaces, " "),
-            (Segment::Punct, "?"),
-            (Segment::Spaces, " "),
-            (Segment::Punct, "_"),
-            (Segment::Spaces, " "),
-            (Segment::Punct, "`"),
-            (Segment::Spaces, " "),
-            (Segment::Punct, "{"),
-            (Segment::Spaces, " "),
-            (Segment::Punct, "}"),
-            (Segment::Spaces, " "),
-            (Segment::Punct, "~"),
-            (Segment::Spaces, " "),
-            (Segment::Punct, "!*"),
-            (Segment::Newline, "\n"),
-        ],
-        &[PromptStyle::Later, PromptStyle::Later, PromptStyle::Later],
-    );
-}
-
-#[test]
-fn test_positive_numbers() {
-    check_segmentation(
-        r#"0 1 01 001. 1.
-123. /* comment 1 */ /* comment 2 */
-.1 0.1 00.1 00.10
-5e1 6E-1 7e+1 6E+01 6e-03
-.3E1 .4e-1 .5E+1 .6e+01 .7E-03
-1.23e1 45.6E-1 78.9e+1 99.9E+01 11.2e-03
-. 1e e1 1e+ 1e- 1.
-"#,
-        Syntax::Auto,
-        &[
-            (Segment::Number, "0"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "1"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "01"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "001."),
-            (Segment::Spaces, " "),
-            (Segment::Number, "1"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::Number, "123"),
-            (Segment::EndCommand, "."),
-            (Segment::Spaces, " "),
-            (Segment::Comment, "/* comment 1 */"),
-            (Segment::Spaces, " "),
-            (Segment::Comment, "/* comment 2 */"),
-            (Segment::Newline, "\n"),
-            (Segment::StartCommand, "."),
-            (Segment::Number, "1"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "0.1"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "00.1"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "00.10"),
-            (Segment::Newline, "\n"),
-            (Segment::Number, "5e1"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "6E-1"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "7e+1"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "6E+01"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "6e-03"),
-            (Segment::Newline, "\n"),
-            (Segment::StartCommand, "."),
-            (Segment::Number, "3E1"),
-            (Segment::Spaces, " "),
-            (Segment::Number, ".4e-1"),
-            (Segment::Spaces, " "),
-            (Segment::Number, ".5E+1"),
-            (Segment::Spaces, " "),
-            (Segment::Number, ".6e+01"),
-            (Segment::Spaces, " "),
-            (Segment::Number, ".7E-03"),
-            (Segment::Newline, "\n"),
-            (Segment::Number, "1.23e1"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "45.6E-1"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "78.9e+1"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "99.9E+01"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "11.2e-03"),
-            (Segment::Newline, "\n"),
-            (Segment::StartCommand, "."),
-            (Segment::Spaces, " "),
-            (Segment::ExpectedExponent, "1e"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "e1"),
-            (Segment::Spaces, " "),
-            (Segment::ExpectedExponent, "1e+"),
-            (Segment::Spaces, " "),
-            (Segment::ExpectedExponent, "1e-"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "1"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-        ],
-        &[
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::First,
-        ],
-    );
-}
-
-#[test]
-fn test_negative_numbers() {
-    check_segmentation(
-        r#" -0 -1 -01 -001. -1.
- -123. /* comment 1 */ /* comment 2 */
- -.1 -0.1 -00.1 -00.10
- -5e1 -6E-1 -7e+1 -6E+01 -6e-03
- -.3E1 -.4e-1 -.5E+1 -.6e+01 -.7E-03
- -1.23e1 -45.6E-1 -78.9e+1 -99.9E+01 -11.2e-03
- -/**/1
- -. -1e -e1 -1e+ -1e- -1.
-"#,
-        Syntax::Auto,
-        &[
-            (Segment::Spaces, " "),
-            (Segment::Number, "-0"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "-1"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "-01"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "-001."),
-            (Segment::Spaces, " "),
-            (Segment::Number, "-1"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "-123"),
-            (Segment::EndCommand, "."),
-            (Segment::Spaces, " "),
-            (Segment::Comment, "/* comment 1 */"),
-            (Segment::Spaces, " "),
-            (Segment::Comment, "/* comment 2 */"),
-            (Segment::Newline, "\n"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "-.1"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "-0.1"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "-00.1"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "-00.10"),
-            (Segment::Newline, "\n"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "-5e1"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "-6E-1"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "-7e+1"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "-6E+01"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "-6e-03"),
-            (Segment::Newline, "\n"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "-.3E1"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "-.4e-1"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "-.5E+1"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "-.6e+01"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "-.7E-03"),
-            (Segment::Newline, "\n"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "-1.23e1"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "-45.6E-1"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "-78.9e+1"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "-99.9E+01"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "-11.2e-03"),
-            (Segment::Newline, "\n"),
-            (Segment::Spaces, " "),
-            (Segment::Punct, "-"),
-            (Segment::Comment, "/**/"),
-            (Segment::Number, "1"),
-            (Segment::Newline, "\n"),
-            (Segment::Spaces, " "),
-            (Segment::Punct, "-"),
-            (Segment::Punct, "."),
-            (Segment::Spaces, " "),
-            (Segment::ExpectedExponent, "-1e"),
-            (Segment::Spaces, " "),
-            (Segment::Punct, "-"),
-            (Segment::Identifier, "e1"),
-            (Segment::Spaces, " "),
-            (Segment::ExpectedExponent, "-1e+"),
-            (Segment::Spaces, " "),
-            (Segment::ExpectedExponent, "-1e-"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "-1"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-        ],
-        &[
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::First,
-        ],
-    );
-}
-
-#[test]
-fn test_strings() {
-    check_segmentation(
-        r#"'x' "y" 'abc'
-'Don''t' "Can't" 'Won''t'
-"""quoted""" '"quoted"'
-'' ""
-'missing end quote
-"missing double quote
-x"4142" X'5152'
-u'fffd' U"041"
-+ new command
-+ /* comment */ 'string continuation'
-+ /* also a punctuator on blank line
-- 'new command'
-"#,
-        Syntax::Auto,
-        &[
-            (Segment::QuotedString, "'x'"),
-            (Segment::Spaces, " "),
-            (Segment::QuotedString, "\"y\""),
-            (Segment::Spaces, " "),
-            (Segment::QuotedString, "'abc'"),
-            (Segment::Newline, "\n"),
-            (Segment::QuotedString, "'Don''t'"),
-            (Segment::Spaces, " "),
-            (Segment::QuotedString, "\"Can't\""),
-            (Segment::Spaces, " "),
-            (Segment::QuotedString, "'Won''t'"),
-            (Segment::Newline, "\n"),
-            (Segment::QuotedString, "\"\"\"quoted\"\"\""),
-            (Segment::Spaces, " "),
-            (Segment::QuotedString, "'\"quoted\"'"),
-            (Segment::Newline, "\n"),
-            (Segment::QuotedString, "''"),
-            (Segment::Spaces, " "),
-            (Segment::QuotedString, "\"\""),
-            (Segment::Newline, "\n"),
-            (Segment::ExpectedQuote, "'missing end quote"),
-            (Segment::Newline, "\n"),
-            (Segment::ExpectedQuote, "\"missing double quote"),
-            (Segment::Newline, "\n"),
-            (Segment::HexString, "x\"4142\""),
-            (Segment::Spaces, " "),
-            (Segment::HexString, "X'5152'"),
-            (Segment::Newline, "\n"),
-            (Segment::UnicodeString, "u'fffd'"),
-            (Segment::Spaces, " "),
-            (Segment::UnicodeString, "U\"041\""),
-            (Segment::Newline, "\n"),
-            (Segment::StartCommand, "+"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "new"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "command"),
-            (Segment::Newline, "\n"),
-            (Segment::Punct, "+"),
-            (Segment::Spaces, " "),
-            (Segment::Comment, "/* comment */"),
-            (Segment::Spaces, " "),
-            (Segment::QuotedString, "'string continuation'"),
-            (Segment::Newline, "\n"),
-            (Segment::Punct, "+"),
-            (Segment::Spaces, " "),
-            (Segment::Comment, "/* also a punctuator on blank line"),
-            (Segment::Newline, "\n"),
-            (Segment::StartCommand, "-"),
-            (Segment::Spaces, " "),
-            (Segment::QuotedString, "'new command'"),
-            (Segment::Newline, "\n"),
-        ],
-        &[
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-        ],
-    );
-}
-
-#[test]
-fn test_shbang() {
-    check_segmentation(
-        r#"#! /usr/bin/pspp
-title my title.
-#! /usr/bin/pspp
-"#,
-        Syntax::Interactive,
-        &[
-            (Segment::Shbang, "#! /usr/bin/pspp"),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "title"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "my"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "title"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "#"),
-            (Segment::Punct, "!"),
-            (Segment::Spaces, " "),
-            (Segment::Punct, "/"),
-            (Segment::Identifier, "usr"),
-            (Segment::Punct, "/"),
-            (Segment::Identifier, "bin"),
-            (Segment::Punct, "/"),
-            (Segment::Identifier, "pspp"),
-            (Segment::Newline, "\n"),
-        ],
-        &[PromptStyle::First, PromptStyle::First, PromptStyle::Later],
-    );
-}
-
-#[test]
-fn test_comment_command() {
-    check_segmentation(
-        r#"* Comment commands "don't
-have to contain valid tokens.
-
-** Check ambiguity with ** token.
-****************.
-
-comment keyword works too.
-COMM also.
-com is ambiguous with COMPUTE.
-
-   * Comment need not start at left margin.
-
-* Comment ends with blank line
-
-next command.
-
-"#,
-        Syntax::Interactive,
-        &[
-            (Segment::CommentCommand, "* Comment commands \"don't"),
-            (Segment::Newline, "\n"),
-            (Segment::CommentCommand, "have to contain valid tokens"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::SeparateCommands, ""),
-            (Segment::Newline, "\n"),
-            (Segment::CommentCommand, "** Check ambiguity with ** token"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::CommentCommand, "****************"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::SeparateCommands, ""),
-            (Segment::Newline, "\n"),
-            (Segment::CommentCommand, "comment keyword works too"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::CommentCommand, "COMM also"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "com"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "is"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "ambiguous"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "with"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "COMPUTE"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::SeparateCommands, ""),
-            (Segment::Newline, "\n"),
-            (Segment::Spaces, "   "),
-            (
-                Segment::CommentCommand,
-                "* Comment need not start at left margin",
-            ),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::SeparateCommands, ""),
-            (Segment::Newline, "\n"),
-            (Segment::CommentCommand, "* Comment ends with blank line"),
-            (Segment::Newline, "\n"),
-            (Segment::SeparateCommands, ""),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "next"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "command"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::SeparateCommands, ""),
-            (Segment::Newline, "\n"),
-        ],
-        &[
-            PromptStyle::Comment,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::Comment,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::First,
-        ],
-    );
-}
-
-#[test]
-fn test_document_command() {
-    check_segmentation(
-        r#"DOCUMENT one line.
-DOC more
-    than
-        one
-            line.
-docu
-first.paragraph
-isn't parsed as tokens
-
-second paragraph.
-"#,
-        Syntax::Interactive,
-        &[
-            (Segment::StartDocument, ""),
-            (Segment::Document, "DOCUMENT one line."),
-            (Segment::EndCommand, ""),
-            (Segment::SeparateCommands, ""),
-            (Segment::Newline, "\n"),
-            (Segment::StartDocument, ""),
-            (Segment::Document, "DOC more"),
-            (Segment::Newline, "\n"),
-            (Segment::Document, "    than"),
-            (Segment::Newline, "\n"),
-            (Segment::Document, "        one"),
-            (Segment::Newline, "\n"),
-            (Segment::Document, "            line."),
-            (Segment::EndCommand, ""),
-            (Segment::SeparateCommands, ""),
-            (Segment::Newline, "\n"),
-            (Segment::StartDocument, ""),
-            (Segment::Document, "docu"),
-            (Segment::Newline, "\n"),
-            (Segment::Document, "first.paragraph"),
-            (Segment::Newline, "\n"),
-            (Segment::Document, "isn't parsed as tokens"),
-            (Segment::Newline, "\n"),
-            (Segment::Document, ""),
-            (Segment::Newline, "\n"),
-            (Segment::Document, "second paragraph."),
-            (Segment::EndCommand, ""),
-            (Segment::SeparateCommands, ""),
-            (Segment::Newline, "\n"),
-        ],
-        &[
-            PromptStyle::First,
-            PromptStyle::Document,
-            PromptStyle::Document,
-            PromptStyle::Document,
-            PromptStyle::First,
-            PromptStyle::Document,
-            PromptStyle::Document,
-            PromptStyle::Document,
-            PromptStyle::Document,
-            PromptStyle::First,
-        ],
-    );
-}
-
-#[test]
-fn test_file_label_command() {
-    check_segmentation(
-        r#"FIL label isn't quoted.
-FILE
-  lab 'is quoted'.
-FILE /*
-/**/  lab not quoted here either
-
-"#,
-        Syntax::Interactive,
-        &[
-            (Segment::Identifier, "FIL"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "label"),
-            (Segment::Spaces, " "),
-            (Segment::UnquotedString, "isn't quoted"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "FILE"),
-            (Segment::Newline, "\n"),
-            (Segment::Spaces, "  "),
-            (Segment::Identifier, "lab"),
-            (Segment::Spaces, " "),
-            (Segment::QuotedString, "'is quoted'"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "FILE"),
-            (Segment::Spaces, " "),
-            (Segment::Comment, "/*"),
-            (Segment::Newline, "\n"),
-            (Segment::Comment, "/**/"),
-            (Segment::Spaces, "  "),
-            (Segment::Identifier, "lab"),
-            (Segment::Spaces, " "),
-            (Segment::UnquotedString, "not quoted here either"),
-            (Segment::Newline, "\n"),
-            (Segment::SeparateCommands, ""),
-            (Segment::Newline, "\n"),
-        ],
-        &[
-            PromptStyle::First,
-            PromptStyle::Later,
-            PromptStyle::First,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::First,
-        ],
-    );
-}
-
-#[test]
-fn test_begin_data() {
-    check_segmentation(
-        r#"begin data.
-end data.
-
-begin data. /*
-123
-xxx
-end data.
-
-BEG /**/ DAT /*
-5 6 7 /* x
-
-end  data
-end data
-.
-
-begin
- data.
-data
-end data.
-
-begin data "xxx".
-begin data 123.
-not data
-"#,
-        Syntax::Interactive,
-        &[
-            (Segment::Identifier, "begin"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "data"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "end"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "data"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::SeparateCommands, ""),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "begin"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "data"),
-            (Segment::EndCommand, "."),
-            (Segment::Spaces, " "),
-            (Segment::Comment, "/*"),
-            (Segment::Newline, "\n"),
-            (Segment::InlineData, "123"),
-            (Segment::Newline, "\n"),
-            (Segment::InlineData, "xxx"),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "end"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "data"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::SeparateCommands, ""),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "BEG"),
-            (Segment::Spaces, " "),
-            (Segment::Comment, "/**/"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "DAT"),
-            (Segment::Spaces, " "),
-            (Segment::Comment, "/*"),
-            (Segment::Newline, "\n"),
-            (Segment::InlineData, "5 6 7 /* x"),
-            (Segment::Newline, "\n"),
-            (Segment::InlineData, ""),
-            (Segment::Newline, "\n"),
-            (Segment::InlineData, "end  data"),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "end"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "data"),
-            (Segment::Newline, "\n"),
-            (Segment::StartCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::SeparateCommands, ""),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "begin"),
-            (Segment::Newline, "\n"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "data"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::InlineData, "data"),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "end"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "data"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::SeparateCommands, ""),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "begin"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "data"),
-            (Segment::Spaces, " "),
-            (Segment::QuotedString, "\"xxx\""),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "begin"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "data"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "123"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "not"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "data"),
-            (Segment::Newline, "\n"),
-        ],
-        &[
-            PromptStyle::Data,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::Data,
-            PromptStyle::Data,
-            PromptStyle::Data,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::Data,
-            PromptStyle::Data,
-            PromptStyle::Data,
-            PromptStyle::Data,
-            PromptStyle::Later,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::Later,
-            PromptStyle::Data,
-            PromptStyle::Data,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::Later,
-        ],
-    );
-}
-
-#[test]
-fn test_do_repeat() {
-    check_segmentation(
-        r#"do repeat x=a b c
-          y=d e f.
-  do repeat a=1 thru 5.
-another command.
-second command
-+ third command.
-end /* x */ /* y */ repeat print.
-end
- repeat.
-do
-  repeat #a=1.
-  inner command.
-end repeat.
-"#,
-        Syntax::Interactive,
-        &[
-            (Segment::Identifier, "do"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "repeat"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "x"),
-            (Segment::Punct, "="),
-            (Segment::Identifier, "a"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "b"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "c"),
-            (Segment::Newline, "\n"),
-            (Segment::Spaces, "          "),
-            (Segment::Identifier, "y"),
-            (Segment::Punct, "="),
-            (Segment::Identifier, "d"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "e"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "f"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::DoRepeatCommand, "  do repeat a=1 thru 5."),
-            (Segment::Newline, "\n"),
-            (Segment::DoRepeatCommand, "another command."),
-            (Segment::Newline, "\n"),
-            (Segment::DoRepeatCommand, "second command"),
-            (Segment::Newline, "\n"),
-            (Segment::DoRepeatCommand, "+ third command."),
-            (Segment::Newline, "\n"),
-            (
-                Segment::DoRepeatCommand,
-                "end /* x */ /* y */ repeat print.",
-            ),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "end"),
-            (Segment::Newline, "\n"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "repeat"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "do"),
-            (Segment::Newline, "\n"),
-            (Segment::Spaces, "  "),
-            (Segment::Identifier, "repeat"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "#a"),
-            (Segment::Punct, "="),
-            (Segment::Number, "1"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::DoRepeatCommand, "  inner command."),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "end"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "repeat"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-        ],
-        &[
-            PromptStyle::Later,
-            PromptStyle::DoRepeat,
-            PromptStyle::DoRepeat,
-            PromptStyle::DoRepeat,
-            PromptStyle::DoRepeat,
-            PromptStyle::DoRepeat,
-            PromptStyle::DoRepeat,
-            PromptStyle::Later,
-            PromptStyle::First,
-            PromptStyle::Later,
-            PromptStyle::DoRepeat,
-            PromptStyle::DoRepeat,
-            PromptStyle::First,
-        ],
-    );
-}
-
-#[test]
-fn test_do_repeat_overflow() {
-    const N: usize = 257;
-    let do_repeat: Vec<String> = (0..N)
-        .map(|i| format!("do repeat v{i}={i} thru {}.\n", i + 5))
-        .collect();
-    let end_repeat: Vec<String> = (0..N)
-        .rev()
-        .map(|i| format!("end repeat. /* {i}\n"))
-        .collect();
-
-    let s: String = do_repeat
-        .iter()
-        .chain(end_repeat.iter())
-        .map(|s| s.as_str())
-        .collect();
-    let mut expect_output = vec![
-        (Segment::Identifier, "do"),
-        (Segment::Spaces, " "),
-        (Segment::Identifier, "repeat"),
-        (Segment::Spaces, " "),
-        (Segment::Identifier, "v0"),
-        (Segment::Punct, "="),
-        (Segment::Number, "0"),
-        (Segment::Spaces, " "),
-        (Segment::Identifier, "thru"),
-        (Segment::Spaces, " "),
-        (Segment::Number, "5"),
-        (Segment::EndCommand, "."),
-        (Segment::Newline, "\n"),
-    ];
-    for (i, line) in do_repeat.iter().enumerate().take(N).skip(1) {
-        expect_output.push((Segment::DoRepeatCommand, line.trim_end()));
-        if i >= 255 {
-            expect_output.push((Segment::DoRepeatOverflow, ""));
-        }
-        expect_output.push((Segment::Newline, "\n"));
-    }
-    for line in &end_repeat[..254] {
-        expect_output.push((Segment::DoRepeatCommand, line.trim_end()));
-        expect_output.push((Segment::Newline, "\n"));
-    }
-    let comments: Vec<String> = (0..(N - 254)).rev().map(|i| format!("/* {i}")).collect();
-    for comment in &comments {
-        expect_output.extend([
-            (Segment::Identifier, "end"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "repeat"),
-            (Segment::EndCommand, "."),
-            (Segment::Spaces, " "),
-            (Segment::Comment, comment),
-            (Segment::Newline, "\n"),
-        ]);
-    }
-
-    let expect_prompts: Vec<_> = (0..N * 2 - 3)
-        .map(|_| PromptStyle::DoRepeat)
-        .chain([PromptStyle::First, PromptStyle::First, PromptStyle::First])
-        .collect();
-    check_segmentation(&s, Syntax::Interactive, &expect_output, &expect_prompts);
-}
-
-#[test]
-fn test_do_repeat_batch() {
-    check_segmentation(
-        r#"do repeat x=a b c
-          y=d e f
-do repeat a=1 thru 5
-another command
-second command
-+ third command
-end /* x */ /* y */ repeat print
-end
- repeat
-do
-  repeat #a=1
-
-  inner command
-end repeat
-"#,
-        Syntax::Batch,
-        &[
-            (Segment::Identifier, "do"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "repeat"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "x"),
-            (Segment::Punct, "="),
-            (Segment::Identifier, "a"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "b"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "c"),
-            (Segment::Newline, "\n"),
-            (Segment::Spaces, "          "),
-            (Segment::Identifier, "y"),
-            (Segment::Punct, "="),
-            (Segment::Identifier, "d"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "e"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "f"),
-            (Segment::Newline, "\n"),
-            (Segment::StartCommand, ""),
-            (Segment::DoRepeatCommand, "do repeat a=1 thru 5"),
-            (Segment::Newline, "\n"),
-            (Segment::DoRepeatCommand, "another command"),
-            (Segment::Newline, "\n"),
-            (Segment::DoRepeatCommand, "second command"),
-            (Segment::Newline, "\n"),
-            (Segment::DoRepeatCommand, "+ third command"),
-            (Segment::Newline, "\n"),
-            (Segment::DoRepeatCommand, "end /* x */ /* y */ repeat print"),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "end"),
-            (Segment::Newline, "\n"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "repeat"),
-            (Segment::Newline, "\n"),
-            (Segment::StartCommand, ""),
-            (Segment::Identifier, "do"),
-            (Segment::Newline, "\n"),
-            (Segment::Spaces, "  "),
-            (Segment::Identifier, "repeat"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "#a"),
-            (Segment::Punct, "="),
-            (Segment::Number, "1"),
-            (Segment::Newline, "\n"),
-            (Segment::SeparateCommands, ""),
-            (Segment::Newline, "\n"),
-            (Segment::DoRepeatCommand, "  inner command"),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "end"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "repeat"),
-            (Segment::Newline, "\n"),
-        ],
-        &[
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::DoRepeat,
-            PromptStyle::DoRepeat,
-            PromptStyle::DoRepeat,
-            PromptStyle::DoRepeat,
-            PromptStyle::DoRepeat,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::DoRepeat,
-            PromptStyle::DoRepeat,
-            PromptStyle::Later,
-        ],
-    );
-}
-
-mod define {
-    use crate::{
-        lex::segment::{Segment, Syntax},
-        prompt::PromptStyle,
-    };
-
-    use super::check_segmentation;
-
-    #[test]
-    fn test_simple() {
-        check_segmentation(
-            r#"define !macro1()
-var1 var2 var3 "!enddefine"
-!enddefine.
-"#,
-            Syntax::Interactive,
-            &[
-                (Segment::Identifier, "define"),
-                (Segment::Spaces, " "),
-                (Segment::MacroName, "!macro1"),
-                (Segment::Punct, "("),
-                (Segment::Punct, ")"),
-                (Segment::Newline, "\n"),
-                (Segment::MacroBody, "var1 var2 var3 \"!enddefine\""),
-                (Segment::Newline, "\n"),
-                (Segment::Identifier, "!enddefine"),
-                (Segment::EndCommand, "."),
-                (Segment::Newline, "\n"),
-            ],
-            &[PromptStyle::Define, PromptStyle::Define, PromptStyle::First],
-        );
-    }
-
-    #[test]
-    fn test_no_newline_after_parentheses() {
-        check_segmentation(
-            r#"define !macro1() var1 var2 var3 /* !enddefine
-!enddefine.
-"#,
-            Syntax::Interactive,
-            &[
-                (Segment::Identifier, "define"),
-                (Segment::Spaces, " "),
-                (Segment::MacroName, "!macro1"),
-                (Segment::Punct, "("),
-                (Segment::Punct, ")"),
-                (Segment::MacroBody, " var1 var2 var3 /* !enddefine"),
-                (Segment::Newline, "\n"),
-                (Segment::Identifier, "!enddefine"),
-                (Segment::EndCommand, "."),
-                (Segment::Newline, "\n"),
-            ],
-            &[PromptStyle::Define, PromptStyle::First],
-        );
-    }
-
-    #[test]
-    fn test_no_newline_before_enddefine() {
-        check_segmentation(
-            r#"define !macro1()
-var1 var2 var3!enddefine.
-"#,
-            Syntax::Interactive,
-            &[
-                (Segment::Identifier, "define"),
-                (Segment::Spaces, " "),
-                (Segment::MacroName, "!macro1"),
-                (Segment::Punct, "("),
-                (Segment::Punct, ")"),
-                (Segment::Newline, "\n"),
-                (Segment::MacroBody, "var1 var2 var3"),
-                (Segment::Identifier, "!enddefine"),
-                (Segment::EndCommand, "."),
-                (Segment::Newline, "\n"),
-            ],
-            &[PromptStyle::Define, PromptStyle::First],
-        );
-    }
-
-    #[test]
-    fn test_all_on_one_line() {
-        check_segmentation(
-            r#"define !macro1()var1 var2 var3!enddefine.
-"#,
-            Syntax::Interactive,
-            &[
-                (Segment::Identifier, "define"),
-                (Segment::Spaces, " "),
-                (Segment::MacroName, "!macro1"),
-                (Segment::Punct, "("),
-                (Segment::Punct, ")"),
-                (Segment::MacroBody, "var1 var2 var3"),
-                (Segment::Identifier, "!enddefine"),
-                (Segment::EndCommand, "."),
-                (Segment::Newline, "\n"),
-            ],
-            &[PromptStyle::First],
-        );
-    }
-
-    #[test]
-    fn test_empty() {
-        check_segmentation(
-            r#"define !macro1()
-!enddefine.
-"#,
-            Syntax::Interactive,
-            &[
-                (Segment::Identifier, "define"),
-                (Segment::Spaces, " "),
-                (Segment::MacroName, "!macro1"),
-                (Segment::Punct, "("),
-                (Segment::Punct, ")"),
-                (Segment::Newline, "\n"),
-                (Segment::Identifier, "!enddefine"),
-                (Segment::EndCommand, "."),
-                (Segment::Newline, "\n"),
-            ],
-            &[PromptStyle::Define, PromptStyle::First],
-        );
-    }
-
-    #[test]
-    fn test_blank_lines() {
-        check_segmentation(
-            r#"define !macro1()
-
-
-!enddefine.
-"#,
-            Syntax::Interactive,
-            &[
-                (Segment::Identifier, "define"),
-                (Segment::Spaces, " "),
-                (Segment::MacroName, "!macro1"),
-                (Segment::Punct, "("),
-                (Segment::Punct, ")"),
-                (Segment::Newline, "\n"),
-                (Segment::MacroBody, ""),
-                (Segment::Newline, "\n"),
-                (Segment::MacroBody, ""),
-                (Segment::Newline, "\n"),
-                (Segment::Identifier, "!enddefine"),
-                (Segment::EndCommand, "."),
-                (Segment::Newline, "\n"),
-            ],
-            &[
-                PromptStyle::Define,
-                PromptStyle::Define,
-                PromptStyle::Define,
-                PromptStyle::First,
-            ],
-        );
-    }
-
-    #[test]
-    fn test_arguments() {
-        check_segmentation(
-            r#"define !macro1(a(), b(), c())
-!enddefine.
-"#,
-            Syntax::Interactive,
-            &[
-                (Segment::Identifier, "define"),
-                (Segment::Spaces, " "),
-                (Segment::MacroName, "!macro1"),
-                (Segment::Punct, "("),
-                (Segment::Identifier, "a"),
-                (Segment::Punct, "("),
-                (Segment::Punct, ")"),
-                (Segment::Punct, ","),
-                (Segment::Spaces, " "),
-                (Segment::Identifier, "b"),
-                (Segment::Punct, "("),
-                (Segment::Punct, ")"),
-                (Segment::Punct, ","),
-                (Segment::Spaces, " "),
-                (Segment::Identifier, "c"),
-                (Segment::Punct, "("),
-                (Segment::Punct, ")"),
-                (Segment::Punct, ")"),
-                (Segment::Newline, "\n"),
-                (Segment::Identifier, "!enddefine"),
-                (Segment::EndCommand, "."),
-                (Segment::Newline, "\n"),
-            ],
-            &[PromptStyle::Define, PromptStyle::First],
-        );
-    }
-
-    #[test]
-    fn test_multiline_arguments() {
-        check_segmentation(
-            r#"define !macro1(
-  a(), b(
-  ),
-  c()
-)
-!enddefine.
-"#,
-            Syntax::Interactive,
-            &[
-                (Segment::Identifier, "define"),
-                (Segment::Spaces, " "),
-                (Segment::MacroName, "!macro1"),
-                (Segment::Punct, "("),
-                (Segment::Newline, "\n"),
-                (Segment::Spaces, "  "),
-                (Segment::Identifier, "a"),
-                (Segment::Punct, "("),
-                (Segment::Punct, ")"),
-                (Segment::Punct, ","),
-                (Segment::Spaces, " "),
-                (Segment::Identifier, "b"),
-                (Segment::Punct, "("),
-                (Segment::Newline, "\n"),
-                (Segment::Spaces, "  "),
-                (Segment::Punct, ")"),
-                (Segment::Punct, ","),
-                (Segment::Newline, "\n"),
-                (Segment::Spaces, "  "),
-                (Segment::Identifier, "c"),
-                (Segment::Punct, "("),
-                (Segment::Punct, ")"),
-                (Segment::Newline, "\n"),
-                (Segment::Punct, ")"),
-                (Segment::Newline, "\n"),
-                (Segment::Identifier, "!enddefine"),
-                (Segment::EndCommand, "."),
-                (Segment::Newline, "\n"),
-            ],
-            &[
-                PromptStyle::Later,
-                PromptStyle::Later,
-                PromptStyle::Later,
-                PromptStyle::Later,
-                PromptStyle::Define,
-                PromptStyle::First,
-            ],
-        );
-    }
-
-    #[test]
-    fn test_arguments_start_on_second_line() {
-        check_segmentation(
-            r#"define !macro1
-(x,y,z
-)
-content 1
-content 2
-!enddefine.
-"#,
-            Syntax::Interactive,
-            &[
-                (Segment::Identifier, "define"),
-                (Segment::Spaces, " "),
-                (Segment::MacroName, "!macro1"),
-                (Segment::Newline, "\n"),
-                (Segment::Punct, "("),
-                (Segment::Identifier, "x"),
-                (Segment::Punct, ","),
-                (Segment::Identifier, "y"),
-                (Segment::Punct, ","),
-                (Segment::Identifier, "z"),
-                (Segment::Newline, "\n"),
-                (Segment::Punct, ")"),
-                (Segment::Newline, "\n"),
-                (Segment::MacroBody, "content 1"),
-                (Segment::Newline, "\n"),
-                (Segment::MacroBody, "content 2"),
-                (Segment::Newline, "\n"),
-                (Segment::Identifier, "!enddefine"),
-                (Segment::EndCommand, "."),
-                (Segment::Newline, "\n"),
-            ],
-            &[
-                PromptStyle::Later,
-                PromptStyle::Later,
-                PromptStyle::Define,
-                PromptStyle::Define,
-                PromptStyle::Define,
-                PromptStyle::First,
-            ],
-        );
-    }
-
-    #[test]
-    fn test_early_end_of_command_1() {
-        check_segmentation(
-            r#"define !macro1.
-data list /x 1.
-"#,
-            Syntax::Interactive,
-            &[
-                (Segment::Identifier, "define"),
-                (Segment::Spaces, " "),
-                (Segment::MacroName, "!macro1"),
-                (Segment::EndCommand, "."),
-                (Segment::Newline, "\n"),
-                (Segment::Identifier, "data"),
-                (Segment::Spaces, " "),
-                (Segment::Identifier, "list"),
-                (Segment::Spaces, " "),
-                (Segment::Punct, "/"),
-                (Segment::Identifier, "x"),
-                (Segment::Spaces, " "),
-                (Segment::Number, "1"),
-                (Segment::EndCommand, "."),
-                (Segment::Newline, "\n"),
-            ],
-            &[PromptStyle::First, PromptStyle::First],
-        );
-    }
-
-    #[test]
-    fn test_early_end_of_command_2() {
-        check_segmentation(
-            r#"define !macro1
-x.
-data list /x 1.
-"#,
-            Syntax::Interactive,
-            &[
-                (Segment::Identifier, "define"),
-                (Segment::Spaces, " "),
-                (Segment::MacroName, "!macro1"),
-                (Segment::Newline, "\n"),
-                (Segment::Identifier, "x"),
-                (Segment::EndCommand, "."),
-                (Segment::Newline, "\n"),
-                (Segment::Identifier, "data"),
-                (Segment::Spaces, " "),
-                (Segment::Identifier, "list"),
-                (Segment::Spaces, " "),
-                (Segment::Punct, "/"),
-                (Segment::Identifier, "x"),
-                (Segment::Spaces, " "),
-                (Segment::Number, "1"),
-                (Segment::EndCommand, "."),
-                (Segment::Newline, "\n"),
-            ],
-            &[PromptStyle::Later, PromptStyle::First, PromptStyle::First],
-        );
-    }
-
-    #[test]
-    fn test_early_end_of_command_3() {
-        check_segmentation(
-            r#"define !macro1(.
-x.
-data list /x 1.
-"#,
-            Syntax::Interactive,
-            &[
-                (Segment::Identifier, "define"),
-                (Segment::Spaces, " "),
-                (Segment::MacroName, "!macro1"),
-                (Segment::Punct, "("),
-                (Segment::EndCommand, "."),
-                (Segment::Newline, "\n"),
-                (Segment::Identifier, "x"),
-                (Segment::EndCommand, "."),
-                (Segment::Newline, "\n"),
-                (Segment::Identifier, "data"),
-                (Segment::Spaces, " "),
-                (Segment::Identifier, "list"),
-                (Segment::Spaces, " "),
-                (Segment::Punct, "/"),
-                (Segment::Identifier, "x"),
-                (Segment::Spaces, " "),
-                (Segment::Number, "1"),
-                (Segment::EndCommand, "."),
-                (Segment::Newline, "\n"),
-            ],
-            &[PromptStyle::First, PromptStyle::First, PromptStyle::First],
-        );
-    }
-
-    #[test]
-    fn test_early_end_of_command_4() {
-        // Notice the command terminator at the end of the `DEFINE` command,
-        // which should not be there and ends it early.
-        check_segmentation(
-            r#"define !macro1.
-data list /x 1.
-"#,
-            Syntax::Interactive,
-            &[
-                (Segment::Identifier, "define"),
-                (Segment::Spaces, " "),
-                (Segment::MacroName, "!macro1"),
-                (Segment::EndCommand, "."),
-                (Segment::Newline, "\n"),
-                (Segment::Identifier, "data"),
-                (Segment::Spaces, " "),
-                (Segment::Identifier, "list"),
-                (Segment::Spaces, " "),
-                (Segment::Punct, "/"),
-                (Segment::Identifier, "x"),
-                (Segment::Spaces, " "),
-                (Segment::Number, "1"),
-                (Segment::EndCommand, "."),
-                (Segment::Newline, "\n"),
-            ],
-            &[PromptStyle::First, PromptStyle::First],
-        );
-    }
-
-    #[test]
-    fn test_missing_enddefine() {
-        check_segmentation(
-            r#"define !macro1()
-content line 1
-content line 2
-"#,
-            Syntax::Interactive,
-            &[
-                (Segment::Identifier, "define"),
-                (Segment::Spaces, " "),
-                (Segment::MacroName, "!macro1"),
-                (Segment::Punct, "("),
-                (Segment::Punct, ")"),
-                (Segment::Newline, "\n"),
-                (Segment::MacroBody, "content line 1"),
-                (Segment::Newline, "\n"),
-                (Segment::MacroBody, "content line 2"),
-                (Segment::Newline, "\n"),
-            ],
-            &[
-                PromptStyle::Define,
-                PromptStyle::Define,
-                PromptStyle::Define,
-            ],
-        );
-    }
-
-    #[test]
-    fn test_missing_enddefine_2() {
-        check_segmentation(
-            r#"define !macro1()
-"#,
-            Syntax::Interactive,
-            &[
-                (Segment::Identifier, "define"),
-                (Segment::Spaces, " "),
-                (Segment::MacroName, "!macro1"),
-                (Segment::Punct, "("),
-                (Segment::Punct, ")"),
-                (Segment::Newline, "\n"),
-            ],
-            &[PromptStyle::Define],
-        );
-    }
-}
-
-#[test]
-fn test_batch_mode() {
-    check_segmentation(
-        r#"first command
-     another line of first command
-+  second command
-third command
-
-fourth command.
-   fifth command.
-"#,
-        Syntax::Batch,
-        &[
-            (Segment::Identifier, "first"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "command"),
-            (Segment::Newline, "\n"),
-            (Segment::Spaces, "     "),
-            (Segment::Identifier, "another"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "line"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "of"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "first"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "command"),
-            (Segment::Newline, "\n"),
-            (Segment::StartCommand, "+"),
-            (Segment::Spaces, "  "),
-            (Segment::Identifier, "second"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "command"),
-            (Segment::Newline, "\n"),
-            (Segment::StartCommand, ""),
-            (Segment::Identifier, "third"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "command"),
-            (Segment::Newline, "\n"),
-            (Segment::SeparateCommands, ""),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "fourth"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "command"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::Spaces, "   "),
-            (Segment::Identifier, "fifth"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "command"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-        ],
-        &[
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::First,
-        ],
-    );
-}
-
-#[test]
-fn test_auto_mode() {
-    check_segmentation(
-        r#"command
-     another line of command
-2sls
-+  another command
-another line of second command
-data list /x 1
-aggregate.
-print eject.
-twostep cluster
-
-
-fourth command.
-   fifth command.
-"#,
-        Syntax::Auto,
-        &[
-            (Segment::Identifier, "command"),
-            (Segment::Newline, "\n"),
-            (Segment::Spaces, "     "),
-            (Segment::Identifier, "another"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "line"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "of"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "command"),
-            (Segment::Newline, "\n"),
-            (Segment::StartCommand, ""),
-            (Segment::Number, "2"),
-            (Segment::Identifier, "sls"),
-            (Segment::Newline, "\n"),
-            (Segment::StartCommand, "+"),
-            (Segment::Spaces, "  "),
-            (Segment::Identifier, "another"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "command"),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "another"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "line"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "of"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "second"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "command"),
-            (Segment::Newline, "\n"),
-            (Segment::StartCommand, ""),
-            (Segment::Identifier, "data"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "list"),
-            (Segment::Spaces, " "),
-            (Segment::Punct, "/"),
-            (Segment::Identifier, "x"),
-            (Segment::Spaces, " "),
-            (Segment::Number, "1"),
-            (Segment::Newline, "\n"),
-            (Segment::StartCommand, ""),
-            (Segment::Identifier, "aggregate"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "print"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "eject"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "twostep"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "cluster"),
-            (Segment::Newline, "\n"),
-            (Segment::SeparateCommands, ""),
-            (Segment::Newline, "\n"),
-            (Segment::SeparateCommands, ""),
-            (Segment::Newline, "\n"),
-            (Segment::Identifier, "fourth"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "command"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-            (Segment::Spaces, "   "),
-            (Segment::Identifier, "fifth"),
-            (Segment::Spaces, " "),
-            (Segment::Identifier, "command"),
-            (Segment::EndCommand, "."),
-            (Segment::Newline, "\n"),
-        ],
-        &[
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::Later,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::Later,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::First,
-            PromptStyle::First,
-        ],
-    );
-}
diff --git a/rust/pspp/src/lex/segment/tests.rs b/rust/pspp/src/lex/segment/tests.rs
new file mode 100644 (file)
index 0000000..12b6591
--- /dev/null
@@ -0,0 +1,2148 @@
+// PSPP - a program for statistical analysis.
+// Copyright (C) 2025 Free Software Foundation, Inc.
+//
+// This program is free software: you can redistribute it and/or modify it under
+// the terms of the GNU General Public License as published by the Free Software
+// Foundation, either version 3 of the License, or (at your option) any later
+// version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+// FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+// details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program.  If not, see <http://www.gnu.org/licenses/>.
+
+use crate::prompt::PromptStyle;
+
+use super::{Segment, Segmenter, Syntax};
+
+fn push_segment(
+    segmenter: &mut Segmenter,
+    input: &str,
+    one_byte: bool,
+) -> Option<(usize, Segment)> {
+    if one_byte {
+        for len in input.char_indices().map(|(pos, _c)| pos) {
+            if let Ok(result) = segmenter.push(&input[..len], false) {
+                return result;
+            }
+        }
+    }
+    segmenter.push(input, true).unwrap()
+}
+
+fn _check_segmentation(
+    mut input: &str,
+    mode: Syntax,
+    expect_segments: &[(Segment, &str)],
+    expect_prompts: &[PromptStyle],
+    one_byte: bool,
+) {
+    let mut segments = Vec::with_capacity(expect_segments.len());
+    let mut prompts = Vec::new();
+    let mut segmenter = Segmenter::new(mode, false);
+    while let Some((seg_len, seg_type)) = push_segment(&mut segmenter, input, one_byte) {
+        let (token, rest) = input.split_at(seg_len);
+        segments.push((seg_type, token));
+        if let Segment::Newline = seg_type {
+            prompts.push(segmenter.prompt());
+        }
+        input = rest;
+    }
+
+    if segments != expect_segments {
+        eprintln!("segments differ from expected:");
+        let difference = diff::slice(expect_segments, &segments);
+        for result in difference {
+            match result {
+                diff::Result::Left(left) => eprintln!("-{left:?}"),
+                diff::Result::Both(left, _right) => eprintln!(" {left:?}"),
+                diff::Result::Right(right) => eprintln!("+{right:?}"),
+            }
+        }
+        panic!();
+    }
+
+    if prompts != expect_prompts {
+        eprintln!("prompts differ from expected:");
+        let difference = diff::slice(expect_prompts, &prompts);
+        for result in difference {
+            match result {
+                diff::Result::Left(left) => eprintln!("-{left:?}"),
+                diff::Result::Both(left, _right) => eprintln!(" {left:?}"),
+                diff::Result::Right(right) => eprintln!("+{right:?}"),
+            }
+        }
+        panic!();
+    }
+}
+
+fn check_segmentation(
+    input: &str,
+    mode: Syntax,
+    expect_segments: &[(Segment, &str)],
+    expect_prompts: &[PromptStyle],
+) {
+    for (one_byte, one_byte_name) in [(false, "full-string"), (true, "byte-by-byte")] {
+        println!("running {one_byte_name} segmentation test with LF newlines...");
+        _check_segmentation(input, mode, expect_segments, expect_prompts, one_byte);
+
+        println!("running {one_byte_name} segmentation test with CRLF newlines...");
+        _check_segmentation(
+            &input.replace('\n', "\r\n"),
+            mode,
+            &expect_segments
+                .iter()
+                .map(|(segment, s)| match *segment {
+                    Segment::Newline => (Segment::Newline, "\r\n"),
+                    _ => (*segment, *s),
+                })
+                .collect::<Vec<_>>(),
+            expect_prompts,
+            one_byte,
+        );
+
+        if let Some(input) = input.strip_suffix('\n') {
+            println!("running {one_byte_name} segmentation test without final newline...");
+            let mut expect_segments: Vec<_> = expect_segments.to_vec();
+            assert_eq!(expect_segments.pop(), Some((Segment::Newline, "\n")));
+            while let Some((Segment::SeparateCommands | Segment::EndCommand, "")) =
+                expect_segments.last()
+            {
+                expect_segments.pop();
+            }
+            _check_segmentation(
+                input,
+                mode,
+                &expect_segments,
+                &expect_prompts[..expect_prompts.len() - 1],
+                one_byte,
+            );
+        }
+    }
+}
+
+#[allow(dead_code)]
+fn print_segmentation(mut input: &str) {
+    let mut segmenter = Segmenter::new(Syntax::Interactive, false);
+    while let Some((seg_len, seg_type)) = segmenter.push(input, true).unwrap() {
+        let (token, rest) = input.split_at(seg_len);
+        print!("{seg_type:?} {token:?}");
+        if let Segment::Newline = seg_type {
+            print!(" ({:?})", segmenter.prompt())
+        }
+        println!();
+        input = rest;
+    }
+}
+
+#[test]
+fn test_identifiers() {
+    check_segmentation(
+        r#"a ab abc abcd !abcd
+A AB ABC ABCD !ABCD
+aB aBC aBcD !aBcD
+$x $y $z !$z
+grève Ângstrom poté
+#a #b #c ## #d !#d
+@efg @ @@. @#@ !@ 
+## # #12345 #.#
+f@#_.#6
+GhIjK
+.x 1y _z
+!abc abc!
+"#,
+        Syntax::Auto,
+        &[
+            (Segment::Identifier, "a"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "ab"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "abc"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "abcd"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "!abcd"),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "A"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "AB"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "ABC"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "ABCD"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "!ABCD"),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "aB"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "aBC"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "aBcD"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "!aBcD"),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "$x"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "$y"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "$z"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "!$z"),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "grève"),
+            (Segment::Spaces, "\u{00a0}"),
+            (Segment::Identifier, "Ângstrom"),
+            (Segment::Spaces, "\u{00a0}"),
+            (Segment::Identifier, "poté"),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "#a"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "#b"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "#c"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "##"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "#d"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "!#d"),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "@efg"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "@"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "@@."),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "@#@"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "!@"),
+            (Segment::Spaces, " "),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "##"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "#"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "#12345"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "#.#"),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "f@#_.#6"),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "GhIjK"),
+            (Segment::Newline, "\n"),
+            (Segment::StartCommand, "."),
+            (Segment::Identifier, "x"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "1"),
+            (Segment::Identifier, "y"),
+            (Segment::Spaces, " "),
+            (Segment::Punct, "_"),
+            (Segment::Identifier, "z"),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "!abc"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "abc"),
+            (Segment::Punct, "!"),
+            (Segment::Newline, "\n"),
+        ],
+        &[
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+        ],
+    );
+}
+
+#[test]
+fn test_identifiers_ending_in_dot() {
+    check_segmentation(
+        r#"abcd. abcd.
+ABCD. ABCD.
+aBcD. aBcD. 
+$y. $z. あいうえお.
+#c. #d..
+@@. @@....
+#.#.
+#abcd.
+.
+. 
+LMNOP. 
+QRSTUV./* end of line comment */
+qrstuv. /* end of line comment */
+QrStUv./* end of line comment */ 
+wxyz./* unterminated end of line comment
+WXYZ. /* unterminated end of line comment
+WxYz./* unterminated end of line comment 
+"#,
+        Syntax::Auto,
+        &[
+            (Segment::Identifier, "abcd."),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "abcd"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "ABCD."),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "ABCD"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "aBcD."),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "aBcD"),
+            (Segment::EndCommand, "."),
+            (Segment::Spaces, " "),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "$y."),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "$z."),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "あいうえお"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "#c."),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "#d."),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "@@."),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "@@..."),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "#.#"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "#abcd"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::StartCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::StartCommand, "."),
+            (Segment::Spaces, " "),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "LMNOP"),
+            (Segment::EndCommand, "."),
+            (Segment::Spaces, " "),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "QRSTUV"),
+            (Segment::EndCommand, "."),
+            (Segment::Comment, "/* end of line comment */"),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "qrstuv"),
+            (Segment::EndCommand, "."),
+            (Segment::Spaces, " "),
+            (Segment::Comment, "/* end of line comment */"),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "QrStUv"),
+            (Segment::EndCommand, "."),
+            (Segment::Comment, "/* end of line comment */"),
+            (Segment::Spaces, " "),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "wxyz"),
+            (Segment::EndCommand, "."),
+            (Segment::Comment, "/* unterminated end of line comment"),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "WXYZ"),
+            (Segment::EndCommand, "."),
+            (Segment::Spaces, " "),
+            (Segment::Comment, "/* unterminated end of line comment"),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "WxYz"),
+            (Segment::EndCommand, "."),
+            (Segment::Comment, "/* unterminated end of line comment "),
+            (Segment::Newline, "\n"),
+        ],
+        &[
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::First,
+        ],
+    );
+}
+
+#[test]
+fn test_reserved_words() {
+    check_segmentation(
+        r#"and or not eq ge gt le lt ne all by to with
+AND OR NOT EQ GE GT LE LT NE ALL BY TO WITH
+andx orx notx eqx gex gtx lex ltx nex allx byx tox withx
+and. with.
+"#,
+        Syntax::Auto,
+        &[
+            (Segment::Identifier, "and"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "or"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "not"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "eq"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "ge"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "gt"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "le"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "lt"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "ne"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "all"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "by"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "to"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "with"),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "AND"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "OR"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "NOT"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "EQ"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "GE"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "GT"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "LE"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "LT"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "NE"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "ALL"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "BY"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "TO"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "WITH"),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "andx"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "orx"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "notx"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "eqx"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "gex"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "gtx"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "lex"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "ltx"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "nex"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "allx"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "byx"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "tox"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "withx"),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "and."),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "with"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+        ],
+        &[
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::First,
+        ],
+    );
+}
+
+#[test]
+fn test_punctuation() {
+    check_segmentation(
+        r#"~ & | = >= > <= < ~= <> ( ) , - + * / [ ] **
+~&|=>=><=<~=<>(),-+*/[]**!*
+% : ; ? _ ` { } ~ !*
+"#,
+        Syntax::Auto,
+        &[
+            (Segment::Punct, "~"),
+            (Segment::Spaces, " "),
+            (Segment::Punct, "&"),
+            (Segment::Spaces, " "),
+            (Segment::Punct, "|"),
+            (Segment::Spaces, " "),
+            (Segment::Punct, "="),
+            (Segment::Spaces, " "),
+            (Segment::Punct, ">="),
+            (Segment::Spaces, " "),
+            (Segment::Punct, ">"),
+            (Segment::Spaces, " "),
+            (Segment::Punct, "<="),
+            (Segment::Spaces, " "),
+            (Segment::Punct, "<"),
+            (Segment::Spaces, " "),
+            (Segment::Punct, "~="),
+            (Segment::Spaces, " "),
+            (Segment::Punct, "<>"),
+            (Segment::Spaces, " "),
+            (Segment::Punct, "("),
+            (Segment::Spaces, " "),
+            (Segment::Punct, ")"),
+            (Segment::Spaces, " "),
+            (Segment::Punct, ","),
+            (Segment::Spaces, " "),
+            (Segment::Punct, "-"),
+            (Segment::Spaces, " "),
+            (Segment::Punct, "+"),
+            (Segment::Spaces, " "),
+            (Segment::Punct, "*"),
+            (Segment::Spaces, " "),
+            (Segment::Punct, "/"),
+            (Segment::Spaces, " "),
+            (Segment::Punct, "["),
+            (Segment::Spaces, " "),
+            (Segment::Punct, "]"),
+            (Segment::Spaces, " "),
+            (Segment::Punct, "**"),
+            (Segment::Newline, "\n"),
+            (Segment::Punct, "~"),
+            (Segment::Punct, "&"),
+            (Segment::Punct, "|"),
+            (Segment::Punct, "="),
+            (Segment::Punct, ">="),
+            (Segment::Punct, ">"),
+            (Segment::Punct, "<="),
+            (Segment::Punct, "<"),
+            (Segment::Punct, "~="),
+            (Segment::Punct, "<>"),
+            (Segment::Punct, "("),
+            (Segment::Punct, ")"),
+            (Segment::Punct, ","),
+            (Segment::Punct, "-"),
+            (Segment::Punct, "+"),
+            (Segment::Punct, "*"),
+            (Segment::Punct, "/"),
+            (Segment::Punct, "["),
+            (Segment::Punct, "]"),
+            (Segment::Punct, "**"),
+            (Segment::Punct, "!*"),
+            (Segment::Newline, "\n"),
+            (Segment::Punct, "%"),
+            (Segment::Spaces, " "),
+            (Segment::Punct, ":"),
+            (Segment::Spaces, " "),
+            (Segment::Punct, ";"),
+            (Segment::Spaces, " "),
+            (Segment::Punct, "?"),
+            (Segment::Spaces, " "),
+            (Segment::Punct, "_"),
+            (Segment::Spaces, " "),
+            (Segment::Punct, "`"),
+            (Segment::Spaces, " "),
+            (Segment::Punct, "{"),
+            (Segment::Spaces, " "),
+            (Segment::Punct, "}"),
+            (Segment::Spaces, " "),
+            (Segment::Punct, "~"),
+            (Segment::Spaces, " "),
+            (Segment::Punct, "!*"),
+            (Segment::Newline, "\n"),
+        ],
+        &[PromptStyle::Later, PromptStyle::Later, PromptStyle::Later],
+    );
+}
+
+#[test]
+fn test_positive_numbers() {
+    check_segmentation(
+        r#"0 1 01 001. 1.
+123. /* comment 1 */ /* comment 2 */
+.1 0.1 00.1 00.10
+5e1 6E-1 7e+1 6E+01 6e-03
+.3E1 .4e-1 .5E+1 .6e+01 .7E-03
+1.23e1 45.6E-1 78.9e+1 99.9E+01 11.2e-03
+. 1e e1 1e+ 1e- 1.
+"#,
+        Syntax::Auto,
+        &[
+            (Segment::Number, "0"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "1"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "01"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "001."),
+            (Segment::Spaces, " "),
+            (Segment::Number, "1"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::Number, "123"),
+            (Segment::EndCommand, "."),
+            (Segment::Spaces, " "),
+            (Segment::Comment, "/* comment 1 */"),
+            (Segment::Spaces, " "),
+            (Segment::Comment, "/* comment 2 */"),
+            (Segment::Newline, "\n"),
+            (Segment::StartCommand, "."),
+            (Segment::Number, "1"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "0.1"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "00.1"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "00.10"),
+            (Segment::Newline, "\n"),
+            (Segment::Number, "5e1"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "6E-1"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "7e+1"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "6E+01"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "6e-03"),
+            (Segment::Newline, "\n"),
+            (Segment::StartCommand, "."),
+            (Segment::Number, "3E1"),
+            (Segment::Spaces, " "),
+            (Segment::Number, ".4e-1"),
+            (Segment::Spaces, " "),
+            (Segment::Number, ".5E+1"),
+            (Segment::Spaces, " "),
+            (Segment::Number, ".6e+01"),
+            (Segment::Spaces, " "),
+            (Segment::Number, ".7E-03"),
+            (Segment::Newline, "\n"),
+            (Segment::Number, "1.23e1"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "45.6E-1"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "78.9e+1"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "99.9E+01"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "11.2e-03"),
+            (Segment::Newline, "\n"),
+            (Segment::StartCommand, "."),
+            (Segment::Spaces, " "),
+            (Segment::ExpectedExponent, "1e"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "e1"),
+            (Segment::Spaces, " "),
+            (Segment::ExpectedExponent, "1e+"),
+            (Segment::Spaces, " "),
+            (Segment::ExpectedExponent, "1e-"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "1"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+        ],
+        &[
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::First,
+        ],
+    );
+}
+
+#[test]
+fn test_negative_numbers() {
+    check_segmentation(
+        r#" -0 -1 -01 -001. -1.
+ -123. /* comment 1 */ /* comment 2 */
+ -.1 -0.1 -00.1 -00.10
+ -5e1 -6E-1 -7e+1 -6E+01 -6e-03
+ -.3E1 -.4e-1 -.5E+1 -.6e+01 -.7E-03
+ -1.23e1 -45.6E-1 -78.9e+1 -99.9E+01 -11.2e-03
+ -/**/1
+ -. -1e -e1 -1e+ -1e- -1.
+"#,
+        Syntax::Auto,
+        &[
+            (Segment::Spaces, " "),
+            (Segment::Number, "-0"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "-1"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "-01"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "-001."),
+            (Segment::Spaces, " "),
+            (Segment::Number, "-1"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "-123"),
+            (Segment::EndCommand, "."),
+            (Segment::Spaces, " "),
+            (Segment::Comment, "/* comment 1 */"),
+            (Segment::Spaces, " "),
+            (Segment::Comment, "/* comment 2 */"),
+            (Segment::Newline, "\n"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "-.1"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "-0.1"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "-00.1"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "-00.10"),
+            (Segment::Newline, "\n"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "-5e1"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "-6E-1"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "-7e+1"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "-6E+01"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "-6e-03"),
+            (Segment::Newline, "\n"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "-.3E1"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "-.4e-1"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "-.5E+1"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "-.6e+01"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "-.7E-03"),
+            (Segment::Newline, "\n"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "-1.23e1"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "-45.6E-1"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "-78.9e+1"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "-99.9E+01"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "-11.2e-03"),
+            (Segment::Newline, "\n"),
+            (Segment::Spaces, " "),
+            (Segment::Punct, "-"),
+            (Segment::Comment, "/**/"),
+            (Segment::Number, "1"),
+            (Segment::Newline, "\n"),
+            (Segment::Spaces, " "),
+            (Segment::Punct, "-"),
+            (Segment::Punct, "."),
+            (Segment::Spaces, " "),
+            (Segment::ExpectedExponent, "-1e"),
+            (Segment::Spaces, " "),
+            (Segment::Punct, "-"),
+            (Segment::Identifier, "e1"),
+            (Segment::Spaces, " "),
+            (Segment::ExpectedExponent, "-1e+"),
+            (Segment::Spaces, " "),
+            (Segment::ExpectedExponent, "-1e-"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "-1"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+        ],
+        &[
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::First,
+        ],
+    );
+}
+
+#[test]
+fn test_strings() {
+    check_segmentation(
+        r#"'x' "y" 'abc'
+'Don''t' "Can't" 'Won''t'
+"""quoted""" '"quoted"'
+'' ""
+'missing end quote
+"missing double quote
+x"4142" X'5152'
+u'fffd' U"041"
++ new command
++ /* comment */ 'string continuation'
++ /* also a punctuator on blank line
+- 'new command'
+"#,
+        Syntax::Auto,
+        &[
+            (Segment::QuotedString, "'x'"),
+            (Segment::Spaces, " "),
+            (Segment::QuotedString, "\"y\""),
+            (Segment::Spaces, " "),
+            (Segment::QuotedString, "'abc'"),
+            (Segment::Newline, "\n"),
+            (Segment::QuotedString, "'Don''t'"),
+            (Segment::Spaces, " "),
+            (Segment::QuotedString, "\"Can't\""),
+            (Segment::Spaces, " "),
+            (Segment::QuotedString, "'Won''t'"),
+            (Segment::Newline, "\n"),
+            (Segment::QuotedString, "\"\"\"quoted\"\"\""),
+            (Segment::Spaces, " "),
+            (Segment::QuotedString, "'\"quoted\"'"),
+            (Segment::Newline, "\n"),
+            (Segment::QuotedString, "''"),
+            (Segment::Spaces, " "),
+            (Segment::QuotedString, "\"\""),
+            (Segment::Newline, "\n"),
+            (Segment::ExpectedQuote, "'missing end quote"),
+            (Segment::Newline, "\n"),
+            (Segment::ExpectedQuote, "\"missing double quote"),
+            (Segment::Newline, "\n"),
+            (Segment::HexString, "x\"4142\""),
+            (Segment::Spaces, " "),
+            (Segment::HexString, "X'5152'"),
+            (Segment::Newline, "\n"),
+            (Segment::UnicodeString, "u'fffd'"),
+            (Segment::Spaces, " "),
+            (Segment::UnicodeString, "U\"041\""),
+            (Segment::Newline, "\n"),
+            (Segment::StartCommand, "+"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "new"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "command"),
+            (Segment::Newline, "\n"),
+            (Segment::Punct, "+"),
+            (Segment::Spaces, " "),
+            (Segment::Comment, "/* comment */"),
+            (Segment::Spaces, " "),
+            (Segment::QuotedString, "'string continuation'"),
+            (Segment::Newline, "\n"),
+            (Segment::Punct, "+"),
+            (Segment::Spaces, " "),
+            (Segment::Comment, "/* also a punctuator on blank line"),
+            (Segment::Newline, "\n"),
+            (Segment::StartCommand, "-"),
+            (Segment::Spaces, " "),
+            (Segment::QuotedString, "'new command'"),
+            (Segment::Newline, "\n"),
+        ],
+        &[
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+        ],
+    );
+}
+
+#[test]
+fn test_shbang() {
+    check_segmentation(
+        r#"#! /usr/bin/pspp
+title my title.
+#! /usr/bin/pspp
+"#,
+        Syntax::Interactive,
+        &[
+            (Segment::Shbang, "#! /usr/bin/pspp"),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "title"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "my"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "title"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "#"),
+            (Segment::Punct, "!"),
+            (Segment::Spaces, " "),
+            (Segment::Punct, "/"),
+            (Segment::Identifier, "usr"),
+            (Segment::Punct, "/"),
+            (Segment::Identifier, "bin"),
+            (Segment::Punct, "/"),
+            (Segment::Identifier, "pspp"),
+            (Segment::Newline, "\n"),
+        ],
+        &[PromptStyle::First, PromptStyle::First, PromptStyle::Later],
+    );
+}
+
+#[test]
+fn test_comment_command() {
+    check_segmentation(
+        r#"* Comment commands "don't
+have to contain valid tokens.
+
+** Check ambiguity with ** token.
+****************.
+
+comment keyword works too.
+COMM also.
+com is ambiguous with COMPUTE.
+
+   * Comment need not start at left margin.
+
+* Comment ends with blank line
+
+next command.
+
+"#,
+        Syntax::Interactive,
+        &[
+            (Segment::CommentCommand, "* Comment commands \"don't"),
+            (Segment::Newline, "\n"),
+            (Segment::CommentCommand, "have to contain valid tokens"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::SeparateCommands, ""),
+            (Segment::Newline, "\n"),
+            (Segment::CommentCommand, "** Check ambiguity with ** token"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::CommentCommand, "****************"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::SeparateCommands, ""),
+            (Segment::Newline, "\n"),
+            (Segment::CommentCommand, "comment keyword works too"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::CommentCommand, "COMM also"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "com"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "is"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "ambiguous"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "with"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "COMPUTE"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::SeparateCommands, ""),
+            (Segment::Newline, "\n"),
+            (Segment::Spaces, "   "),
+            (
+                Segment::CommentCommand,
+                "* Comment need not start at left margin",
+            ),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::SeparateCommands, ""),
+            (Segment::Newline, "\n"),
+            (Segment::CommentCommand, "* Comment ends with blank line"),
+            (Segment::Newline, "\n"),
+            (Segment::SeparateCommands, ""),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "next"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "command"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::SeparateCommands, ""),
+            (Segment::Newline, "\n"),
+        ],
+        &[
+            PromptStyle::Comment,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::Comment,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::First,
+        ],
+    );
+}
+
+#[test]
+fn test_document_command() {
+    check_segmentation(
+        r#"DOCUMENT one line.
+DOC more
+    than
+        one
+            line.
+docu
+first.paragraph
+isn't parsed as tokens
+
+second paragraph.
+"#,
+        Syntax::Interactive,
+        &[
+            (Segment::StartDocument, ""),
+            (Segment::Document, "DOCUMENT one line."),
+            (Segment::EndCommand, ""),
+            (Segment::SeparateCommands, ""),
+            (Segment::Newline, "\n"),
+            (Segment::StartDocument, ""),
+            (Segment::Document, "DOC more"),
+            (Segment::Newline, "\n"),
+            (Segment::Document, "    than"),
+            (Segment::Newline, "\n"),
+            (Segment::Document, "        one"),
+            (Segment::Newline, "\n"),
+            (Segment::Document, "            line."),
+            (Segment::EndCommand, ""),
+            (Segment::SeparateCommands, ""),
+            (Segment::Newline, "\n"),
+            (Segment::StartDocument, ""),
+            (Segment::Document, "docu"),
+            (Segment::Newline, "\n"),
+            (Segment::Document, "first.paragraph"),
+            (Segment::Newline, "\n"),
+            (Segment::Document, "isn't parsed as tokens"),
+            (Segment::Newline, "\n"),
+            (Segment::Document, ""),
+            (Segment::Newline, "\n"),
+            (Segment::Document, "second paragraph."),
+            (Segment::EndCommand, ""),
+            (Segment::SeparateCommands, ""),
+            (Segment::Newline, "\n"),
+        ],
+        &[
+            PromptStyle::First,
+            PromptStyle::Document,
+            PromptStyle::Document,
+            PromptStyle::Document,
+            PromptStyle::First,
+            PromptStyle::Document,
+            PromptStyle::Document,
+            PromptStyle::Document,
+            PromptStyle::Document,
+            PromptStyle::First,
+        ],
+    );
+}
+
+#[test]
+fn test_file_label_command() {
+    check_segmentation(
+        r#"FIL label isn't quoted.
+FILE
+  lab 'is quoted'.
+FILE /*
+/**/  lab not quoted here either
+
+"#,
+        Syntax::Interactive,
+        &[
+            (Segment::Identifier, "FIL"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "label"),
+            (Segment::Spaces, " "),
+            (Segment::UnquotedString, "isn't quoted"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "FILE"),
+            (Segment::Newline, "\n"),
+            (Segment::Spaces, "  "),
+            (Segment::Identifier, "lab"),
+            (Segment::Spaces, " "),
+            (Segment::QuotedString, "'is quoted'"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "FILE"),
+            (Segment::Spaces, " "),
+            (Segment::Comment, "/*"),
+            (Segment::Newline, "\n"),
+            (Segment::Comment, "/**/"),
+            (Segment::Spaces, "  "),
+            (Segment::Identifier, "lab"),
+            (Segment::Spaces, " "),
+            (Segment::UnquotedString, "not quoted here either"),
+            (Segment::Newline, "\n"),
+            (Segment::SeparateCommands, ""),
+            (Segment::Newline, "\n"),
+        ],
+        &[
+            PromptStyle::First,
+            PromptStyle::Later,
+            PromptStyle::First,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::First,
+        ],
+    );
+}
+
+#[test]
+fn test_begin_data() {
+    check_segmentation(
+        r#"begin data.
+end data.
+
+begin data. /*
+123
+xxx
+end data.
+
+BEG /**/ DAT /*
+5 6 7 /* x
+
+end  data
+end data
+.
+
+begin
+ data.
+data
+end data.
+
+begin data "xxx".
+begin data 123.
+not data
+"#,
+        Syntax::Interactive,
+        &[
+            (Segment::Identifier, "begin"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "data"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "end"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "data"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::SeparateCommands, ""),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "begin"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "data"),
+            (Segment::EndCommand, "."),
+            (Segment::Spaces, " "),
+            (Segment::Comment, "/*"),
+            (Segment::Newline, "\n"),
+            (Segment::InlineData, "123"),
+            (Segment::Newline, "\n"),
+            (Segment::InlineData, "xxx"),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "end"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "data"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::SeparateCommands, ""),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "BEG"),
+            (Segment::Spaces, " "),
+            (Segment::Comment, "/**/"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "DAT"),
+            (Segment::Spaces, " "),
+            (Segment::Comment, "/*"),
+            (Segment::Newline, "\n"),
+            (Segment::InlineData, "5 6 7 /* x"),
+            (Segment::Newline, "\n"),
+            (Segment::InlineData, ""),
+            (Segment::Newline, "\n"),
+            (Segment::InlineData, "end  data"),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "end"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "data"),
+            (Segment::Newline, "\n"),
+            (Segment::StartCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::SeparateCommands, ""),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "begin"),
+            (Segment::Newline, "\n"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "data"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::InlineData, "data"),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "end"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "data"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::SeparateCommands, ""),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "begin"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "data"),
+            (Segment::Spaces, " "),
+            (Segment::QuotedString, "\"xxx\""),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "begin"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "data"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "123"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "not"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "data"),
+            (Segment::Newline, "\n"),
+        ],
+        &[
+            PromptStyle::Data,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::Data,
+            PromptStyle::Data,
+            PromptStyle::Data,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::Data,
+            PromptStyle::Data,
+            PromptStyle::Data,
+            PromptStyle::Data,
+            PromptStyle::Later,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::Later,
+            PromptStyle::Data,
+            PromptStyle::Data,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::Later,
+        ],
+    );
+}
+
+#[test]
+fn test_do_repeat() {
+    check_segmentation(
+        r#"do repeat x=a b c
+          y=d e f.
+  do repeat a=1 thru 5.
+another command.
+second command
++ third command.
+end /* x */ /* y */ repeat print.
+end
+ repeat.
+do
+  repeat #a=1.
+  inner command.
+end repeat.
+"#,
+        Syntax::Interactive,
+        &[
+            (Segment::Identifier, "do"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "repeat"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "x"),
+            (Segment::Punct, "="),
+            (Segment::Identifier, "a"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "b"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "c"),
+            (Segment::Newline, "\n"),
+            (Segment::Spaces, "          "),
+            (Segment::Identifier, "y"),
+            (Segment::Punct, "="),
+            (Segment::Identifier, "d"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "e"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "f"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::DoRepeatCommand, "  do repeat a=1 thru 5."),
+            (Segment::Newline, "\n"),
+            (Segment::DoRepeatCommand, "another command."),
+            (Segment::Newline, "\n"),
+            (Segment::DoRepeatCommand, "second command"),
+            (Segment::Newline, "\n"),
+            (Segment::DoRepeatCommand, "+ third command."),
+            (Segment::Newline, "\n"),
+            (
+                Segment::DoRepeatCommand,
+                "end /* x */ /* y */ repeat print.",
+            ),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "end"),
+            (Segment::Newline, "\n"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "repeat"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "do"),
+            (Segment::Newline, "\n"),
+            (Segment::Spaces, "  "),
+            (Segment::Identifier, "repeat"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "#a"),
+            (Segment::Punct, "="),
+            (Segment::Number, "1"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::DoRepeatCommand, "  inner command."),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "end"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "repeat"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+        ],
+        &[
+            PromptStyle::Later,
+            PromptStyle::DoRepeat,
+            PromptStyle::DoRepeat,
+            PromptStyle::DoRepeat,
+            PromptStyle::DoRepeat,
+            PromptStyle::DoRepeat,
+            PromptStyle::DoRepeat,
+            PromptStyle::Later,
+            PromptStyle::First,
+            PromptStyle::Later,
+            PromptStyle::DoRepeat,
+            PromptStyle::DoRepeat,
+            PromptStyle::First,
+        ],
+    );
+}
+
+#[test]
+fn test_do_repeat_overflow() {
+    const N: usize = 257;
+    let do_repeat: Vec<String> = (0..N)
+        .map(|i| format!("do repeat v{i}={i} thru {}.\n", i + 5))
+        .collect();
+    let end_repeat: Vec<String> = (0..N)
+        .rev()
+        .map(|i| format!("end repeat. /* {i}\n"))
+        .collect();
+
+    let s: String = do_repeat
+        .iter()
+        .chain(end_repeat.iter())
+        .map(|s| s.as_str())
+        .collect();
+    let mut expect_output = vec![
+        (Segment::Identifier, "do"),
+        (Segment::Spaces, " "),
+        (Segment::Identifier, "repeat"),
+        (Segment::Spaces, " "),
+        (Segment::Identifier, "v0"),
+        (Segment::Punct, "="),
+        (Segment::Number, "0"),
+        (Segment::Spaces, " "),
+        (Segment::Identifier, "thru"),
+        (Segment::Spaces, " "),
+        (Segment::Number, "5"),
+        (Segment::EndCommand, "."),
+        (Segment::Newline, "\n"),
+    ];
+    for (i, line) in do_repeat.iter().enumerate().take(N).skip(1) {
+        expect_output.push((Segment::DoRepeatCommand, line.trim_end()));
+        if i >= 255 {
+            expect_output.push((Segment::DoRepeatOverflow, ""));
+        }
+        expect_output.push((Segment::Newline, "\n"));
+    }
+    for line in &end_repeat[..254] {
+        expect_output.push((Segment::DoRepeatCommand, line.trim_end()));
+        expect_output.push((Segment::Newline, "\n"));
+    }
+    let comments: Vec<String> = (0..(N - 254)).rev().map(|i| format!("/* {i}")).collect();
+    for comment in &comments {
+        expect_output.extend([
+            (Segment::Identifier, "end"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "repeat"),
+            (Segment::EndCommand, "."),
+            (Segment::Spaces, " "),
+            (Segment::Comment, comment),
+            (Segment::Newline, "\n"),
+        ]);
+    }
+
+    let expect_prompts: Vec<_> = (0..N * 2 - 3)
+        .map(|_| PromptStyle::DoRepeat)
+        .chain([PromptStyle::First, PromptStyle::First, PromptStyle::First])
+        .collect();
+    check_segmentation(&s, Syntax::Interactive, &expect_output, &expect_prompts);
+}
+
+#[test]
+fn test_do_repeat_batch() {
+    check_segmentation(
+        r#"do repeat x=a b c
+          y=d e f
+do repeat a=1 thru 5
+another command
+second command
++ third command
+end /* x */ /* y */ repeat print
+end
+ repeat
+do
+  repeat #a=1
+
+  inner command
+end repeat
+"#,
+        Syntax::Batch,
+        &[
+            (Segment::Identifier, "do"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "repeat"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "x"),
+            (Segment::Punct, "="),
+            (Segment::Identifier, "a"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "b"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "c"),
+            (Segment::Newline, "\n"),
+            (Segment::Spaces, "          "),
+            (Segment::Identifier, "y"),
+            (Segment::Punct, "="),
+            (Segment::Identifier, "d"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "e"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "f"),
+            (Segment::Newline, "\n"),
+            (Segment::StartCommand, ""),
+            (Segment::DoRepeatCommand, "do repeat a=1 thru 5"),
+            (Segment::Newline, "\n"),
+            (Segment::DoRepeatCommand, "another command"),
+            (Segment::Newline, "\n"),
+            (Segment::DoRepeatCommand, "second command"),
+            (Segment::Newline, "\n"),
+            (Segment::DoRepeatCommand, "+ third command"),
+            (Segment::Newline, "\n"),
+            (Segment::DoRepeatCommand, "end /* x */ /* y */ repeat print"),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "end"),
+            (Segment::Newline, "\n"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "repeat"),
+            (Segment::Newline, "\n"),
+            (Segment::StartCommand, ""),
+            (Segment::Identifier, "do"),
+            (Segment::Newline, "\n"),
+            (Segment::Spaces, "  "),
+            (Segment::Identifier, "repeat"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "#a"),
+            (Segment::Punct, "="),
+            (Segment::Number, "1"),
+            (Segment::Newline, "\n"),
+            (Segment::SeparateCommands, ""),
+            (Segment::Newline, "\n"),
+            (Segment::DoRepeatCommand, "  inner command"),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "end"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "repeat"),
+            (Segment::Newline, "\n"),
+        ],
+        &[
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::DoRepeat,
+            PromptStyle::DoRepeat,
+            PromptStyle::DoRepeat,
+            PromptStyle::DoRepeat,
+            PromptStyle::DoRepeat,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::DoRepeat,
+            PromptStyle::DoRepeat,
+            PromptStyle::Later,
+        ],
+    );
+}
+
+mod define {
+    use crate::{
+        lex::segment::{Segment, Syntax},
+        prompt::PromptStyle,
+    };
+
+    use super::check_segmentation;
+
+    #[test]
+    fn test_simple() {
+        check_segmentation(
+            r#"define !macro1()
+var1 var2 var3 "!enddefine"
+!enddefine.
+"#,
+            Syntax::Interactive,
+            &[
+                (Segment::Identifier, "define"),
+                (Segment::Spaces, " "),
+                (Segment::MacroName, "!macro1"),
+                (Segment::Punct, "("),
+                (Segment::Punct, ")"),
+                (Segment::Newline, "\n"),
+                (Segment::MacroBody, "var1 var2 var3 \"!enddefine\""),
+                (Segment::Newline, "\n"),
+                (Segment::Identifier, "!enddefine"),
+                (Segment::EndCommand, "."),
+                (Segment::Newline, "\n"),
+            ],
+            &[PromptStyle::Define, PromptStyle::Define, PromptStyle::First],
+        );
+    }
+
+    #[test]
+    fn test_no_newline_after_parentheses() {
+        check_segmentation(
+            r#"define !macro1() var1 var2 var3 /* !enddefine
+!enddefine.
+"#,
+            Syntax::Interactive,
+            &[
+                (Segment::Identifier, "define"),
+                (Segment::Spaces, " "),
+                (Segment::MacroName, "!macro1"),
+                (Segment::Punct, "("),
+                (Segment::Punct, ")"),
+                (Segment::MacroBody, " var1 var2 var3 /* !enddefine"),
+                (Segment::Newline, "\n"),
+                (Segment::Identifier, "!enddefine"),
+                (Segment::EndCommand, "."),
+                (Segment::Newline, "\n"),
+            ],
+            &[PromptStyle::Define, PromptStyle::First],
+        );
+    }
+
+    #[test]
+    fn test_no_newline_before_enddefine() {
+        check_segmentation(
+            r#"define !macro1()
+var1 var2 var3!enddefine.
+"#,
+            Syntax::Interactive,
+            &[
+                (Segment::Identifier, "define"),
+                (Segment::Spaces, " "),
+                (Segment::MacroName, "!macro1"),
+                (Segment::Punct, "("),
+                (Segment::Punct, ")"),
+                (Segment::Newline, "\n"),
+                (Segment::MacroBody, "var1 var2 var3"),
+                (Segment::Identifier, "!enddefine"),
+                (Segment::EndCommand, "."),
+                (Segment::Newline, "\n"),
+            ],
+            &[PromptStyle::Define, PromptStyle::First],
+        );
+    }
+
+    #[test]
+    fn test_all_on_one_line() {
+        check_segmentation(
+            r#"define !macro1()var1 var2 var3!enddefine.
+"#,
+            Syntax::Interactive,
+            &[
+                (Segment::Identifier, "define"),
+                (Segment::Spaces, " "),
+                (Segment::MacroName, "!macro1"),
+                (Segment::Punct, "("),
+                (Segment::Punct, ")"),
+                (Segment::MacroBody, "var1 var2 var3"),
+                (Segment::Identifier, "!enddefine"),
+                (Segment::EndCommand, "."),
+                (Segment::Newline, "\n"),
+            ],
+            &[PromptStyle::First],
+        );
+    }
+
+    #[test]
+    fn test_empty() {
+        check_segmentation(
+            r#"define !macro1()
+!enddefine.
+"#,
+            Syntax::Interactive,
+            &[
+                (Segment::Identifier, "define"),
+                (Segment::Spaces, " "),
+                (Segment::MacroName, "!macro1"),
+                (Segment::Punct, "("),
+                (Segment::Punct, ")"),
+                (Segment::Newline, "\n"),
+                (Segment::Identifier, "!enddefine"),
+                (Segment::EndCommand, "."),
+                (Segment::Newline, "\n"),
+            ],
+            &[PromptStyle::Define, PromptStyle::First],
+        );
+    }
+
+    #[test]
+    fn test_blank_lines() {
+        check_segmentation(
+            r#"define !macro1()
+
+
+!enddefine.
+"#,
+            Syntax::Interactive,
+            &[
+                (Segment::Identifier, "define"),
+                (Segment::Spaces, " "),
+                (Segment::MacroName, "!macro1"),
+                (Segment::Punct, "("),
+                (Segment::Punct, ")"),
+                (Segment::Newline, "\n"),
+                (Segment::MacroBody, ""),
+                (Segment::Newline, "\n"),
+                (Segment::MacroBody, ""),
+                (Segment::Newline, "\n"),
+                (Segment::Identifier, "!enddefine"),
+                (Segment::EndCommand, "."),
+                (Segment::Newline, "\n"),
+            ],
+            &[
+                PromptStyle::Define,
+                PromptStyle::Define,
+                PromptStyle::Define,
+                PromptStyle::First,
+            ],
+        );
+    }
+
+    #[test]
+    fn test_arguments() {
+        check_segmentation(
+            r#"define !macro1(a(), b(), c())
+!enddefine.
+"#,
+            Syntax::Interactive,
+            &[
+                (Segment::Identifier, "define"),
+                (Segment::Spaces, " "),
+                (Segment::MacroName, "!macro1"),
+                (Segment::Punct, "("),
+                (Segment::Identifier, "a"),
+                (Segment::Punct, "("),
+                (Segment::Punct, ")"),
+                (Segment::Punct, ","),
+                (Segment::Spaces, " "),
+                (Segment::Identifier, "b"),
+                (Segment::Punct, "("),
+                (Segment::Punct, ")"),
+                (Segment::Punct, ","),
+                (Segment::Spaces, " "),
+                (Segment::Identifier, "c"),
+                (Segment::Punct, "("),
+                (Segment::Punct, ")"),
+                (Segment::Punct, ")"),
+                (Segment::Newline, "\n"),
+                (Segment::Identifier, "!enddefine"),
+                (Segment::EndCommand, "."),
+                (Segment::Newline, "\n"),
+            ],
+            &[PromptStyle::Define, PromptStyle::First],
+        );
+    }
+
+    #[test]
+    fn test_multiline_arguments() {
+        check_segmentation(
+            r#"define !macro1(
+  a(), b(
+  ),
+  c()
+)
+!enddefine.
+"#,
+            Syntax::Interactive,
+            &[
+                (Segment::Identifier, "define"),
+                (Segment::Spaces, " "),
+                (Segment::MacroName, "!macro1"),
+                (Segment::Punct, "("),
+                (Segment::Newline, "\n"),
+                (Segment::Spaces, "  "),
+                (Segment::Identifier, "a"),
+                (Segment::Punct, "("),
+                (Segment::Punct, ")"),
+                (Segment::Punct, ","),
+                (Segment::Spaces, " "),
+                (Segment::Identifier, "b"),
+                (Segment::Punct, "("),
+                (Segment::Newline, "\n"),
+                (Segment::Spaces, "  "),
+                (Segment::Punct, ")"),
+                (Segment::Punct, ","),
+                (Segment::Newline, "\n"),
+                (Segment::Spaces, "  "),
+                (Segment::Identifier, "c"),
+                (Segment::Punct, "("),
+                (Segment::Punct, ")"),
+                (Segment::Newline, "\n"),
+                (Segment::Punct, ")"),
+                (Segment::Newline, "\n"),
+                (Segment::Identifier, "!enddefine"),
+                (Segment::EndCommand, "."),
+                (Segment::Newline, "\n"),
+            ],
+            &[
+                PromptStyle::Later,
+                PromptStyle::Later,
+                PromptStyle::Later,
+                PromptStyle::Later,
+                PromptStyle::Define,
+                PromptStyle::First,
+            ],
+        );
+    }
+
+    #[test]
+    fn test_arguments_start_on_second_line() {
+        check_segmentation(
+            r#"define !macro1
+(x,y,z
+)
+content 1
+content 2
+!enddefine.
+"#,
+            Syntax::Interactive,
+            &[
+                (Segment::Identifier, "define"),
+                (Segment::Spaces, " "),
+                (Segment::MacroName, "!macro1"),
+                (Segment::Newline, "\n"),
+                (Segment::Punct, "("),
+                (Segment::Identifier, "x"),
+                (Segment::Punct, ","),
+                (Segment::Identifier, "y"),
+                (Segment::Punct, ","),
+                (Segment::Identifier, "z"),
+                (Segment::Newline, "\n"),
+                (Segment::Punct, ")"),
+                (Segment::Newline, "\n"),
+                (Segment::MacroBody, "content 1"),
+                (Segment::Newline, "\n"),
+                (Segment::MacroBody, "content 2"),
+                (Segment::Newline, "\n"),
+                (Segment::Identifier, "!enddefine"),
+                (Segment::EndCommand, "."),
+                (Segment::Newline, "\n"),
+            ],
+            &[
+                PromptStyle::Later,
+                PromptStyle::Later,
+                PromptStyle::Define,
+                PromptStyle::Define,
+                PromptStyle::Define,
+                PromptStyle::First,
+            ],
+        );
+    }
+
+    #[test]
+    fn test_early_end_of_command_1() {
+        check_segmentation(
+            r#"define !macro1.
+data list /x 1.
+"#,
+            Syntax::Interactive,
+            &[
+                (Segment::Identifier, "define"),
+                (Segment::Spaces, " "),
+                (Segment::MacroName, "!macro1"),
+                (Segment::EndCommand, "."),
+                (Segment::Newline, "\n"),
+                (Segment::Identifier, "data"),
+                (Segment::Spaces, " "),
+                (Segment::Identifier, "list"),
+                (Segment::Spaces, " "),
+                (Segment::Punct, "/"),
+                (Segment::Identifier, "x"),
+                (Segment::Spaces, " "),
+                (Segment::Number, "1"),
+                (Segment::EndCommand, "."),
+                (Segment::Newline, "\n"),
+            ],
+            &[PromptStyle::First, PromptStyle::First],
+        );
+    }
+
+    #[test]
+    fn test_early_end_of_command_2() {
+        check_segmentation(
+            r#"define !macro1
+x.
+data list /x 1.
+"#,
+            Syntax::Interactive,
+            &[
+                (Segment::Identifier, "define"),
+                (Segment::Spaces, " "),
+                (Segment::MacroName, "!macro1"),
+                (Segment::Newline, "\n"),
+                (Segment::Identifier, "x"),
+                (Segment::EndCommand, "."),
+                (Segment::Newline, "\n"),
+                (Segment::Identifier, "data"),
+                (Segment::Spaces, " "),
+                (Segment::Identifier, "list"),
+                (Segment::Spaces, " "),
+                (Segment::Punct, "/"),
+                (Segment::Identifier, "x"),
+                (Segment::Spaces, " "),
+                (Segment::Number, "1"),
+                (Segment::EndCommand, "."),
+                (Segment::Newline, "\n"),
+            ],
+            &[PromptStyle::Later, PromptStyle::First, PromptStyle::First],
+        );
+    }
+
+    #[test]
+    fn test_early_end_of_command_3() {
+        check_segmentation(
+            r#"define !macro1(.
+x.
+data list /x 1.
+"#,
+            Syntax::Interactive,
+            &[
+                (Segment::Identifier, "define"),
+                (Segment::Spaces, " "),
+                (Segment::MacroName, "!macro1"),
+                (Segment::Punct, "("),
+                (Segment::EndCommand, "."),
+                (Segment::Newline, "\n"),
+                (Segment::Identifier, "x"),
+                (Segment::EndCommand, "."),
+                (Segment::Newline, "\n"),
+                (Segment::Identifier, "data"),
+                (Segment::Spaces, " "),
+                (Segment::Identifier, "list"),
+                (Segment::Spaces, " "),
+                (Segment::Punct, "/"),
+                (Segment::Identifier, "x"),
+                (Segment::Spaces, " "),
+                (Segment::Number, "1"),
+                (Segment::EndCommand, "."),
+                (Segment::Newline, "\n"),
+            ],
+            &[PromptStyle::First, PromptStyle::First, PromptStyle::First],
+        );
+    }
+
+    #[test]
+    fn test_early_end_of_command_4() {
+        // Notice the command terminator at the end of the `DEFINE` command,
+        // which should not be there and ends it early.
+        check_segmentation(
+            r#"define !macro1.
+data list /x 1.
+"#,
+            Syntax::Interactive,
+            &[
+                (Segment::Identifier, "define"),
+                (Segment::Spaces, " "),
+                (Segment::MacroName, "!macro1"),
+                (Segment::EndCommand, "."),
+                (Segment::Newline, "\n"),
+                (Segment::Identifier, "data"),
+                (Segment::Spaces, " "),
+                (Segment::Identifier, "list"),
+                (Segment::Spaces, " "),
+                (Segment::Punct, "/"),
+                (Segment::Identifier, "x"),
+                (Segment::Spaces, " "),
+                (Segment::Number, "1"),
+                (Segment::EndCommand, "."),
+                (Segment::Newline, "\n"),
+            ],
+            &[PromptStyle::First, PromptStyle::First],
+        );
+    }
+
+    #[test]
+    fn test_missing_enddefine() {
+        check_segmentation(
+            r#"define !macro1()
+content line 1
+content line 2
+"#,
+            Syntax::Interactive,
+            &[
+                (Segment::Identifier, "define"),
+                (Segment::Spaces, " "),
+                (Segment::MacroName, "!macro1"),
+                (Segment::Punct, "("),
+                (Segment::Punct, ")"),
+                (Segment::Newline, "\n"),
+                (Segment::MacroBody, "content line 1"),
+                (Segment::Newline, "\n"),
+                (Segment::MacroBody, "content line 2"),
+                (Segment::Newline, "\n"),
+            ],
+            &[
+                PromptStyle::Define,
+                PromptStyle::Define,
+                PromptStyle::Define,
+            ],
+        );
+    }
+
+    #[test]
+    fn test_missing_enddefine_2() {
+        check_segmentation(
+            r#"define !macro1()
+"#,
+            Syntax::Interactive,
+            &[
+                (Segment::Identifier, "define"),
+                (Segment::Spaces, " "),
+                (Segment::MacroName, "!macro1"),
+                (Segment::Punct, "("),
+                (Segment::Punct, ")"),
+                (Segment::Newline, "\n"),
+            ],
+            &[PromptStyle::Define],
+        );
+    }
+}
+
+#[test]
+fn test_batch_mode() {
+    check_segmentation(
+        r#"first command
+     another line of first command
++  second command
+third command
+
+fourth command.
+   fifth command.
+"#,
+        Syntax::Batch,
+        &[
+            (Segment::Identifier, "first"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "command"),
+            (Segment::Newline, "\n"),
+            (Segment::Spaces, "     "),
+            (Segment::Identifier, "another"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "line"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "of"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "first"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "command"),
+            (Segment::Newline, "\n"),
+            (Segment::StartCommand, "+"),
+            (Segment::Spaces, "  "),
+            (Segment::Identifier, "second"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "command"),
+            (Segment::Newline, "\n"),
+            (Segment::StartCommand, ""),
+            (Segment::Identifier, "third"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "command"),
+            (Segment::Newline, "\n"),
+            (Segment::SeparateCommands, ""),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "fourth"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "command"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::Spaces, "   "),
+            (Segment::Identifier, "fifth"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "command"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+        ],
+        &[
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::First,
+        ],
+    );
+}
+
+#[test]
+fn test_auto_mode() {
+    check_segmentation(
+        r#"command
+     another line of command
+2sls
++  another command
+another line of second command
+data list /x 1
+aggregate.
+print eject.
+twostep cluster
+
+
+fourth command.
+   fifth command.
+"#,
+        Syntax::Auto,
+        &[
+            (Segment::Identifier, "command"),
+            (Segment::Newline, "\n"),
+            (Segment::Spaces, "     "),
+            (Segment::Identifier, "another"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "line"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "of"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "command"),
+            (Segment::Newline, "\n"),
+            (Segment::StartCommand, ""),
+            (Segment::Number, "2"),
+            (Segment::Identifier, "sls"),
+            (Segment::Newline, "\n"),
+            (Segment::StartCommand, "+"),
+            (Segment::Spaces, "  "),
+            (Segment::Identifier, "another"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "command"),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "another"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "line"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "of"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "second"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "command"),
+            (Segment::Newline, "\n"),
+            (Segment::StartCommand, ""),
+            (Segment::Identifier, "data"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "list"),
+            (Segment::Spaces, " "),
+            (Segment::Punct, "/"),
+            (Segment::Identifier, "x"),
+            (Segment::Spaces, " "),
+            (Segment::Number, "1"),
+            (Segment::Newline, "\n"),
+            (Segment::StartCommand, ""),
+            (Segment::Identifier, "aggregate"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "print"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "eject"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "twostep"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "cluster"),
+            (Segment::Newline, "\n"),
+            (Segment::SeparateCommands, ""),
+            (Segment::Newline, "\n"),
+            (Segment::SeparateCommands, ""),
+            (Segment::Newline, "\n"),
+            (Segment::Identifier, "fourth"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "command"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+            (Segment::Spaces, "   "),
+            (Segment::Identifier, "fifth"),
+            (Segment::Spaces, " "),
+            (Segment::Identifier, "command"),
+            (Segment::EndCommand, "."),
+            (Segment::Newline, "\n"),
+        ],
+        &[
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::Later,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::Later,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::First,
+            PromptStyle::First,
+        ],
+    );
+}
index 27cc2b1a55c989ac95021cf74f3edd8a73acf8c8..be0cad9f87b157dc9ff10a6235f5e10573f7c675 100644 (file)
@@ -159,7 +159,7 @@ impl Display for Token {
 
 /// Check that all negative numbers, even -0, get formatted with a leading `-`.
 #[cfg(test)]
-mod test {
+mod tests {
     use crate::lex::token::Token;
 
     #[test]
index 0d6782f142de4fcdc8f3f11895649e15374ff20c..260e5c3e2cc28c8180df51993a65cd9b01807a09 100644 (file)
@@ -42,7 +42,7 @@ fn horz_align_to_pango(horz_align: HorzAlign) -> pango::Alignment {
 }
 
 #[cfg(test)]
-mod test {
+mod tests {
     use crate::output::cairo::{CairoConfig, CairoDriver};
 
     #[test]
index 92133e2c513e2621d266c581629b105fbac5b4e8..2c9f17b307c5ec16e57328b75d4f17e2a46c686c 100644 (file)
@@ -82,7 +82,7 @@ pub mod output;
 
 mod look_xml;
 #[cfg(test)]
-pub mod test;
+pub mod tests;
 mod tlo;
 
 /// Areas of a pivot table for styling purposes.
@@ -2775,7 +2775,7 @@ impl Serialize for MetadataEntry {
 }
 
 #[cfg(test)]
-mod tests {
+mod test {
     use crate::output::pivot::{Display26Adic, MetadataEntry, MetadataValue, Value};
 
     #[test]
index adddcac90c936a54893526e36c1011c95d2f5371..c935125e3161b4e579397f0aff4127ed89064fb8 100644 (file)
@@ -439,7 +439,7 @@ impl<'de> Deserialize<'de> for Dimension {
 }
 
 #[cfg(test)]
-mod test {
+mod tests {
     use std::str::FromStr;
 
     use quick_xml::de::from_str;
diff --git a/rust/pspp/src/output/pivot/test.rs b/rust/pspp/src/output/pivot/test.rs
deleted file mode 100644 (file)
index a69f821..0000000
+++ /dev/null
@@ -1,1445 +0,0 @@
-// PSPP - a program for statistical analysis.
-// Copyright (C) 2025 Free Software Foundation, Inc.
-//
-// This program is free software: you can redistribute it and/or modify it under
-// the terms of the GNU General Public License as published by the Free Software
-// Foundation, either version 3 of the License, or (at your option) any later
-// version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-// FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
-// details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program.  If not, see <http://www.gnu.org/licenses/>.
-
-use std::{fmt::Display, fs::File, path::Path, sync::Arc};
-
-use enum_map::EnumMap;
-
-use crate::output::{
-    cairo::{CairoConfig, CairoDriver},
-    driver::Driver,
-    html::HtmlDriver,
-    pivot::{
-        Area, Axis2, Border, BorderStyle, Class, Color, Dimension, Footnote,
-        FootnoteMarkerPosition, FootnoteMarkerType, Footnotes, Group, HeadingRegion, LabelPosition,
-        Look, PivotTable, RowColBorder, Stroke,
-    },
-    spv::SpvDriver,
-    Details, Item,
-};
-
-use super::{Axis3, Value};
-
-#[test]
-fn color() {
-    assert_eq!("#112233".parse(), Ok(Color::new(0x11, 0x22, 0x33)));
-    assert_eq!("112233".parse(), Ok(Color::new(0x11, 0x22, 0x33)));
-    assert_eq!("rgb(11,22,33)".parse(), Ok(Color::new(11, 22, 33)));
-    assert_eq!(
-        "rgba(11,22,33, 0.25)".parse(),
-        Ok(Color::new(11, 22, 33).with_alpha(64))
-    );
-    assert_eq!("lavender".parse(), Ok(Color::new(230, 230, 250)));
-    assert_eq!("transparent".parse(), Ok(Color::new(0, 0, 0).with_alpha(0)));
-}
-
-fn d1(title: &str, axis: Axis3) -> PivotTable {
-    let dimension = Dimension::new(
-        Group::new("a")
-            .with_label_shown()
-            .with("a1")
-            .with("a2")
-            .with("a3"),
-    );
-    let mut pt = PivotTable::new([(axis, dimension)])
-        .with_title(title)
-        .with_look(Arc::new(test_look()));
-    for i in 0..3 {
-        pt.insert(&[i], Value::new_integer(Some(i as f64)));
-    }
-    pt
-}
-
-#[test]
-fn d1_c() {
-    assert_rendering(
-        "d1_c",
-        &d1("Columns", Axis3::X),
-        "\
-Columns
-╭────────╮
-│    a   │
-├──┬──┬──┤
-│a1│a2│a3│
-├──┼──┼──┤
-│ 0│ 1│ 2│
-╰──┴──┴──╯
-",
-    );
-}
-
-#[test]
-fn d1_r() {
-    assert_rendering(
-        "d1_r",
-        &d1("Rows", Axis3::Y),
-        "\
-Rows
-╭──┬─╮
-│a │ │
-├──┼─┤
-│a1│0│
-│a2│1│
-│a3│2│
-╰──┴─╯
-",
-    );
-}
-
-fn test_look() -> Look {
-    let mut look = Look::default();
-    look.areas[Area::Title].cell_style.horz_align = Some(super::HorzAlign::Left);
-    look.areas[Area::Title].font_style.bold = false;
-    look
-}
-
-fn d2(title: &str, axes: [Axis3; 2], dimension_labels: Option<LabelPosition>) -> PivotTable {
-    let d1 = Dimension::new(
-        Group::new("a")
-            .with_show_label(dimension_labels.is_some())
-            .with("a1")
-            .with("a2")
-            .with("a3"),
-    );
-
-    let d2 = Dimension::new(
-        Group::new("b")
-            .with_show_label(dimension_labels.is_some())
-            .with("b1")
-            .with("b2")
-            .with("b3"),
-    );
-
-    let mut pt = PivotTable::new([(axes[0], d1), (axes[1], d2)]).with_title(title);
-    let mut i = 0;
-    for b in 0..3 {
-        for a in 0..3 {
-            pt.insert(&[a, b], Value::new_integer(Some(i as f64)));
-            i += 1;
-        }
-    }
-    let look = match dimension_labels {
-        Some(position) => test_look().with_row_label_position(position),
-        None => test_look(),
-    };
-    pt.with_look(Arc::new(look))
-}
-
-#[track_caller]
-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) {
-            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") {
-        let writer = File::create(Path::new(&dir).join(name).with_extension("html")).unwrap();
-        HtmlDriver::for_writer(writer).write(&item);
-    }
-
-    let item = Arc::new(Item::new(Details::Table(Box::new(pivot_table.clone()))));
-    if let Some(dir) = std::env::var_os("PSPP_TEST_PDF_DIR") {
-        let config = CairoConfig::new(Path::new(&dir).join(name).with_extension("pdf"));
-        CairoDriver::new(&config).unwrap().write(&item);
-    }
-
-    if let Some(dir) = std::env::var_os("PSPP_TEST_SPV_DIR") {
-        let writer = File::create(Path::new(&dir).join(name).with_extension("spv")).unwrap();
-        SpvDriver::for_writer(writer).write(&item);
-    }
-}
-
-#[test]
-fn d2_cc() {
-    assert_rendering(
-        "d2_cc",
-        &d2("Columns", [Axis3::X, Axis3::X], None),
-        "\
-Columns
-╭────────┬────────┬────────╮
-│   b1   │   b2   │   b3   │
-├──┬──┬──┼──┬──┬──┼──┬──┬──┤
-│a1│a2│a3│a1│a2│a3│a1│a2│a3│
-├──┼──┼──┼──┼──┼──┼──┼──┼──┤
-│ 0│ 1│ 2│ 3│ 4│ 5│ 6│ 7│ 8│
-╰──┴──┴──┴──┴──┴──┴──┴──┴──╯
-",
-    );
-}
-
-#[test]
-fn d2_cc_with_dim_labels() {
-    assert_rendering(
-        "d2_cc_with_dim_labels",
-        &d2("Columns", [Axis3::X, Axis3::X], Some(LabelPosition::Corner)),
-        "\
-Columns
-╭──────────────────────────╮
-│             b            │
-├────────┬────────┬────────┤
-│   b1   │   b2   │   b3   │
-├────────┼────────┼────────┤
-│    a   │    a   │    a   │
-├──┬──┬──┼──┬──┬──┼──┬──┬──┤
-│a1│a2│a3│a1│a2│a3│a1│a2│a3│
-├──┼──┼──┼──┼──┼──┼──┼──┼──┤
-│ 0│ 1│ 2│ 3│ 4│ 5│ 6│ 7│ 8│
-╰──┴──┴──┴──┴──┴──┴──┴──┴──╯
-",
-    );
-}
-
-#[test]
-fn d2_rr() {
-    assert_rendering(
-        "d2_rr",
-        &d2("Rows", [Axis3::Y, Axis3::Y], None),
-        "\
-Rows
-╭─────┬─╮
-│b1 a1│0│
-│   a2│1│
-│   a3│2│
-├─────┼─┤
-│b2 a1│3│
-│   a2│4│
-│   a3│5│
-├─────┼─┤
-│b3 a1│6│
-│   a2│7│
-│   a3│8│
-╰─────┴─╯
-",
-    );
-}
-
-#[test]
-fn d2_rr_with_corner_dim_labels() {
-    assert_rendering(
-        "d2_rr_with_corner_dim_labels",
-        &d2(
-            "Rows - Corner",
-            [Axis3::Y, Axis3::Y],
-            Some(LabelPosition::Corner),
-        ),
-        "\
-Rows - Corner
-╭─────┬─╮
-│b  a │ │
-├─────┼─┤
-│b1 a1│0│
-│   a2│1│
-│   a3│2│
-├─────┼─┤
-│b2 a1│3│
-│   a2│4│
-│   a3│5│
-├─────┼─┤
-│b3 a1│6│
-│   a2│7│
-│   a3│8│
-╰─────┴─╯
-",
-    );
-}
-
-#[test]
-fn d2_rr_with_nested_dim_labels() {
-    assert_rendering(
-        "d2_rr_with_nested_dim_labels",
-        &d2(
-            "Rows - Nested",
-            [Axis3::Y, Axis3::Y],
-            Some(LabelPosition::Nested),
-        ),
-        "\
-Rows - Nested
-╭─────────┬─╮
-│b b1 a a1│0│
-│       a2│1│
-│       a3│2│
-│ ╶───────┼─┤
-│  b2 a a1│3│
-│       a2│4│
-│       a3│5│
-│ ╶───────┼─┤
-│  b3 a a1│6│
-│       a2│7│
-│       a3│8│
-╰─────────┴─╯
-",
-    );
-}
-
-#[test]
-fn d2_cr() {
-    assert_rendering(
-        "d2_cr",
-        &d2("Column x Row", [Axis3::X, Axis3::Y], None),
-        "\
-Column x Row
-╭──┬──┬──┬──╮
-│  │a1│a2│a3│
-├──┼──┼──┼──┤
-│b1│ 0│ 1│ 2│
-│b2│ 3│ 4│ 5│
-│b3│ 6│ 7│ 8│
-╰──┴──┴──┴──╯
-",
-    );
-}
-
-#[test]
-fn d2_cr_with_corner_dim_labels() {
-    assert_rendering(
-        "d2_cr_with_corner_dim_labels",
-        &d2(
-            "Column x Row - Corner",
-            [Axis3::X, Axis3::Y],
-            Some(LabelPosition::Corner),
-        ),
-        "\
-Column x Row - Corner
-╭──┬────────╮
-│  │    a   │
-│  ├──┬──┬──┤
-│b │a1│a2│a3│
-├──┼──┼──┼──┤
-│b1│ 0│ 1│ 2│
-│b2│ 3│ 4│ 5│
-│b3│ 6│ 7│ 8│
-╰──┴──┴──┴──╯
-",
-    );
-}
-
-#[test]
-fn d2_cr_with_nested_dim_labels() {
-    assert_rendering(
-        "d2_cr_with_nested_dim_labels",
-        &d2(
-            "Column x Row - Nested",
-            [Axis3::X, Axis3::Y],
-            Some(LabelPosition::Nested),
-        ),
-        "\
-Column x Row - Nested
-╭────┬────────╮
-│    │    a   │
-│    ├──┬──┬──┤
-│    │a1│a2│a3│
-├────┼──┼──┼──┤
-│b b1│ 0│ 1│ 2│
-│  b2│ 3│ 4│ 5│
-│  b3│ 6│ 7│ 8│
-╰────┴──┴──┴──╯
-",
-    );
-}
-
-#[test]
-fn d2_rc() {
-    assert_rendering(
-        "d2_rc",
-        &d2("Row x Column", [Axis3::Y, Axis3::X], None),
-        "\
-Row x Column
-╭──┬──┬──┬──╮
-│  │b1│b2│b3│
-├──┼──┼──┼──┤
-│a1│ 0│ 3│ 6│
-│a2│ 1│ 4│ 7│
-│a3│ 2│ 5│ 8│
-╰──┴──┴──┴──╯
-",
-    );
-}
-
-#[test]
-fn d2_rc_with_corner_dim_labels() {
-    assert_rendering(
-        "d2_rc_with_corner_dim_labels",
-        &d2(
-            "Row x Column - Corner",
-            [Axis3::Y, Axis3::X],
-            Some(LabelPosition::Corner),
-        ),
-        "\
-Row x Column - Corner
-╭──┬────────╮
-│  │    b   │
-│  ├──┬──┬──┤
-│a │b1│b2│b3│
-├──┼──┼──┼──┤
-│a1│ 0│ 3│ 6│
-│a2│ 1│ 4│ 7│
-│a3│ 2│ 5│ 8│
-╰──┴──┴──┴──╯
-",
-    );
-}
-
-#[test]
-fn d2_rc_with_nested_dim_labels() {
-    assert_rendering(
-        "d2_rc_with_nested_dim_labels",
-        &d2(
-            "Row x Column - Nested",
-            [Axis3::Y, Axis3::X],
-            Some(LabelPosition::Nested),
-        ),
-        "\
-Row x Column - Nested
-╭────┬────────╮
-│    │    b   │
-│    ├──┬──┬──┤
-│    │b1│b2│b3│
-├────┼──┼──┼──┤
-│a a1│ 0│ 3│ 6│
-│  a2│ 1│ 4│ 7│
-│  a3│ 2│ 5│ 8│
-╰────┴──┴──┴──╯
-",
-    );
-}
-
-#[test]
-fn d2_cl() {
-    let pivot_table = d2("Column x b1", [Axis3::X, Axis3::Z], None);
-    assert_rendering(
-        "d2_cl-layer0",
-        &pivot_table,
-        "\
-Column x b1
-b1
-╭──┬──┬──╮
-│a1│a2│a3│
-├──┼──┼──┤
-│ 0│ 1│ 2│
-╰──┴──┴──╯
-",
-    );
-
-    let pivot_table = pivot_table
-        .with_layer(&[1])
-        .with_title(Value::new_text("Column x b2"));
-    assert_rendering(
-        "d2_cl-layer1",
-        &pivot_table,
-        "\
-Column x b2
-b2
-╭──┬──┬──╮
-│a1│a2│a3│
-├──┼──┼──┤
-│ 3│ 4│ 5│
-╰──┴──┴──╯
-",
-    );
-
-    let pivot_table = pivot_table
-        .with_all_layers()
-        .with_title(Value::new_text("Column (All Layers)"));
-    assert_rendering(
-        "d2_cl-all_layers",
-        &pivot_table,
-        "\
-Column (All Layers)
-b1
-╭──┬──┬──╮
-│a1│a2│a3│
-├──┼──┼──┤
-│ 0│ 1│ 2│
-╰──┴──┴──╯
-
-Column (All Layers)
-b2
-╭──┬──┬──╮
-│a1│a2│a3│
-├──┼──┼──┤
-│ 3│ 4│ 5│
-╰──┴──┴──╯
-
-Column (All Layers)
-b3
-╭──┬──┬──╮
-│a1│a2│a3│
-├──┼──┼──┤
-│ 6│ 7│ 8│
-╰──┴──┴──╯
-",
-    );
-}
-
-#[test]
-fn d2_rl() {
-    let pivot_table = d2("Row x b1", [Axis3::Y, Axis3::Z], None);
-    assert_rendering(
-        "d2_rl-layer0",
-        &pivot_table,
-        "\
-Row x b1
-b1
-╭──┬─╮
-│a1│0│
-│a2│1│
-│a3│2│
-╰──┴─╯
-",
-    );
-
-    let pivot_table = pivot_table
-        .with_layer(&[1])
-        .with_title(Value::new_text("Row x b2"));
-    assert_rendering(
-        "d2_rl-layer1",
-        &pivot_table,
-        "\
-Row x b2
-b2
-╭──┬─╮
-│a1│3│
-│a2│4│
-│a3│5│
-╰──┴─╯
-",
-    );
-
-    let pivot_table = pivot_table
-        .with_all_layers()
-        .with_title(Value::new_text("Row (All Layers)"));
-    assert_rendering(
-        "d2_rl-all_layers",
-        &pivot_table,
-        "\
-Row (All Layers)
-b1
-╭──┬─╮
-│a1│0│
-│a2│1│
-│a3│2│
-╰──┴─╯
-
-Row (All Layers)
-b2
-╭──┬─╮
-│a1│3│
-│a2│4│
-│a3│5│
-╰──┴─╯
-
-Row (All Layers)
-b3
-╭──┬─╮
-│a1│6│
-│a2│7│
-│a3│8│
-╰──┴─╯
-",
-    );
-}
-
-#[test]
-fn d3() {
-    let a = (
-        Axis3::Z,
-        Dimension::new(Group::new("a").with("a1").with("a2").with("a3")),
-    );
-    let b = (
-        Axis3::Z,
-        Dimension::new(Group::new("b").with("b1").with("b2").with("b3").with("b4")),
-    );
-    let c = (
-        Axis3::X,
-        Dimension::new(
-            Group::new("c")
-                .with("c1")
-                .with("c2")
-                .with("c3")
-                .with("c4")
-                .with("c5"),
-        ),
-    );
-    let mut pt = PivotTable::new([a, b, c])
-        .with_title("Column x b1 x a1")
-        .with_look(Arc::new(test_look()));
-    let mut i = 0;
-    for c in 0..5 {
-        for b in 0..4 {
-            for a in 0..3 {
-                pt.insert(&[a, b, c], Value::new_integer(Some(i as f64)));
-                i += 1;
-            }
-        }
-    }
-    assert_rendering(
-        "d3-layer0_0",
-        &pt,
-        "\
-Column x b1 x a1
-b1
-a1
-╭──┬──┬──┬──┬──╮
-│c1│c2│c3│c4│c5│
-├──┼──┼──┼──┼──┤
-│ 0│12│24│36│48│
-╰──┴──┴──┴──┴──╯
-",
-    );
-
-    let pt = pt.with_layer(&[0, 1]).with_title("Column x b2 x a1");
-    assert_rendering(
-        "d3-layer0_1",
-        &pt,
-        "\
-Column x b2 x a1
-b2
-a1
-╭──┬──┬──┬──┬──╮
-│c1│c2│c3│c4│c5│
-├──┼──┼──┼──┼──┤
-│ 3│15│27│39│51│
-╰──┴──┴──┴──┴──╯
-",
-    );
-
-    let pt = pt.with_layer(&[1, 2]).with_title("Column x b3 x a2");
-    assert_rendering(
-        "d3-layer1_2",
-        &pt,
-        "\
-Column x b3 x a2
-b3
-a2
-╭──┬──┬──┬──┬──╮
-│c1│c2│c3│c4│c5│
-├──┼──┼──┼──┼──┤
-│ 7│19│31│43│55│
-╰──┴──┴──┴──┴──╯
-",
-    );
-}
-
-#[test]
-fn title_and_caption() {
-    let pivot_table =
-        d2("Title", [Axis3::X, Axis3::Y], None).with_caption(Value::new_text("Caption"));
-    assert_rendering(
-        "title_and_caption",
-        &pivot_table,
-        "\
-Title
-╭──┬──┬──┬──╮
-│  │a1│a2│a3│
-├──┼──┼──┼──┤
-│b1│ 0│ 1│ 2│
-│b2│ 3│ 4│ 5│
-│b3│ 6│ 7│ 8│
-╰──┴──┴──┴──╯
-Caption
-",
-    );
-
-    let pivot_table = pivot_table.with_show_title(false);
-    assert_rendering(
-        "caption",
-        &pivot_table,
-        "\
-╭──┬──┬──┬──╮
-│  │a1│a2│a3│
-├──┼──┼──┼──┤
-│b1│ 0│ 1│ 2│
-│b2│ 3│ 4│ 5│
-│b3│ 6│ 7│ 8│
-╰──┴──┴──┴──╯
-Caption
-",
-    );
-
-    let pivot_table = pivot_table.with_show_caption(false);
-    assert_rendering(
-        "no_title_or_caption",
-        &pivot_table,
-        "\
-╭──┬──┬──┬──╮
-│  │a1│a2│a3│
-├──┼──┼──┼──┤
-│b1│ 0│ 1│ 2│
-│b2│ 3│ 4│ 5│
-│b3│ 6│ 7│ 8│
-╰──┴──┴──┴──╯
-",
-    );
-}
-
-fn footnote_table(show_f0: bool) -> PivotTable {
-    let mut footnotes = Footnotes::new();
-    let f0 = footnotes.push(
-        Footnote::new("First footnote")
-            .with_marker("*")
-            .with_show(show_f0),
-    );
-    let f1 = footnotes.push(Footnote::new("Second footnote"));
-    let a = (
-        Axis3::X,
-        Dimension::new(
-            Group::new(Value::new_text("A").with_footnote(&f0))
-                .with_label_shown()
-                .with(Value::new_text("B").with_footnote(&f1))
-                .with(Value::new_text("C").with_footnote(&f0).with_footnote(&f1)),
-        ),
-    );
-    let d = (
-        Axis3::Y,
-        Dimension::new(
-            Group::new(Value::new_text("D").with_footnote(&f1))
-                .with_label_shown()
-                .with(Value::new_text("E").with_footnote(&f0))
-                .with(Value::new_text("F").with_footnote(&f1).with_footnote(&f0)),
-        ),
-    );
-    let look = test_look().with_row_label_position(LabelPosition::Nested);
-    let mut pt = PivotTable::new([a, d]).with_title(
-        Value::new_text("Pivot Table with Alphabetic Subscript Footnotes").with_footnote(&f0),
-    );
-    pt.insert(&[0, 0], Value::new_number(Some(0.0)));
-    pt.insert(&[1, 0], Value::new_number(Some(1.0)).with_footnote(&f0));
-    pt.insert(&[0, 1], Value::new_number(Some(2.0)).with_footnote(&f1));
-    pt.insert(
-        &[1, 1],
-        Value::new_number(Some(3.0))
-            .with_footnote(&f0)
-            .with_footnote(&f1),
-    );
-    pt.with_look(Arc::new(look))
-        .with_footnotes(footnotes)
-        .with_caption(Value::new_text("Caption").with_footnote(&f0))
-        .with_corner_text(
-            Value::new_text("Corner")
-                .with_footnote(&f0)
-                .with_footnote(&f1),
-        )
-}
-
-#[test]
-fn footnote_alphabetic_subscript() {
-    assert_rendering(
-        "footnote_alphabetic_subscript",
-        &footnote_table(true),
-        "\
-Pivot Table with Alphabetic Subscript Footnotes[*]
-╭────────────┬──────────────────╮
-│            │       A[*]       │
-│            ├───────┬──────────┤
-│Corner[*][b]│  B[b] │  C[*][b] │
-├────────────┼───────┼──────────┤
-│D[b] E[*]   │    .00│   1.00[*]│
-│     F[*][b]│2.00[b]│3.00[*][b]│
-╰────────────┴───────┴──────────╯
-Caption[*]
-*. First footnote
-b. Second footnote
-",
-    );
-}
-
-#[test]
-fn footnote_alphabetic_superscript() {
-    let mut pt = footnote_table(true);
-    let f0 = pt.footnotes.0[0].clone();
-    pt = pt.with_title(
-        Value::new_text("Pivot Table with Alphabetic Superscript Footnotes").with_footnote(&f0),
-    );
-    pt.look_mut().footnote_marker_position = FootnoteMarkerPosition::Superscript;
-    assert_rendering(
-        "footnote_alphabetic_superscript",
-        &pt,
-        "\
-Pivot Table with Alphabetic Superscript Footnotes[*]
-╭────────────┬──────────────────╮
-│            │       A[*]       │
-│            ├───────┬──────────┤
-│Corner[*][b]│  B[b] │  C[*][b] │
-├────────────┼───────┼──────────┤
-│D[b] E[*]   │    .00│   1.00[*]│
-│     F[*][b]│2.00[b]│3.00[*][b]│
-╰────────────┴───────┴──────────╯
-Caption[*]
-*. First footnote
-b. Second footnote
-",
-    );
-}
-
-#[test]
-fn footnote_numeric_subscript() {
-    let mut pt = footnote_table(true);
-    let f0 = pt.footnotes.0[0].clone();
-    pt = pt.with_title(
-        Value::new_text("Pivot Table with Numeric Subscript Footnotes").with_footnote(&f0),
-    );
-    pt.look_mut().footnote_marker_type = FootnoteMarkerType::Numeric;
-    assert_rendering(
-        "footnote_numeric_subscript",
-        &pt,
-        "\
-Pivot Table with Numeric Subscript Footnotes[*]
-╭────────────┬──────────────────╮
-│            │       A[*]       │
-│            ├───────┬──────────┤
-│Corner[*][2]│  B[2] │  C[*][2] │
-├────────────┼───────┼──────────┤
-│D[2] E[*]   │    .00│   1.00[*]│
-│     F[*][2]│2.00[2]│3.00[*][2]│
-╰────────────┴───────┴──────────╯
-Caption[*]
-*. First footnote
-2. Second footnote
-",
-    );
-}
-
-#[test]
-fn footnote_numeric_superscript() {
-    let mut pt = footnote_table(true);
-    let f0 = pt.footnotes.0[0].clone();
-    pt = pt.with_title(
-        Value::new_text("Pivot Table with Numeric Superscript Footnotes").with_footnote(&f0),
-    );
-    pt.look_mut().footnote_marker_type = FootnoteMarkerType::Numeric;
-    pt.look_mut().footnote_marker_position = FootnoteMarkerPosition::Superscript;
-    assert_rendering(
-        "footnote_numeric_superscript",
-        &pt,
-        "\
-Pivot Table with Numeric Superscript Footnotes[*]
-╭────────────┬──────────────────╮
-│            │       A[*]       │
-│            ├───────┬──────────┤
-│Corner[*][2]│  B[2] │  C[*][2] │
-├────────────┼───────┼──────────┤
-│D[2] E[*]   │    .00│   1.00[*]│
-│     F[*][2]│2.00[2]│3.00[*][2]│
-╰────────────┴───────┴──────────╯
-Caption[*]
-*. First footnote
-2. Second footnote
-",
-    );
-}
-
-#[test]
-fn footnote_hidden() {
-    assert_rendering(
-        "footnote_hidden",
-        &footnote_table(false),
-        "\
-Pivot Table with Alphabetic Subscript Footnotes[*]
-╭────────────┬──────────────────╮
-│            │       A[*]       │
-│            ├───────┬──────────┤
-│Corner[*][b]│  B[b] │  C[*][b] │
-├────────────┼───────┼──────────┤
-│D[b] E[*]   │    .00│   1.00[*]│
-│     F[*][b]│2.00[b]│3.00[*][b]│
-╰────────────┴───────┴──────────╯
-Caption[*]
-b. Second footnote
-",
-    );
-}
-
-#[test]
-fn no_dimension() {
-    let pivot_table = PivotTable::new([])
-        .with_title("No Dimensions")
-        .with_look(Arc::new(test_look()));
-    assert_rendering(
-        "no_dimension",
-        &pivot_table,
-        "No Dimensions
-╭╮
-╰╯
-",
-    );
-}
-
-#[test]
-fn empty_dimensions() {
-    let look = Arc::new(test_look().with_omit_empty(false));
-
-    let d1 = (Axis3::X, Dimension::new(Group::new("a")));
-    let pivot_table = PivotTable::new([d1])
-        .with_title("One Empty Dimension")
-        .with_look(look.clone());
-    assert_rendering("one_empty_dimension", &pivot_table, "One Empty Dimension\n");
-
-    let d1 = (Axis3::X, Dimension::new(Group::new("a")));
-    let d2 = (Axis3::X, Dimension::new(Group::new("b").with_label_shown()));
-    let pivot_table = PivotTable::new([d1, d2])
-        .with_title("Two Empty Dimensions")
-        .with_look(look.clone());
-    assert_rendering(
-        "two_empty_dimensions",
-        &pivot_table,
-        "Two Empty Dimensions\n",
-    );
-
-    let d1 = (Axis3::X, Dimension::new(Group::new("a")));
-    let d2 = (Axis3::X, Dimension::new(Group::new("b").with_label_shown()));
-    let d3 = (
-        Axis3::X,
-        Dimension::new(Group::new("c").with("c1").with("c2")),
-    );
-    let pivot_table = PivotTable::new([d1, d2, d3])
-        .with_title("Three Dimensions, Two Empty")
-        .with_look(look.clone());
-    assert_rendering(
-        "three_dimensions_two_empty",
-        &pivot_table,
-        "Three Dimensions, Two Empty\n",
-    );
-}
-
-#[test]
-fn empty_groups() {
-    let d1 = (
-        Axis3::X,
-        Dimension::new(Group::new("a").with("a1").with(Group::new("a2")).with("a3")),
-    );
-
-    let d2 = (
-        Axis3::Y,
-        Dimension::new(Group::new("b").with(Group::new("b1")).with("b2").with("b3")),
-    );
-
-    let mut pt = PivotTable::new([d1, d2]).with_title("Empty Groups");
-    let mut i = 0;
-    for b in 0..2 {
-        for a in 0..2 {
-            pt.insert(&[a, b], Value::new_integer(Some(i as f64)));
-            i += 1;
-        }
-    }
-    let pivot_table = pt.with_look(Arc::new(test_look().with_omit_empty(false)));
-    assert_rendering(
-        "empty_groups",
-        &pivot_table,
-        "\
-Empty Groups
-╭──┬──┬──╮
-│  │a1│a3│
-├──┼──┼──┤
-│b2│ 0│ 1│
-│b3│ 2│ 3│
-╰──┴──┴──╯
-",
-    );
-}
-
-fn d4(
-    title: &str,
-    borders: EnumMap<Border, BorderStyle>,
-    show_dimension_labels: bool,
-) -> PivotTable {
-    let a = (
-        Axis3::X,
-        Dimension::new(
-            Group::new("a")
-                .with_show_label(show_dimension_labels)
-                .with("a1")
-                .with(Group::new("ag1").with("a2").with("a3")),
-        ),
-    );
-    let b = (
-        Axis3::X,
-        Dimension::new(
-            Group::new("b")
-                .with_show_label(show_dimension_labels)
-                .with(Group::new("bg1").with("b1").with("b2"))
-                .with("b3"),
-        ),
-    );
-    let c = (
-        Axis3::Y,
-        Dimension::new(
-            Group::new("c")
-                .with_show_label(show_dimension_labels)
-                .with("c1")
-                .with(Group::new("cg1").with("c2").with("c3")),
-        ),
-    );
-    let d = (
-        Axis3::Y,
-        Dimension::new(
-            Group::new("d")
-                .with_show_label(show_dimension_labels)
-                .with(Group::new("dg1").with("d1").with("d2"))
-                .with("d3"),
-        ),
-    );
-    let mut pivot_table = PivotTable::new([a, b, c, d])
-        .with_title(title)
-        .with_look(Arc::new(test_look().with_borders(borders)));
-    let mut i = 0;
-    for d in 0..3 {
-        for c in 0..3 {
-            for b in 0..3 {
-                for a in 0..3 {
-                    pivot_table.insert(&[a, b, c, d], Value::new_integer(Some(i as f64)));
-                    i += 1;
-                }
-            }
-        }
-    }
-    pivot_table
-}
-
-#[test]
-fn dimension_borders_1() {
-    let pivot_table = d4(
-        "Dimension Borders 1",
-        EnumMap::from_fn(|border| match border {
-            Border::Dimension(RowColBorder(HeadingRegion::Rows, Axis2::X))
-            | Border::Dimension(RowColBorder(HeadingRegion::Columns, Axis2::Y)) => SOLID_BLUE,
-            _ => BorderStyle::none(),
-        }),
-        true,
-    );
-    assert_rendering(
-        "dimension_borders_1",
-        &pivot_table,
-        "\
-Dimension Borders 1
-                           b
-                     bg1       │
-                 b1   │   b2   │   b3
-                  a   │    a   │    a
-                │ ag1 │  │ ag1 │  │ ag1
-d      c      a1│a2 a3│a1│a2 a3│a1│a2 a3
-dg1 d1     c1  0│ 1  2│ 3│ 4  5│ 6│ 7  8
-      ╶─────────┼─────┼──┼─────┼──┼─────
-       cg1 c2  9│10 11│12│13 14│15│16 17
-           c3 18│19 20│21│22 23│24│25 26
-   ╶────────────┼─────┼──┼─────┼──┼─────
-    d2     c1 27│28 29│30│31 32│33│34 35
-      ╶─────────┼─────┼──┼─────┼──┼─────
-       cg1 c2 36│37 38│39│40 41│42│43 44
-           c3 45│46 47│48│49 50│51│52 53
-────────────────┼─────┼──┼─────┼──┼─────
-    d3     c1 54│55 56│57│58 59│60│61 62
-      ╶─────────┼─────┼──┼─────┼──┼─────
-       cg1 c2 63│64 65│66│67 68│69│70 71
-           c3 72│73 74│75│76 77│78│79 80
-",
-    );
-}
-
-#[test]
-fn dimension_borders_2() {
-    let pivot_table = d4(
-        "Dimension Borders 2",
-        EnumMap::from_fn(|border| match border {
-            Border::Dimension(RowColBorder(HeadingRegion::Rows, Axis2::Y))
-            | Border::Dimension(RowColBorder(HeadingRegion::Columns, Axis2::X)) => SOLID_BLUE,
-            _ => BorderStyle::none(),
-        }),
-        true,
-    );
-    assert_rendering(
-        "dimension_borders_2",
-        &pivot_table,
-        "\
-Dimension Borders 2
-                           b
-                     bg1
-                 b1       b2       b3
-             ╶──────────────────────────
-                  a        a        a
-                  ag1      ag1      ag1
-d      c      a1 a2 a3 a1 a2 a3 a1 a2 a3
-dg1 d1│    c1  0  1  2  3  4  5  6  7  8
-      │cg1 c2  9 10 11 12 13 14 15 16 17
-      │    c3 18 19 20 21 22 23 24 25 26
-    d2│    c1 27 28 29 30 31 32 33 34 35
-      │cg1 c2 36 37 38 39 40 41 42 43 44
-      │    c3 45 46 47 48 49 50 51 52 53
-    d3│    c1 54 55 56 57 58 59 60 61 62
-      │cg1 c2 63 64 65 66 67 68 69 70 71
-      │    c3 72 73 74 75 76 77 78 79 80
-",
-    );
-}
-
-#[test]
-fn category_borders_1() {
-    let pivot_table = d4(
-        "Category Borders 1",
-        EnumMap::from_fn(|border| match border {
-            Border::Category(RowColBorder(HeadingRegion::Rows, Axis2::X))
-            | Border::Category(RowColBorder(HeadingRegion::Columns, Axis2::Y)) => DASHED_RED,
-            _ => BorderStyle::none(),
-        }),
-        true,
-    );
-    assert_rendering(
-        "category_borders_1",
-        &pivot_table,
-        "\
-Category Borders 1
-                           b
-                     bg1       ┊
-                 b1   ┊   b2   ┊   b3
-                  a   ┊    a   ┊    a
-                ┊ ag1 ┊  ┊ ag1 ┊  ┊ ag1
-d      c      a1┊a2┊a3┊a1┊a2┊a3┊a1┊a2┊a3
-dg1 d1     c1  0┊ 1┊ 2┊ 3┊ 4┊ 5┊ 6┊ 7┊ 8
-      ╌╌╌╌╌╌╌╌╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌
-       cg1 c2  9┊10┊11┊12┊13┊14┊15┊16┊17
-          ╌╌╌╌╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌
-           c3 18┊19┊20┊21┊22┊23┊24┊25┊26
-   ╌╌╌╌╌╌╌╌╌╌╌╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌
-    d2     c1 27┊28┊29┊30┊31┊32┊33┊34┊35
-      ╌╌╌╌╌╌╌╌╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌
-       cg1 c2 36┊37┊38┊39┊40┊41┊42┊43┊44
-          ╌╌╌╌╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌
-           c3 45┊46┊47┊48┊49┊50┊51┊52┊53
-╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌
-    d3     c1 54┊55┊56┊57┊58┊59┊60┊61┊62
-      ╌╌╌╌╌╌╌╌╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌
-       cg1 c2 63┊64┊65┊66┊67┊68┊69┊70┊71
-          ╌╌╌╌╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌
-           c3 72┊73┊74┊75┊76┊77┊78┊79┊80
-",
-    );
-}
-
-#[test]
-fn category_borders_2() {
-    let pivot_table = d4(
-        "Category Borders 2",
-        EnumMap::from_fn(|border| match border {
-            Border::Category(RowColBorder(HeadingRegion::Rows, Axis2::Y))
-            | Border::Category(RowColBorder(HeadingRegion::Columns, Axis2::X)) => DASHED_RED,
-            _ => BorderStyle::none(),
-        }),
-        true,
-    );
-    assert_rendering(
-        "category_borders_2",
-        &pivot_table,
-        "\
-Category Borders 2
-                           b
-             ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
-                     bg1
-             ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
-                 b1       b2       b3
-             ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
-                  a        a        a
-             ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
-                  ag1      ag1      ag1
-                ╌╌╌╌╌╌╌  ╌╌╌╌╌╌╌  ╌╌╌╌╌╌
-d      c      a1 a2 a3 a1 a2 a3 a1 a2 a3
-dg1┊d1┊    c1  0  1  2  3  4  5  6  7  8
-   ┊  ┊cg1┊c2  9 10 11 12 13 14 15 16 17
-   ┊  ┊   ┊c3 18 19 20 21 22 23 24 25 26
-   ┊d2┊    c1 27 28 29 30 31 32 33 34 35
-   ┊  ┊cg1┊c2 36 37 38 39 40 41 42 43 44
-   ┊  ┊   ┊c3 45 46 47 48 49 50 51 52 53
-    d3┊    c1 54 55 56 57 58 59 60 61 62
-      ┊cg1┊c2 63 64 65 66 67 68 69 70 71
-      ┊   ┊c3 72 73 74 75 76 77 78 79 80
-",
-    );
-}
-
-#[test]
-fn category_and_dimension_borders_1() {
-    let pivot_table = d4(
-        "Category and Dimension Borders 1",
-        EnumMap::from_fn(|border| match border {
-            Border::Dimension(RowColBorder(HeadingRegion::Rows, Axis2::X))
-            | Border::Dimension(RowColBorder(HeadingRegion::Columns, Axis2::Y)) => SOLID_BLUE,
-            Border::Category(RowColBorder(HeadingRegion::Rows, Axis2::X))
-            | Border::Category(RowColBorder(HeadingRegion::Columns, Axis2::Y)) => DASHED_RED,
-            _ => BorderStyle::none(),
-        }),
-        true,
-    );
-    assert_rendering(
-        "category_and_dimension_borders_1",
-        &pivot_table,
-        "\
-Category and Dimension Borders 1
-                           b
-                     bg1       │
-                 b1   │   b2   │   b3
-                  a   │    a   │    a
-                │ ag1 │  │ ag1 │  │ ag1
-d      c      a1│a2┊a3│a1│a2┊a3│a1│a2┊a3
-dg1 d1     c1  0│ 1┊ 2│ 3│ 4┊ 5│ 6│ 7┊ 8
-      ╶─────────┼──┼──┼──┼──┼──┼──┼──┼──
-       cg1 c2  9│10┊11│12│13┊14│15│16┊17
-          ╌╌╌╌╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌
-           c3 18│19┊20│21│22┊23│24│25┊26
-   ╶────────────┼──┼──┼──┼──┼──┼──┼──┼──
-    d2     c1 27│28┊29│30│31┊32│33│34┊35
-      ╶─────────┼──┼──┼──┼──┼──┼──┼──┼──
-       cg1 c2 36│37┊38│39│40┊41│42│43┊44
-          ╌╌╌╌╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌
-           c3 45│46┊47│48│49┊50│51│52┊53
-────────────────┼──┼──┼──┼──┼──┼──┼──┼──
-    d3     c1 54│55┊56│57│58┊59│60│61┊62
-      ╶─────────┼──┼──┼──┼──┼──┼──┼──┼──
-       cg1 c2 63│64┊65│66│67┊68│69│70┊71
-          ╌╌╌╌╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌
-           c3 72│73┊74│75│76┊77│78│79┊80
-",
-    );
-}
-
-#[test]
-fn category_and_dimension_borders_2() {
-    let pivot_table = d4(
-        "Category and Dimension Borders 2",
-        EnumMap::from_fn(|border| match border {
-            Border::Dimension(RowColBorder(HeadingRegion::Rows, Axis2::Y))
-            | Border::Dimension(RowColBorder(HeadingRegion::Columns, Axis2::X)) => SOLID_BLUE,
-            Border::Category(RowColBorder(HeadingRegion::Rows, Axis2::Y))
-            | Border::Category(RowColBorder(HeadingRegion::Columns, Axis2::X)) => DASHED_RED,
-            _ => BorderStyle::none(),
-        }),
-        true,
-    );
-    assert_rendering(
-        "category_and_dimension_borders_2",
-        &pivot_table,
-        "\
-Category and Dimension Borders 2
-                           b
-             ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
-                     bg1
-             ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
-                 b1       b2       b3
-             ╶──────────────────────────
-                  a        a        a
-             ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
-                  ag1      ag1      ag1
-                ╌╌╌╌╌╌╌  ╌╌╌╌╌╌╌  ╌╌╌╌╌╌
-d      c      a1 a2 a3 a1 a2 a3 a1 a2 a3
-dg1┊d1│    c1  0  1  2  3  4  5  6  7  8
-   ┊  │cg1┊c2  9 10 11 12 13 14 15 16 17
-   ┊  │   ┊c3 18 19 20 21 22 23 24 25 26
-   ┊d2│    c1 27 28 29 30 31 32 33 34 35
-   ┊  │cg1┊c2 36 37 38 39 40 41 42 43 44
-   ┊  │   ┊c3 45 46 47 48 49 50 51 52 53
-    d3│    c1 54 55 56 57 58 59 60 61 62
-      │cg1┊c2 63 64 65 66 67 68 69 70 71
-      │   ┊c3 72 73 74 75 76 77 78 79 80
-",
-    );
-}
-
-const SOLID_BLUE: BorderStyle = BorderStyle {
-    stroke: Stroke::Solid,
-    color: Color::BLUE,
-};
-
-const DASHED_RED: BorderStyle = BorderStyle {
-    stroke: Stroke::Dashed,
-    color: Color::RED,
-};
-
-#[test]
-fn category_and_dimension_borders_3() {
-    let pivot_table = d4(
-        "Category and Dimension Borders 3",
-        EnumMap::from_fn(|border| match border {
-            Border::Dimension(_) => SOLID_BLUE,
-            Border::Category(_) => DASHED_RED,
-            _ => BorderStyle::none(),
-        }),
-        false,
-    );
-    assert_rendering(
-        "category_and_dimension_borders_3",
-        &pivot_table,
-        "\
-Category and Dimension Borders 3
-                     bg1       │
-             ╌╌╌╌╌╌╌╌╌┬╌╌╌╌╌╌╌╌┤
-                 b1   │   b2   │   b3
-             ╶──┬─────┼──┬─────┼──┬─────
-                │ ag1 │  │ ag1 │  │ ag1
-                ├╌╌┬╌╌┤  ├╌╌┬╌╌┤  ├╌╌┬╌╌
-              a1│a2┊a3│a1│a2┊a3│a1│a2┊a3
-dg1┊d1│    c1  0│ 1┊ 2│ 3│ 4┊ 5│ 6│ 7┊ 8
-   ┊  ├───┬─────┼──┼──┼──┼──┼──┼──┼──┼──
-   ┊  │cg1┊c2  9│10┊11│12│13┊14│15│16┊17
-   ┊  │   ├╌╌╌╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌
-   ┊  │   ┊c3 18│19┊20│21│22┊23│24│25┊26
-   ├──┼───┴─────┼──┼──┼──┼──┼──┼──┼──┼──
-   ┊d2│    c1 27│28┊29│30│31┊32│33│34┊35
-   ┊  ├───┬─────┼──┼──┼──┼──┼──┼──┼──┼──
-   ┊  │cg1┊c2 36│37┊38│39│40┊41│42│43┊44
-   ┊  │   ├╌╌╌╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌
-   ┊  │   ┊c3 45│46┊47│48│49┊50│51│52┊53
-───┴──┼───┴─────┼──┼──┼──┼──┼──┼──┼──┼──
-    d3│    c1 54│55┊56│57│58┊59│60│61┊62
-      ├───┬─────┼──┼──┼──┼──┼──┼──┼──┼──
-      │cg1┊c2 63│64┊65│66│67┊68│69│70┊71
-      │   ├╌╌╌╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌
-      │   ┊c3 72│73┊74│75│76┊77│78│79┊80
-",
-    );
-}
-
-#[test]
-fn small_numbers() {
-    let exponent = (
-        Axis3::Y,
-        Dimension::new(
-            Group::new("exponent")
-                .with("0")
-                .with("-1")
-                .with("-2")
-                .with("-3")
-                .with("-4")
-                .with("-5")
-                .with("-6")
-                .with("-7")
-                .with("-8")
-                .with("-9")
-                .with_label_shown(),
-        ),
-    );
-    let sign = (
-        Axis3::X,
-        Dimension::new(
-            Group::new("sign")
-                .with("positive")
-                .with("negative")
-                .with_label_shown(),
-        ),
-    );
-    let rc = (
-        Axis3::X,
-        Dimension::new(
-            Group::new("result class")
-                .with("general")
-                .with("specific")
-                .with_label_shown(),
-        ),
-    );
-    let mut pt = PivotTable::new([exponent, sign, rc]).with_title("small numbers");
-    pt.insert_number(&[0, 0, 0], Some(1.0), Class::Other);
-    pt.insert_number(&[1, 0, 0], Some(0.1), Class::Other);
-    pt.insert_number(&[2, 0, 0], Some(0.01), Class::Other);
-    pt.insert_number(&[3, 0, 0], Some(0.001), Class::Other);
-    pt.insert_number(&[4, 0, 0], Some(0.0001), Class::Other);
-    pt.insert_number(&[5, 0, 0], Some(0.00001), Class::Other);
-    pt.insert_number(&[6, 0, 0], Some(0.000001), Class::Other);
-    pt.insert_number(&[7, 0, 0], Some(0.0000001), Class::Other);
-    pt.insert_number(&[8, 0, 0], Some(0.00000001), Class::Other);
-    pt.insert_number(&[9, 0, 0], Some(0.000000001), Class::Other);
-    pt.insert_number(&[0, 0, 1], Some(-1.0), Class::Residual);
-    pt.insert_number(&[1, 0, 1], Some(-0.1), Class::Residual);
-    pt.insert_number(&[2, 0, 1], Some(-0.01), Class::Residual);
-    pt.insert_number(&[3, 0, 1], Some(-0.001), Class::Residual);
-    pt.insert_number(&[4, 0, 1], Some(-0.0001), Class::Residual);
-    pt.insert_number(&[5, 0, 1], Some(-0.00001), Class::Residual);
-    pt.insert_number(&[6, 0, 1], Some(-0.000001), Class::Residual);
-    pt.insert_number(&[7, 0, 1], Some(-0.0000001), Class::Residual);
-    pt.insert_number(&[8, 0, 1], Some(-0.00000001), Class::Residual);
-    pt.insert_number(&[9, 0, 1], Some(-0.000000001), Class::Residual);
-    pt.insert_number(&[0, 1, 0], Some(1.0), Class::Other);
-    pt.insert_number(&[1, 1, 0], Some(0.1), Class::Other);
-    pt.insert_number(&[2, 1, 0], Some(0.01), Class::Other);
-    pt.insert_number(&[3, 1, 0], Some(0.001), Class::Other);
-    pt.insert_number(&[4, 1, 0], Some(0.0001), Class::Other);
-    pt.insert_number(&[5, 1, 0], Some(0.00001), Class::Other);
-    pt.insert_number(&[6, 1, 0], Some(0.000001), Class::Other);
-    pt.insert_number(&[7, 1, 0], Some(0.0000001), Class::Other);
-    pt.insert_number(&[8, 1, 0], Some(0.00000001), Class::Other);
-    pt.insert_number(&[9, 1, 0], Some(0.000000001), Class::Other);
-    pt.insert_number(&[0, 1, 1], Some(-1.0), Class::Residual);
-    pt.insert_number(&[1, 1, 1], Some(-0.1), Class::Residual);
-    pt.insert_number(&[2, 1, 1], Some(-0.01), Class::Residual);
-    pt.insert_number(&[3, 1, 1], Some(-0.001), Class::Residual);
-    pt.insert_number(&[4, 1, 1], Some(-0.0001), Class::Residual);
-    pt.insert_number(&[5, 1, 1], Some(-0.00001), Class::Residual);
-    pt.insert_number(&[6, 1, 1], Some(-0.000001), Class::Residual);
-    pt.insert_number(&[7, 1, 1], Some(-0.0000001), Class::Residual);
-    pt.insert_number(&[8, 1, 1], Some(-0.00000001), Class::Residual);
-    pt.insert_number(&[9, 1, 1], Some(-0.000000001), Class::Residual);
-    let pivot_table = pt.with_look(Arc::new(test_look()));
-    assert_rendering(
-        "small_numbers",
-        &pivot_table,
-        "\
-small numbers
-╭────────┬─────────────────────────────────────╮
-│        │             result class            │
-│        ├───────────────────┬─────────────────┤
-│        │      general      │     specific    │
-│        ├───────────────────┼─────────────────┤
-│        │        sign       │       sign      │
-│        ├─────────┬─────────┼────────┬────────┤
-│exponent│ positive│ negative│positive│negative│
-├────────┼─────────┼─────────┼────────┼────────┤
-│0       │     1.00│     1.00│   -1.00│   -1.00│
-│-1      │      .10│      .10│    -.10│    -.10│
-│-2      │      .01│      .01│    -.01│    -.01│
-│-3      │      .00│      .00│     .00│     .00│
-│-4      │      .00│      .00│     .00│     .00│
-│-5      │1.00E-005│1.00E-005│     .00│     .00│
-│-6      │1.00E-006│1.00E-006│     .00│     .00│
-│-7      │1.00E-007│1.00E-007│     .00│     .00│
-│-8      │1.00E-008│1.00E-008│     .00│     .00│
-│-9      │1.00E-009│1.00E-009│     .00│     .00│
-╰────────┴─────────┴─────────┴────────┴────────╯
-",
-    );
-}
diff --git a/rust/pspp/src/output/pivot/tests.rs b/rust/pspp/src/output/pivot/tests.rs
new file mode 100644 (file)
index 0000000..a69f821
--- /dev/null
@@ -0,0 +1,1445 @@
+// PSPP - a program for statistical analysis.
+// Copyright (C) 2025 Free Software Foundation, Inc.
+//
+// This program is free software: you can redistribute it and/or modify it under
+// the terms of the GNU General Public License as published by the Free Software
+// Foundation, either version 3 of the License, or (at your option) any later
+// version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+// FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+// details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program.  If not, see <http://www.gnu.org/licenses/>.
+
+use std::{fmt::Display, fs::File, path::Path, sync::Arc};
+
+use enum_map::EnumMap;
+
+use crate::output::{
+    cairo::{CairoConfig, CairoDriver},
+    driver::Driver,
+    html::HtmlDriver,
+    pivot::{
+        Area, Axis2, Border, BorderStyle, Class, Color, Dimension, Footnote,
+        FootnoteMarkerPosition, FootnoteMarkerType, Footnotes, Group, HeadingRegion, LabelPosition,
+        Look, PivotTable, RowColBorder, Stroke,
+    },
+    spv::SpvDriver,
+    Details, Item,
+};
+
+use super::{Axis3, Value};
+
+#[test]
+fn color() {
+    assert_eq!("#112233".parse(), Ok(Color::new(0x11, 0x22, 0x33)));
+    assert_eq!("112233".parse(), Ok(Color::new(0x11, 0x22, 0x33)));
+    assert_eq!("rgb(11,22,33)".parse(), Ok(Color::new(11, 22, 33)));
+    assert_eq!(
+        "rgba(11,22,33, 0.25)".parse(),
+        Ok(Color::new(11, 22, 33).with_alpha(64))
+    );
+    assert_eq!("lavender".parse(), Ok(Color::new(230, 230, 250)));
+    assert_eq!("transparent".parse(), Ok(Color::new(0, 0, 0).with_alpha(0)));
+}
+
+fn d1(title: &str, axis: Axis3) -> PivotTable {
+    let dimension = Dimension::new(
+        Group::new("a")
+            .with_label_shown()
+            .with("a1")
+            .with("a2")
+            .with("a3"),
+    );
+    let mut pt = PivotTable::new([(axis, dimension)])
+        .with_title(title)
+        .with_look(Arc::new(test_look()));
+    for i in 0..3 {
+        pt.insert(&[i], Value::new_integer(Some(i as f64)));
+    }
+    pt
+}
+
+#[test]
+fn d1_c() {
+    assert_rendering(
+        "d1_c",
+        &d1("Columns", Axis3::X),
+        "\
+Columns
+╭────────╮
+│    a   │
+├──┬──┬──┤
+│a1│a2│a3│
+├──┼──┼──┤
+│ 0│ 1│ 2│
+╰──┴──┴──╯
+",
+    );
+}
+
+#[test]
+fn d1_r() {
+    assert_rendering(
+        "d1_r",
+        &d1("Rows", Axis3::Y),
+        "\
+Rows
+╭──┬─╮
+│a │ │
+├──┼─┤
+│a1│0│
+│a2│1│
+│a3│2│
+╰──┴─╯
+",
+    );
+}
+
+fn test_look() -> Look {
+    let mut look = Look::default();
+    look.areas[Area::Title].cell_style.horz_align = Some(super::HorzAlign::Left);
+    look.areas[Area::Title].font_style.bold = false;
+    look
+}
+
+fn d2(title: &str, axes: [Axis3; 2], dimension_labels: Option<LabelPosition>) -> PivotTable {
+    let d1 = Dimension::new(
+        Group::new("a")
+            .with_show_label(dimension_labels.is_some())
+            .with("a1")
+            .with("a2")
+            .with("a3"),
+    );
+
+    let d2 = Dimension::new(
+        Group::new("b")
+            .with_show_label(dimension_labels.is_some())
+            .with("b1")
+            .with("b2")
+            .with("b3"),
+    );
+
+    let mut pt = PivotTable::new([(axes[0], d1), (axes[1], d2)]).with_title(title);
+    let mut i = 0;
+    for b in 0..3 {
+        for a in 0..3 {
+            pt.insert(&[a, b], Value::new_integer(Some(i as f64)));
+            i += 1;
+        }
+    }
+    let look = match dimension_labels {
+        Some(position) => test_look().with_row_label_position(position),
+        None => test_look(),
+    };
+    pt.with_look(Arc::new(look))
+}
+
+#[track_caller]
+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) {
+            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") {
+        let writer = File::create(Path::new(&dir).join(name).with_extension("html")).unwrap();
+        HtmlDriver::for_writer(writer).write(&item);
+    }
+
+    let item = Arc::new(Item::new(Details::Table(Box::new(pivot_table.clone()))));
+    if let Some(dir) = std::env::var_os("PSPP_TEST_PDF_DIR") {
+        let config = CairoConfig::new(Path::new(&dir).join(name).with_extension("pdf"));
+        CairoDriver::new(&config).unwrap().write(&item);
+    }
+
+    if let Some(dir) = std::env::var_os("PSPP_TEST_SPV_DIR") {
+        let writer = File::create(Path::new(&dir).join(name).with_extension("spv")).unwrap();
+        SpvDriver::for_writer(writer).write(&item);
+    }
+}
+
+#[test]
+fn d2_cc() {
+    assert_rendering(
+        "d2_cc",
+        &d2("Columns", [Axis3::X, Axis3::X], None),
+        "\
+Columns
+╭────────┬────────┬────────╮
+│   b1   │   b2   │   b3   │
+├──┬──┬──┼──┬──┬──┼──┬──┬──┤
+│a1│a2│a3│a1│a2│a3│a1│a2│a3│
+├──┼──┼──┼──┼──┼──┼──┼──┼──┤
+│ 0│ 1│ 2│ 3│ 4│ 5│ 6│ 7│ 8│
+╰──┴──┴──┴──┴──┴──┴──┴──┴──╯
+",
+    );
+}
+
+#[test]
+fn d2_cc_with_dim_labels() {
+    assert_rendering(
+        "d2_cc_with_dim_labels",
+        &d2("Columns", [Axis3::X, Axis3::X], Some(LabelPosition::Corner)),
+        "\
+Columns
+╭──────────────────────────╮
+│             b            │
+├────────┬────────┬────────┤
+│   b1   │   b2   │   b3   │
+├────────┼────────┼────────┤
+│    a   │    a   │    a   │
+├──┬──┬──┼──┬──┬──┼──┬──┬──┤
+│a1│a2│a3│a1│a2│a3│a1│a2│a3│
+├──┼──┼──┼──┼──┼──┼──┼──┼──┤
+│ 0│ 1│ 2│ 3│ 4│ 5│ 6│ 7│ 8│
+╰──┴──┴──┴──┴──┴──┴──┴──┴──╯
+",
+    );
+}
+
+#[test]
+fn d2_rr() {
+    assert_rendering(
+        "d2_rr",
+        &d2("Rows", [Axis3::Y, Axis3::Y], None),
+        "\
+Rows
+╭─────┬─╮
+│b1 a1│0│
+│   a2│1│
+│   a3│2│
+├─────┼─┤
+│b2 a1│3│
+│   a2│4│
+│   a3│5│
+├─────┼─┤
+│b3 a1│6│
+│   a2│7│
+│   a3│8│
+╰─────┴─╯
+",
+    );
+}
+
+#[test]
+fn d2_rr_with_corner_dim_labels() {
+    assert_rendering(
+        "d2_rr_with_corner_dim_labels",
+        &d2(
+            "Rows - Corner",
+            [Axis3::Y, Axis3::Y],
+            Some(LabelPosition::Corner),
+        ),
+        "\
+Rows - Corner
+╭─────┬─╮
+│b  a │ │
+├─────┼─┤
+│b1 a1│0│
+│   a2│1│
+│   a3│2│
+├─────┼─┤
+│b2 a1│3│
+│   a2│4│
+│   a3│5│
+├─────┼─┤
+│b3 a1│6│
+│   a2│7│
+│   a3│8│
+╰─────┴─╯
+",
+    );
+}
+
+#[test]
+fn d2_rr_with_nested_dim_labels() {
+    assert_rendering(
+        "d2_rr_with_nested_dim_labels",
+        &d2(
+            "Rows - Nested",
+            [Axis3::Y, Axis3::Y],
+            Some(LabelPosition::Nested),
+        ),
+        "\
+Rows - Nested
+╭─────────┬─╮
+│b b1 a a1│0│
+│       a2│1│
+│       a3│2│
+│ ╶───────┼─┤
+│  b2 a a1│3│
+│       a2│4│
+│       a3│5│
+│ ╶───────┼─┤
+│  b3 a a1│6│
+│       a2│7│
+│       a3│8│
+╰─────────┴─╯
+",
+    );
+}
+
+#[test]
+fn d2_cr() {
+    assert_rendering(
+        "d2_cr",
+        &d2("Column x Row", [Axis3::X, Axis3::Y], None),
+        "\
+Column x Row
+╭──┬──┬──┬──╮
+│  │a1│a2│a3│
+├──┼──┼──┼──┤
+│b1│ 0│ 1│ 2│
+│b2│ 3│ 4│ 5│
+│b3│ 6│ 7│ 8│
+╰──┴──┴──┴──╯
+",
+    );
+}
+
+#[test]
+fn d2_cr_with_corner_dim_labels() {
+    assert_rendering(
+        "d2_cr_with_corner_dim_labels",
+        &d2(
+            "Column x Row - Corner",
+            [Axis3::X, Axis3::Y],
+            Some(LabelPosition::Corner),
+        ),
+        "\
+Column x Row - Corner
+╭──┬────────╮
+│  │    a   │
+│  ├──┬──┬──┤
+│b │a1│a2│a3│
+├──┼──┼──┼──┤
+│b1│ 0│ 1│ 2│
+│b2│ 3│ 4│ 5│
+│b3│ 6│ 7│ 8│
+╰──┴──┴──┴──╯
+",
+    );
+}
+
+#[test]
+fn d2_cr_with_nested_dim_labels() {
+    assert_rendering(
+        "d2_cr_with_nested_dim_labels",
+        &d2(
+            "Column x Row - Nested",
+            [Axis3::X, Axis3::Y],
+            Some(LabelPosition::Nested),
+        ),
+        "\
+Column x Row - Nested
+╭────┬────────╮
+│    │    a   │
+│    ├──┬──┬──┤
+│    │a1│a2│a3│
+├────┼──┼──┼──┤
+│b b1│ 0│ 1│ 2│
+│  b2│ 3│ 4│ 5│
+│  b3│ 6│ 7│ 8│
+╰────┴──┴──┴──╯
+",
+    );
+}
+
+#[test]
+fn d2_rc() {
+    assert_rendering(
+        "d2_rc",
+        &d2("Row x Column", [Axis3::Y, Axis3::X], None),
+        "\
+Row x Column
+╭──┬──┬──┬──╮
+│  │b1│b2│b3│
+├──┼──┼──┼──┤
+│a1│ 0│ 3│ 6│
+│a2│ 1│ 4│ 7│
+│a3│ 2│ 5│ 8│
+╰──┴──┴──┴──╯
+",
+    );
+}
+
+#[test]
+fn d2_rc_with_corner_dim_labels() {
+    assert_rendering(
+        "d2_rc_with_corner_dim_labels",
+        &d2(
+            "Row x Column - Corner",
+            [Axis3::Y, Axis3::X],
+            Some(LabelPosition::Corner),
+        ),
+        "\
+Row x Column - Corner
+╭──┬────────╮
+│  │    b   │
+│  ├──┬──┬──┤
+│a │b1│b2│b3│
+├──┼──┼──┼──┤
+│a1│ 0│ 3│ 6│
+│a2│ 1│ 4│ 7│
+│a3│ 2│ 5│ 8│
+╰──┴──┴──┴──╯
+",
+    );
+}
+
+#[test]
+fn d2_rc_with_nested_dim_labels() {
+    assert_rendering(
+        "d2_rc_with_nested_dim_labels",
+        &d2(
+            "Row x Column - Nested",
+            [Axis3::Y, Axis3::X],
+            Some(LabelPosition::Nested),
+        ),
+        "\
+Row x Column - Nested
+╭────┬────────╮
+│    │    b   │
+│    ├──┬──┬──┤
+│    │b1│b2│b3│
+├────┼──┼──┼──┤
+│a a1│ 0│ 3│ 6│
+│  a2│ 1│ 4│ 7│
+│  a3│ 2│ 5│ 8│
+╰────┴──┴──┴──╯
+",
+    );
+}
+
+#[test]
+fn d2_cl() {
+    let pivot_table = d2("Column x b1", [Axis3::X, Axis3::Z], None);
+    assert_rendering(
+        "d2_cl-layer0",
+        &pivot_table,
+        "\
+Column x b1
+b1
+╭──┬──┬──╮
+│a1│a2│a3│
+├──┼──┼──┤
+│ 0│ 1│ 2│
+╰──┴──┴──╯
+",
+    );
+
+    let pivot_table = pivot_table
+        .with_layer(&[1])
+        .with_title(Value::new_text("Column x b2"));
+    assert_rendering(
+        "d2_cl-layer1",
+        &pivot_table,
+        "\
+Column x b2
+b2
+╭──┬──┬──╮
+│a1│a2│a3│
+├──┼──┼──┤
+│ 3│ 4│ 5│
+╰──┴──┴──╯
+",
+    );
+
+    let pivot_table = pivot_table
+        .with_all_layers()
+        .with_title(Value::new_text("Column (All Layers)"));
+    assert_rendering(
+        "d2_cl-all_layers",
+        &pivot_table,
+        "\
+Column (All Layers)
+b1
+╭──┬──┬──╮
+│a1│a2│a3│
+├──┼──┼──┤
+│ 0│ 1│ 2│
+╰──┴──┴──╯
+
+Column (All Layers)
+b2
+╭──┬──┬──╮
+│a1│a2│a3│
+├──┼──┼──┤
+│ 3│ 4│ 5│
+╰──┴──┴──╯
+
+Column (All Layers)
+b3
+╭──┬──┬──╮
+│a1│a2│a3│
+├──┼──┼──┤
+│ 6│ 7│ 8│
+╰──┴──┴──╯
+",
+    );
+}
+
+#[test]
+fn d2_rl() {
+    let pivot_table = d2("Row x b1", [Axis3::Y, Axis3::Z], None);
+    assert_rendering(
+        "d2_rl-layer0",
+        &pivot_table,
+        "\
+Row x b1
+b1
+╭──┬─╮
+│a1│0│
+│a2│1│
+│a3│2│
+╰──┴─╯
+",
+    );
+
+    let pivot_table = pivot_table
+        .with_layer(&[1])
+        .with_title(Value::new_text("Row x b2"));
+    assert_rendering(
+        "d2_rl-layer1",
+        &pivot_table,
+        "\
+Row x b2
+b2
+╭──┬─╮
+│a1│3│
+│a2│4│
+│a3│5│
+╰──┴─╯
+",
+    );
+
+    let pivot_table = pivot_table
+        .with_all_layers()
+        .with_title(Value::new_text("Row (All Layers)"));
+    assert_rendering(
+        "d2_rl-all_layers",
+        &pivot_table,
+        "\
+Row (All Layers)
+b1
+╭──┬─╮
+│a1│0│
+│a2│1│
+│a3│2│
+╰──┴─╯
+
+Row (All Layers)
+b2
+╭──┬─╮
+│a1│3│
+│a2│4│
+│a3│5│
+╰──┴─╯
+
+Row (All Layers)
+b3
+╭──┬─╮
+│a1│6│
+│a2│7│
+│a3│8│
+╰──┴─╯
+",
+    );
+}
+
+#[test]
+fn d3() {
+    let a = (
+        Axis3::Z,
+        Dimension::new(Group::new("a").with("a1").with("a2").with("a3")),
+    );
+    let b = (
+        Axis3::Z,
+        Dimension::new(Group::new("b").with("b1").with("b2").with("b3").with("b4")),
+    );
+    let c = (
+        Axis3::X,
+        Dimension::new(
+            Group::new("c")
+                .with("c1")
+                .with("c2")
+                .with("c3")
+                .with("c4")
+                .with("c5"),
+        ),
+    );
+    let mut pt = PivotTable::new([a, b, c])
+        .with_title("Column x b1 x a1")
+        .with_look(Arc::new(test_look()));
+    let mut i = 0;
+    for c in 0..5 {
+        for b in 0..4 {
+            for a in 0..3 {
+                pt.insert(&[a, b, c], Value::new_integer(Some(i as f64)));
+                i += 1;
+            }
+        }
+    }
+    assert_rendering(
+        "d3-layer0_0",
+        &pt,
+        "\
+Column x b1 x a1
+b1
+a1
+╭──┬──┬──┬──┬──╮
+│c1│c2│c3│c4│c5│
+├──┼──┼──┼──┼──┤
+│ 0│12│24│36│48│
+╰──┴──┴──┴──┴──╯
+",
+    );
+
+    let pt = pt.with_layer(&[0, 1]).with_title("Column x b2 x a1");
+    assert_rendering(
+        "d3-layer0_1",
+        &pt,
+        "\
+Column x b2 x a1
+b2
+a1
+╭──┬──┬──┬──┬──╮
+│c1│c2│c3│c4│c5│
+├──┼──┼──┼──┼──┤
+│ 3│15│27│39│51│
+╰──┴──┴──┴──┴──╯
+",
+    );
+
+    let pt = pt.with_layer(&[1, 2]).with_title("Column x b3 x a2");
+    assert_rendering(
+        "d3-layer1_2",
+        &pt,
+        "\
+Column x b3 x a2
+b3
+a2
+╭──┬──┬──┬──┬──╮
+│c1│c2│c3│c4│c5│
+├──┼──┼──┼──┼──┤
+│ 7│19│31│43│55│
+╰──┴──┴──┴──┴──╯
+",
+    );
+}
+
+#[test]
+fn title_and_caption() {
+    let pivot_table =
+        d2("Title", [Axis3::X, Axis3::Y], None).with_caption(Value::new_text("Caption"));
+    assert_rendering(
+        "title_and_caption",
+        &pivot_table,
+        "\
+Title
+╭──┬──┬──┬──╮
+│  │a1│a2│a3│
+├──┼──┼──┼──┤
+│b1│ 0│ 1│ 2│
+│b2│ 3│ 4│ 5│
+│b3│ 6│ 7│ 8│
+╰──┴──┴──┴──╯
+Caption
+",
+    );
+
+    let pivot_table = pivot_table.with_show_title(false);
+    assert_rendering(
+        "caption",
+        &pivot_table,
+        "\
+╭──┬──┬──┬──╮
+│  │a1│a2│a3│
+├──┼──┼──┼──┤
+│b1│ 0│ 1│ 2│
+│b2│ 3│ 4│ 5│
+│b3│ 6│ 7│ 8│
+╰──┴──┴──┴──╯
+Caption
+",
+    );
+
+    let pivot_table = pivot_table.with_show_caption(false);
+    assert_rendering(
+        "no_title_or_caption",
+        &pivot_table,
+        "\
+╭──┬──┬──┬──╮
+│  │a1│a2│a3│
+├──┼──┼──┼──┤
+│b1│ 0│ 1│ 2│
+│b2│ 3│ 4│ 5│
+│b3│ 6│ 7│ 8│
+╰──┴──┴──┴──╯
+",
+    );
+}
+
+fn footnote_table(show_f0: bool) -> PivotTable {
+    let mut footnotes = Footnotes::new();
+    let f0 = footnotes.push(
+        Footnote::new("First footnote")
+            .with_marker("*")
+            .with_show(show_f0),
+    );
+    let f1 = footnotes.push(Footnote::new("Second footnote"));
+    let a = (
+        Axis3::X,
+        Dimension::new(
+            Group::new(Value::new_text("A").with_footnote(&f0))
+                .with_label_shown()
+                .with(Value::new_text("B").with_footnote(&f1))
+                .with(Value::new_text("C").with_footnote(&f0).with_footnote(&f1)),
+        ),
+    );
+    let d = (
+        Axis3::Y,
+        Dimension::new(
+            Group::new(Value::new_text("D").with_footnote(&f1))
+                .with_label_shown()
+                .with(Value::new_text("E").with_footnote(&f0))
+                .with(Value::new_text("F").with_footnote(&f1).with_footnote(&f0)),
+        ),
+    );
+    let look = test_look().with_row_label_position(LabelPosition::Nested);
+    let mut pt = PivotTable::new([a, d]).with_title(
+        Value::new_text("Pivot Table with Alphabetic Subscript Footnotes").with_footnote(&f0),
+    );
+    pt.insert(&[0, 0], Value::new_number(Some(0.0)));
+    pt.insert(&[1, 0], Value::new_number(Some(1.0)).with_footnote(&f0));
+    pt.insert(&[0, 1], Value::new_number(Some(2.0)).with_footnote(&f1));
+    pt.insert(
+        &[1, 1],
+        Value::new_number(Some(3.0))
+            .with_footnote(&f0)
+            .with_footnote(&f1),
+    );
+    pt.with_look(Arc::new(look))
+        .with_footnotes(footnotes)
+        .with_caption(Value::new_text("Caption").with_footnote(&f0))
+        .with_corner_text(
+            Value::new_text("Corner")
+                .with_footnote(&f0)
+                .with_footnote(&f1),
+        )
+}
+
+#[test]
+fn footnote_alphabetic_subscript() {
+    assert_rendering(
+        "footnote_alphabetic_subscript",
+        &footnote_table(true),
+        "\
+Pivot Table with Alphabetic Subscript Footnotes[*]
+╭────────────┬──────────────────╮
+│            │       A[*]       │
+│            ├───────┬──────────┤
+│Corner[*][b]│  B[b] │  C[*][b] │
+├────────────┼───────┼──────────┤
+│D[b] E[*]   │    .00│   1.00[*]│
+│     F[*][b]│2.00[b]│3.00[*][b]│
+╰────────────┴───────┴──────────╯
+Caption[*]
+*. First footnote
+b. Second footnote
+",
+    );
+}
+
+#[test]
+fn footnote_alphabetic_superscript() {
+    let mut pt = footnote_table(true);
+    let f0 = pt.footnotes.0[0].clone();
+    pt = pt.with_title(
+        Value::new_text("Pivot Table with Alphabetic Superscript Footnotes").with_footnote(&f0),
+    );
+    pt.look_mut().footnote_marker_position = FootnoteMarkerPosition::Superscript;
+    assert_rendering(
+        "footnote_alphabetic_superscript",
+        &pt,
+        "\
+Pivot Table with Alphabetic Superscript Footnotes[*]
+╭────────────┬──────────────────╮
+│            │       A[*]       │
+│            ├───────┬──────────┤
+│Corner[*][b]│  B[b] │  C[*][b] │
+├────────────┼───────┼──────────┤
+│D[b] E[*]   │    .00│   1.00[*]│
+│     F[*][b]│2.00[b]│3.00[*][b]│
+╰────────────┴───────┴──────────╯
+Caption[*]
+*. First footnote
+b. Second footnote
+",
+    );
+}
+
+#[test]
+fn footnote_numeric_subscript() {
+    let mut pt = footnote_table(true);
+    let f0 = pt.footnotes.0[0].clone();
+    pt = pt.with_title(
+        Value::new_text("Pivot Table with Numeric Subscript Footnotes").with_footnote(&f0),
+    );
+    pt.look_mut().footnote_marker_type = FootnoteMarkerType::Numeric;
+    assert_rendering(
+        "footnote_numeric_subscript",
+        &pt,
+        "\
+Pivot Table with Numeric Subscript Footnotes[*]
+╭────────────┬──────────────────╮
+│            │       A[*]       │
+│            ├───────┬──────────┤
+│Corner[*][2]│  B[2] │  C[*][2] │
+├────────────┼───────┼──────────┤
+│D[2] E[*]   │    .00│   1.00[*]│
+│     F[*][2]│2.00[2]│3.00[*][2]│
+╰────────────┴───────┴──────────╯
+Caption[*]
+*. First footnote
+2. Second footnote
+",
+    );
+}
+
+#[test]
+fn footnote_numeric_superscript() {
+    let mut pt = footnote_table(true);
+    let f0 = pt.footnotes.0[0].clone();
+    pt = pt.with_title(
+        Value::new_text("Pivot Table with Numeric Superscript Footnotes").with_footnote(&f0),
+    );
+    pt.look_mut().footnote_marker_type = FootnoteMarkerType::Numeric;
+    pt.look_mut().footnote_marker_position = FootnoteMarkerPosition::Superscript;
+    assert_rendering(
+        "footnote_numeric_superscript",
+        &pt,
+        "\
+Pivot Table with Numeric Superscript Footnotes[*]
+╭────────────┬──────────────────╮
+│            │       A[*]       │
+│            ├───────┬──────────┤
+│Corner[*][2]│  B[2] │  C[*][2] │
+├────────────┼───────┼──────────┤
+│D[2] E[*]   │    .00│   1.00[*]│
+│     F[*][2]│2.00[2]│3.00[*][2]│
+╰────────────┴───────┴──────────╯
+Caption[*]
+*. First footnote
+2. Second footnote
+",
+    );
+}
+
+#[test]
+fn footnote_hidden() {
+    assert_rendering(
+        "footnote_hidden",
+        &footnote_table(false),
+        "\
+Pivot Table with Alphabetic Subscript Footnotes[*]
+╭────────────┬──────────────────╮
+│            │       A[*]       │
+│            ├───────┬──────────┤
+│Corner[*][b]│  B[b] │  C[*][b] │
+├────────────┼───────┼──────────┤
+│D[b] E[*]   │    .00│   1.00[*]│
+│     F[*][b]│2.00[b]│3.00[*][b]│
+╰────────────┴───────┴──────────╯
+Caption[*]
+b. Second footnote
+",
+    );
+}
+
+#[test]
+fn no_dimension() {
+    let pivot_table = PivotTable::new([])
+        .with_title("No Dimensions")
+        .with_look(Arc::new(test_look()));
+    assert_rendering(
+        "no_dimension",
+        &pivot_table,
+        "No Dimensions
+╭╮
+╰╯
+",
+    );
+}
+
+#[test]
+fn empty_dimensions() {
+    let look = Arc::new(test_look().with_omit_empty(false));
+
+    let d1 = (Axis3::X, Dimension::new(Group::new("a")));
+    let pivot_table = PivotTable::new([d1])
+        .with_title("One Empty Dimension")
+        .with_look(look.clone());
+    assert_rendering("one_empty_dimension", &pivot_table, "One Empty Dimension\n");
+
+    let d1 = (Axis3::X, Dimension::new(Group::new("a")));
+    let d2 = (Axis3::X, Dimension::new(Group::new("b").with_label_shown()));
+    let pivot_table = PivotTable::new([d1, d2])
+        .with_title("Two Empty Dimensions")
+        .with_look(look.clone());
+    assert_rendering(
+        "two_empty_dimensions",
+        &pivot_table,
+        "Two Empty Dimensions\n",
+    );
+
+    let d1 = (Axis3::X, Dimension::new(Group::new("a")));
+    let d2 = (Axis3::X, Dimension::new(Group::new("b").with_label_shown()));
+    let d3 = (
+        Axis3::X,
+        Dimension::new(Group::new("c").with("c1").with("c2")),
+    );
+    let pivot_table = PivotTable::new([d1, d2, d3])
+        .with_title("Three Dimensions, Two Empty")
+        .with_look(look.clone());
+    assert_rendering(
+        "three_dimensions_two_empty",
+        &pivot_table,
+        "Three Dimensions, Two Empty\n",
+    );
+}
+
+#[test]
+fn empty_groups() {
+    let d1 = (
+        Axis3::X,
+        Dimension::new(Group::new("a").with("a1").with(Group::new("a2")).with("a3")),
+    );
+
+    let d2 = (
+        Axis3::Y,
+        Dimension::new(Group::new("b").with(Group::new("b1")).with("b2").with("b3")),
+    );
+
+    let mut pt = PivotTable::new([d1, d2]).with_title("Empty Groups");
+    let mut i = 0;
+    for b in 0..2 {
+        for a in 0..2 {
+            pt.insert(&[a, b], Value::new_integer(Some(i as f64)));
+            i += 1;
+        }
+    }
+    let pivot_table = pt.with_look(Arc::new(test_look().with_omit_empty(false)));
+    assert_rendering(
+        "empty_groups",
+        &pivot_table,
+        "\
+Empty Groups
+╭──┬──┬──╮
+│  │a1│a3│
+├──┼──┼──┤
+│b2│ 0│ 1│
+│b3│ 2│ 3│
+╰──┴──┴──╯
+",
+    );
+}
+
+fn d4(
+    title: &str,
+    borders: EnumMap<Border, BorderStyle>,
+    show_dimension_labels: bool,
+) -> PivotTable {
+    let a = (
+        Axis3::X,
+        Dimension::new(
+            Group::new("a")
+                .with_show_label(show_dimension_labels)
+                .with("a1")
+                .with(Group::new("ag1").with("a2").with("a3")),
+        ),
+    );
+    let b = (
+        Axis3::X,
+        Dimension::new(
+            Group::new("b")
+                .with_show_label(show_dimension_labels)
+                .with(Group::new("bg1").with("b1").with("b2"))
+                .with("b3"),
+        ),
+    );
+    let c = (
+        Axis3::Y,
+        Dimension::new(
+            Group::new("c")
+                .with_show_label(show_dimension_labels)
+                .with("c1")
+                .with(Group::new("cg1").with("c2").with("c3")),
+        ),
+    );
+    let d = (
+        Axis3::Y,
+        Dimension::new(
+            Group::new("d")
+                .with_show_label(show_dimension_labels)
+                .with(Group::new("dg1").with("d1").with("d2"))
+                .with("d3"),
+        ),
+    );
+    let mut pivot_table = PivotTable::new([a, b, c, d])
+        .with_title(title)
+        .with_look(Arc::new(test_look().with_borders(borders)));
+    let mut i = 0;
+    for d in 0..3 {
+        for c in 0..3 {
+            for b in 0..3 {
+                for a in 0..3 {
+                    pivot_table.insert(&[a, b, c, d], Value::new_integer(Some(i as f64)));
+                    i += 1;
+                }
+            }
+        }
+    }
+    pivot_table
+}
+
+#[test]
+fn dimension_borders_1() {
+    let pivot_table = d4(
+        "Dimension Borders 1",
+        EnumMap::from_fn(|border| match border {
+            Border::Dimension(RowColBorder(HeadingRegion::Rows, Axis2::X))
+            | Border::Dimension(RowColBorder(HeadingRegion::Columns, Axis2::Y)) => SOLID_BLUE,
+            _ => BorderStyle::none(),
+        }),
+        true,
+    );
+    assert_rendering(
+        "dimension_borders_1",
+        &pivot_table,
+        "\
+Dimension Borders 1
+                           b
+                     bg1       │
+                 b1   │   b2   │   b3
+                  a   │    a   │    a
+                │ ag1 │  │ ag1 │  │ ag1
+d      c      a1│a2 a3│a1│a2 a3│a1│a2 a3
+dg1 d1     c1  0│ 1  2│ 3│ 4  5│ 6│ 7  8
+      ╶─────────┼─────┼──┼─────┼──┼─────
+       cg1 c2  9│10 11│12│13 14│15│16 17
+           c3 18│19 20│21│22 23│24│25 26
+   ╶────────────┼─────┼──┼─────┼──┼─────
+    d2     c1 27│28 29│30│31 32│33│34 35
+      ╶─────────┼─────┼──┼─────┼──┼─────
+       cg1 c2 36│37 38│39│40 41│42│43 44
+           c3 45│46 47│48│49 50│51│52 53
+────────────────┼─────┼──┼─────┼──┼─────
+    d3     c1 54│55 56│57│58 59│60│61 62
+      ╶─────────┼─────┼──┼─────┼──┼─────
+       cg1 c2 63│64 65│66│67 68│69│70 71
+           c3 72│73 74│75│76 77│78│79 80
+",
+    );
+}
+
+#[test]
+fn dimension_borders_2() {
+    let pivot_table = d4(
+        "Dimension Borders 2",
+        EnumMap::from_fn(|border| match border {
+            Border::Dimension(RowColBorder(HeadingRegion::Rows, Axis2::Y))
+            | Border::Dimension(RowColBorder(HeadingRegion::Columns, Axis2::X)) => SOLID_BLUE,
+            _ => BorderStyle::none(),
+        }),
+        true,
+    );
+    assert_rendering(
+        "dimension_borders_2",
+        &pivot_table,
+        "\
+Dimension Borders 2
+                           b
+                     bg1
+                 b1       b2       b3
+             ╶──────────────────────────
+                  a        a        a
+                  ag1      ag1      ag1
+d      c      a1 a2 a3 a1 a2 a3 a1 a2 a3
+dg1 d1│    c1  0  1  2  3  4  5  6  7  8
+      │cg1 c2  9 10 11 12 13 14 15 16 17
+      │    c3 18 19 20 21 22 23 24 25 26
+    d2│    c1 27 28 29 30 31 32 33 34 35
+      │cg1 c2 36 37 38 39 40 41 42 43 44
+      │    c3 45 46 47 48 49 50 51 52 53
+    d3│    c1 54 55 56 57 58 59 60 61 62
+      │cg1 c2 63 64 65 66 67 68 69 70 71
+      │    c3 72 73 74 75 76 77 78 79 80
+",
+    );
+}
+
+#[test]
+fn category_borders_1() {
+    let pivot_table = d4(
+        "Category Borders 1",
+        EnumMap::from_fn(|border| match border {
+            Border::Category(RowColBorder(HeadingRegion::Rows, Axis2::X))
+            | Border::Category(RowColBorder(HeadingRegion::Columns, Axis2::Y)) => DASHED_RED,
+            _ => BorderStyle::none(),
+        }),
+        true,
+    );
+    assert_rendering(
+        "category_borders_1",
+        &pivot_table,
+        "\
+Category Borders 1
+                           b
+                     bg1       ┊
+                 b1   ┊   b2   ┊   b3
+                  a   ┊    a   ┊    a
+                ┊ ag1 ┊  ┊ ag1 ┊  ┊ ag1
+d      c      a1┊a2┊a3┊a1┊a2┊a3┊a1┊a2┊a3
+dg1 d1     c1  0┊ 1┊ 2┊ 3┊ 4┊ 5┊ 6┊ 7┊ 8
+      ╌╌╌╌╌╌╌╌╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌
+       cg1 c2  9┊10┊11┊12┊13┊14┊15┊16┊17
+          ╌╌╌╌╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌
+           c3 18┊19┊20┊21┊22┊23┊24┊25┊26
+   ╌╌╌╌╌╌╌╌╌╌╌╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌
+    d2     c1 27┊28┊29┊30┊31┊32┊33┊34┊35
+      ╌╌╌╌╌╌╌╌╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌
+       cg1 c2 36┊37┊38┊39┊40┊41┊42┊43┊44
+          ╌╌╌╌╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌
+           c3 45┊46┊47┊48┊49┊50┊51┊52┊53
+╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌
+    d3     c1 54┊55┊56┊57┊58┊59┊60┊61┊62
+      ╌╌╌╌╌╌╌╌╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌
+       cg1 c2 63┊64┊65┊66┊67┊68┊69┊70┊71
+          ╌╌╌╌╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌+╌╌
+           c3 72┊73┊74┊75┊76┊77┊78┊79┊80
+",
+    );
+}
+
+#[test]
+fn category_borders_2() {
+    let pivot_table = d4(
+        "Category Borders 2",
+        EnumMap::from_fn(|border| match border {
+            Border::Category(RowColBorder(HeadingRegion::Rows, Axis2::Y))
+            | Border::Category(RowColBorder(HeadingRegion::Columns, Axis2::X)) => DASHED_RED,
+            _ => BorderStyle::none(),
+        }),
+        true,
+    );
+    assert_rendering(
+        "category_borders_2",
+        &pivot_table,
+        "\
+Category Borders 2
+                           b
+             ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
+                     bg1
+             ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
+                 b1       b2       b3
+             ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
+                  a        a        a
+             ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
+                  ag1      ag1      ag1
+                ╌╌╌╌╌╌╌  ╌╌╌╌╌╌╌  ╌╌╌╌╌╌
+d      c      a1 a2 a3 a1 a2 a3 a1 a2 a3
+dg1┊d1┊    c1  0  1  2  3  4  5  6  7  8
+   ┊  ┊cg1┊c2  9 10 11 12 13 14 15 16 17
+   ┊  ┊   ┊c3 18 19 20 21 22 23 24 25 26
+   ┊d2┊    c1 27 28 29 30 31 32 33 34 35
+   ┊  ┊cg1┊c2 36 37 38 39 40 41 42 43 44
+   ┊  ┊   ┊c3 45 46 47 48 49 50 51 52 53
+    d3┊    c1 54 55 56 57 58 59 60 61 62
+      ┊cg1┊c2 63 64 65 66 67 68 69 70 71
+      ┊   ┊c3 72 73 74 75 76 77 78 79 80
+",
+    );
+}
+
+#[test]
+fn category_and_dimension_borders_1() {
+    let pivot_table = d4(
+        "Category and Dimension Borders 1",
+        EnumMap::from_fn(|border| match border {
+            Border::Dimension(RowColBorder(HeadingRegion::Rows, Axis2::X))
+            | Border::Dimension(RowColBorder(HeadingRegion::Columns, Axis2::Y)) => SOLID_BLUE,
+            Border::Category(RowColBorder(HeadingRegion::Rows, Axis2::X))
+            | Border::Category(RowColBorder(HeadingRegion::Columns, Axis2::Y)) => DASHED_RED,
+            _ => BorderStyle::none(),
+        }),
+        true,
+    );
+    assert_rendering(
+        "category_and_dimension_borders_1",
+        &pivot_table,
+        "\
+Category and Dimension Borders 1
+                           b
+                     bg1       │
+                 b1   │   b2   │   b3
+                  a   │    a   │    a
+                │ ag1 │  │ ag1 │  │ ag1
+d      c      a1│a2┊a3│a1│a2┊a3│a1│a2┊a3
+dg1 d1     c1  0│ 1┊ 2│ 3│ 4┊ 5│ 6│ 7┊ 8
+      ╶─────────┼──┼──┼──┼──┼──┼──┼──┼──
+       cg1 c2  9│10┊11│12│13┊14│15│16┊17
+          ╌╌╌╌╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌
+           c3 18│19┊20│21│22┊23│24│25┊26
+   ╶────────────┼──┼──┼──┼──┼──┼──┼──┼──
+    d2     c1 27│28┊29│30│31┊32│33│34┊35
+      ╶─────────┼──┼──┼──┼──┼──┼──┼──┼──
+       cg1 c2 36│37┊38│39│40┊41│42│43┊44
+          ╌╌╌╌╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌
+           c3 45│46┊47│48│49┊50│51│52┊53
+────────────────┼──┼──┼──┼──┼──┼──┼──┼──
+    d3     c1 54│55┊56│57│58┊59│60│61┊62
+      ╶─────────┼──┼──┼──┼──┼──┼──┼──┼──
+       cg1 c2 63│64┊65│66│67┊68│69│70┊71
+          ╌╌╌╌╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌
+           c3 72│73┊74│75│76┊77│78│79┊80
+",
+    );
+}
+
+#[test]
+fn category_and_dimension_borders_2() {
+    let pivot_table = d4(
+        "Category and Dimension Borders 2",
+        EnumMap::from_fn(|border| match border {
+            Border::Dimension(RowColBorder(HeadingRegion::Rows, Axis2::Y))
+            | Border::Dimension(RowColBorder(HeadingRegion::Columns, Axis2::X)) => SOLID_BLUE,
+            Border::Category(RowColBorder(HeadingRegion::Rows, Axis2::Y))
+            | Border::Category(RowColBorder(HeadingRegion::Columns, Axis2::X)) => DASHED_RED,
+            _ => BorderStyle::none(),
+        }),
+        true,
+    );
+    assert_rendering(
+        "category_and_dimension_borders_2",
+        &pivot_table,
+        "\
+Category and Dimension Borders 2
+                           b
+             ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
+                     bg1
+             ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
+                 b1       b2       b3
+             ╶──────────────────────────
+                  a        a        a
+             ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌
+                  ag1      ag1      ag1
+                ╌╌╌╌╌╌╌  ╌╌╌╌╌╌╌  ╌╌╌╌╌╌
+d      c      a1 a2 a3 a1 a2 a3 a1 a2 a3
+dg1┊d1│    c1  0  1  2  3  4  5  6  7  8
+   ┊  │cg1┊c2  9 10 11 12 13 14 15 16 17
+   ┊  │   ┊c3 18 19 20 21 22 23 24 25 26
+   ┊d2│    c1 27 28 29 30 31 32 33 34 35
+   ┊  │cg1┊c2 36 37 38 39 40 41 42 43 44
+   ┊  │   ┊c3 45 46 47 48 49 50 51 52 53
+    d3│    c1 54 55 56 57 58 59 60 61 62
+      │cg1┊c2 63 64 65 66 67 68 69 70 71
+      │   ┊c3 72 73 74 75 76 77 78 79 80
+",
+    );
+}
+
+const SOLID_BLUE: BorderStyle = BorderStyle {
+    stroke: Stroke::Solid,
+    color: Color::BLUE,
+};
+
+const DASHED_RED: BorderStyle = BorderStyle {
+    stroke: Stroke::Dashed,
+    color: Color::RED,
+};
+
+#[test]
+fn category_and_dimension_borders_3() {
+    let pivot_table = d4(
+        "Category and Dimension Borders 3",
+        EnumMap::from_fn(|border| match border {
+            Border::Dimension(_) => SOLID_BLUE,
+            Border::Category(_) => DASHED_RED,
+            _ => BorderStyle::none(),
+        }),
+        false,
+    );
+    assert_rendering(
+        "category_and_dimension_borders_3",
+        &pivot_table,
+        "\
+Category and Dimension Borders 3
+                     bg1       │
+             ╌╌╌╌╌╌╌╌╌┬╌╌╌╌╌╌╌╌┤
+                 b1   │   b2   │   b3
+             ╶──┬─────┼──┬─────┼──┬─────
+                │ ag1 │  │ ag1 │  │ ag1
+                ├╌╌┬╌╌┤  ├╌╌┬╌╌┤  ├╌╌┬╌╌
+              a1│a2┊a3│a1│a2┊a3│a1│a2┊a3
+dg1┊d1│    c1  0│ 1┊ 2│ 3│ 4┊ 5│ 6│ 7┊ 8
+   ┊  ├───┬─────┼──┼──┼──┼──┼──┼──┼──┼──
+   ┊  │cg1┊c2  9│10┊11│12│13┊14│15│16┊17
+   ┊  │   ├╌╌╌╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌
+   ┊  │   ┊c3 18│19┊20│21│22┊23│24│25┊26
+   ├──┼───┴─────┼──┼──┼──┼──┼──┼──┼──┼──
+   ┊d2│    c1 27│28┊29│30│31┊32│33│34┊35
+   ┊  ├───┬─────┼──┼──┼──┼──┼──┼──┼──┼──
+   ┊  │cg1┊c2 36│37┊38│39│40┊41│42│43┊44
+   ┊  │   ├╌╌╌╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌
+   ┊  │   ┊c3 45│46┊47│48│49┊50│51│52┊53
+───┴──┼───┴─────┼──┼──┼──┼──┼──┼──┼──┼──
+    d3│    c1 54│55┊56│57│58┊59│60│61┊62
+      ├───┬─────┼──┼──┼──┼──┼──┼──┼──┼──
+      │cg1┊c2 63│64┊65│66│67┊68│69│70┊71
+      │   ├╌╌╌╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌┼╌╌┼╌╌+╌╌
+      │   ┊c3 72│73┊74│75│76┊77│78│79┊80
+",
+    );
+}
+
+#[test]
+fn small_numbers() {
+    let exponent = (
+        Axis3::Y,
+        Dimension::new(
+            Group::new("exponent")
+                .with("0")
+                .with("-1")
+                .with("-2")
+                .with("-3")
+                .with("-4")
+                .with("-5")
+                .with("-6")
+                .with("-7")
+                .with("-8")
+                .with("-9")
+                .with_label_shown(),
+        ),
+    );
+    let sign = (
+        Axis3::X,
+        Dimension::new(
+            Group::new("sign")
+                .with("positive")
+                .with("negative")
+                .with_label_shown(),
+        ),
+    );
+    let rc = (
+        Axis3::X,
+        Dimension::new(
+            Group::new("result class")
+                .with("general")
+                .with("specific")
+                .with_label_shown(),
+        ),
+    );
+    let mut pt = PivotTable::new([exponent, sign, rc]).with_title("small numbers");
+    pt.insert_number(&[0, 0, 0], Some(1.0), Class::Other);
+    pt.insert_number(&[1, 0, 0], Some(0.1), Class::Other);
+    pt.insert_number(&[2, 0, 0], Some(0.01), Class::Other);
+    pt.insert_number(&[3, 0, 0], Some(0.001), Class::Other);
+    pt.insert_number(&[4, 0, 0], Some(0.0001), Class::Other);
+    pt.insert_number(&[5, 0, 0], Some(0.00001), Class::Other);
+    pt.insert_number(&[6, 0, 0], Some(0.000001), Class::Other);
+    pt.insert_number(&[7, 0, 0], Some(0.0000001), Class::Other);
+    pt.insert_number(&[8, 0, 0], Some(0.00000001), Class::Other);
+    pt.insert_number(&[9, 0, 0], Some(0.000000001), Class::Other);
+    pt.insert_number(&[0, 0, 1], Some(-1.0), Class::Residual);
+    pt.insert_number(&[1, 0, 1], Some(-0.1), Class::Residual);
+    pt.insert_number(&[2, 0, 1], Some(-0.01), Class::Residual);
+    pt.insert_number(&[3, 0, 1], Some(-0.001), Class::Residual);
+    pt.insert_number(&[4, 0, 1], Some(-0.0001), Class::Residual);
+    pt.insert_number(&[5, 0, 1], Some(-0.00001), Class::Residual);
+    pt.insert_number(&[6, 0, 1], Some(-0.000001), Class::Residual);
+    pt.insert_number(&[7, 0, 1], Some(-0.0000001), Class::Residual);
+    pt.insert_number(&[8, 0, 1], Some(-0.00000001), Class::Residual);
+    pt.insert_number(&[9, 0, 1], Some(-0.000000001), Class::Residual);
+    pt.insert_number(&[0, 1, 0], Some(1.0), Class::Other);
+    pt.insert_number(&[1, 1, 0], Some(0.1), Class::Other);
+    pt.insert_number(&[2, 1, 0], Some(0.01), Class::Other);
+    pt.insert_number(&[3, 1, 0], Some(0.001), Class::Other);
+    pt.insert_number(&[4, 1, 0], Some(0.0001), Class::Other);
+    pt.insert_number(&[5, 1, 0], Some(0.00001), Class::Other);
+    pt.insert_number(&[6, 1, 0], Some(0.000001), Class::Other);
+    pt.insert_number(&[7, 1, 0], Some(0.0000001), Class::Other);
+    pt.insert_number(&[8, 1, 0], Some(0.00000001), Class::Other);
+    pt.insert_number(&[9, 1, 0], Some(0.000000001), Class::Other);
+    pt.insert_number(&[0, 1, 1], Some(-1.0), Class::Residual);
+    pt.insert_number(&[1, 1, 1], Some(-0.1), Class::Residual);
+    pt.insert_number(&[2, 1, 1], Some(-0.01), Class::Residual);
+    pt.insert_number(&[3, 1, 1], Some(-0.001), Class::Residual);
+    pt.insert_number(&[4, 1, 1], Some(-0.0001), Class::Residual);
+    pt.insert_number(&[5, 1, 1], Some(-0.00001), Class::Residual);
+    pt.insert_number(&[6, 1, 1], Some(-0.000001), Class::Residual);
+    pt.insert_number(&[7, 1, 1], Some(-0.0000001), Class::Residual);
+    pt.insert_number(&[8, 1, 1], Some(-0.00000001), Class::Residual);
+    pt.insert_number(&[9, 1, 1], Some(-0.000000001), Class::Residual);
+    let pivot_table = pt.with_look(Arc::new(test_look()));
+    assert_rendering(
+        "small_numbers",
+        &pivot_table,
+        "\
+small numbers
+╭────────┬─────────────────────────────────────╮
+│        │             result class            │
+│        ├───────────────────┬─────────────────┤
+│        │      general      │     specific    │
+│        ├───────────────────┼─────────────────┤
+│        │        sign       │       sign      │
+│        ├─────────┬─────────┼────────┬────────┤
+│exponent│ positive│ negative│positive│negative│
+├────────┼─────────┼─────────┼────────┼────────┤
+│0       │     1.00│     1.00│   -1.00│   -1.00│
+│-1      │      .10│      .10│    -.10│    -.10│
+│-2      │      .01│      .01│    -.01│    -.01│
+│-3      │      .00│      .00│     .00│     .00│
+│-4      │      .00│      .00│     .00│     .00│
+│-5      │1.00E-005│1.00E-005│     .00│     .00│
+│-6      │1.00E-006│1.00E-006│     .00│     .00│
+│-7      │1.00E-007│1.00E-007│     .00│     .00│
+│-8      │1.00E-008│1.00E-008│     .00│     .00│
+│-9      │1.00E-009│1.00E-009│     .00│     .00│
+╰────────┴─────────┴─────────┴────────┴────────╯
+",
+    );
+}
index b51e551461822969d8fe85e0b3c34ec508cb8332..dfa292c911159f95193e31ce2359d48ca473fdfa 100644 (file)
@@ -560,7 +560,7 @@ impl Debug for U8String {
 }
 
 #[cfg(test)]
-mod test {
+mod tests {
     use crate::output::pivot::tlo::parse_tlo;
 
     #[test]
index af00cfcc6829b4aebd7e86e2430aadcb0b9783d7..147f475d53d80e6b36dc7c5327d42f688e76ec49 100644 (file)
@@ -652,7 +652,7 @@ impl Device for TextRenderer {
 }
 
 #[cfg(test)]
-mod test {
+mod tests {
     use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
 
     use crate::output::text::new_line_breaks;
index 2911b63bc448a174cd362c62005fcaa316d8ac16..e4d7c5c370c0b34ec27b5fc48af122a003aa51b4 100644 (file)
@@ -330,7 +330,7 @@ pub fn clip_text<'a>(
 }
 
 #[cfg(test)]
-mod test {
+mod tests {
     use super::{Emphasis, TextLine};
     use enum_iterator::all;
 
index 4f596141006e6189afc983c8ba5b33073162d365..1746781d69241a3d7661b06085b8fcde7981e396 100644 (file)
@@ -42,7 +42,7 @@ use serde::Serializer;
 pub use write::{SystemFileVersion, WriteOptions, Writer};
 
 #[cfg(test)]
-mod test;
+mod tests;
 
 fn serialize_endian<S>(endian: &Endian, serializer: S) -> Result<S::Ok, S::Error>
 where
index b23d0d83ea8bd42d04140912ea1c2b518767ed12..348fda6bf5b6c284c7f4fee89dde84f30d8acf3e 100644 (file)
@@ -573,7 +573,7 @@ impl<'a> Lexer<'a> {
 }
 
 #[cfg(test)]
-mod test {
+mod tests {
     use crate::sys::sack::sack;
     use anyhow::Result;
     use binrw::Endian;
diff --git a/rust/pspp/src/sys/test.rs b/rust/pspp/src/sys/test.rs
deleted file mode 100644 (file)
index 9198a71..0000000
+++ /dev/null
@@ -1,812 +0,0 @@
-// PSPP - a program for statistical analysis.
-// Copyright (C) 2025 Free Software Foundation, Inc.
-//
-// This program is free software: you can redistribute it and/or modify it under
-// the terms of the GNU General Public License as published by the Free Software
-// Foundation, either version 3 of the License, or (at your option) any later
-// version.
-//
-// This program is distributed in the hope that it will be useful, but WITHOUT
-// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
-// FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
-// details.
-//
-// You should have received a copy of the GNU General Public License along with
-// this program.  If not, see <http://www.gnu.org/licenses/>.
-
-use std::{
-    fs::File,
-    io::{BufRead, BufReader, Cursor, Seek},
-    path::{Path, PathBuf},
-    sync::Arc,
-};
-
-use binrw::Endian;
-use encoding_rs::UTF_8;
-use itertools::Itertools;
-
-use crate::{
-    crypto::EncryptedFile,
-    data::Datum,
-    dictionary::Dictionary,
-    identifier::Identifier,
-    output::{
-        pivot::{test::assert_lines_eq, Axis3, Dimension, Group, PivotTable, Value},
-        Details, Item, Text,
-    },
-    sys::{
-        cooked::ReadOptions,
-        raw::{self, records::Compression, ErrorDetails},
-        sack::sack,
-        WriteOptions,
-    },
-    variable::{VarWidth, Variable},
-};
-
-#[test]
-fn variable_labels_and_missing_values() {
-    test_sack_sysfile("variable_labels_and_missing_values");
-}
-
-#[test]
-fn unspecified_number_of_variable_positions() {
-    test_sack_sysfile("unspecified_number_of_variable_positions");
-}
-
-#[test]
-fn wrong_variable_positions_but_v13() {
-    test_sack_sysfile("wrong_variable_positions_but_v13");
-}
-
-#[test]
-fn value_labels() {
-    test_sack_sysfile("value_labels");
-}
-
-#[test]
-fn documents() {
-    test_sack_sysfile("documents");
-}
-
-#[test]
-fn empty_document_record() {
-    test_sack_sysfile("empty_document_record");
-}
-
-#[test]
-fn variable_sets() {
-    test_sack_sysfile("variable_sets");
-}
-
-#[test]
-fn multiple_response_sets() {
-    test_sack_sysfile("multiple_response_sets");
-}
-
-#[test]
-fn extra_product_info() {
-    // Also checks for handling of CR-only line ends in file label and extra
-    // product info.
-    test_sack_sysfile("extra_product_info");
-}
-
-#[test]
-fn variable_display_without_width() {
-    test_sack_sysfile("variable_display_without_width");
-}
-
-#[test]
-fn variable_display_with_width() {
-    test_sack_sysfile("variable_display_with_width");
-}
-
-#[test]
-fn long_variable_names() {
-    test_sack_sysfile("long_variable_names");
-}
-
-#[test]
-fn very_long_strings() {
-    test_sack_sysfile("very_long_strings");
-}
-
-#[test]
-fn attributes() {
-    test_sack_sysfile("attributes");
-}
-
-#[test]
-fn variable_roles() {
-    test_sack_sysfile("variable_roles");
-}
-
-#[test]
-fn compressed_data() {
-    test_sack_sysfile("compressed_data");
-}
-
-#[test]
-fn compressed_data_zero_bias() {
-    test_sack_sysfile("compressed_data_zero_bias");
-}
-
-#[test]
-fn compressed_data_other_bias() {
-    test_sack_sysfile("compressed_data_other_bias");
-}
-
-#[test]
-fn zcompressed_data() {
-    test_sack_sysfile("zcompressed_data");
-}
-
-#[test]
-fn no_variables() {
-    test_sack_sysfile("no_variables");
-}
-
-#[test]
-fn unknown_encoding() {
-    test_sack_sysfile("unknown_encoding");
-}
-
-#[test]
-fn misplaced_type_4_record() {
-    test_sack_sysfile("misplaced_type_4_record");
-}
-
-#[test]
-fn bad_record_type() {
-    test_sack_sysfile("bad_record_type");
-}
-
-#[test]
-fn wrong_variable_positions() {
-    test_sack_sysfile("wrong_variable_positions");
-}
-
-#[test]
-fn invalid_variable_name() {
-    test_sack_sysfile("invalid_variable_name");
-}
-
-#[test]
-fn invalid_label_indicator() {
-    test_sack_sysfile("invalid_label_indicator");
-}
-
-#[test]
-fn invalid_missing_indicator() {
-    test_sack_sysfile("invalid_missing_indicator");
-}
-
-#[test]
-fn invalid_missing_indicator2() {
-    test_sack_sysfile("invalid_missing_indicator2");
-}
-
-#[test]
-fn missing_string_continuation() {
-    test_sack_sysfile("missing_string_continuation");
-}
-
-#[test]
-fn invalid_variable_format() {
-    test_sack_sysfile("invalid_variable_format");
-}
-
-#[test]
-fn invalid_long_string_missing_values() {
-    test_sack_sysfile("invalid_long_string_missing_values");
-}
-
-#[test]
-fn weight_must_be_numeric() {
-    test_sack_sysfile("weight_must_be_numeric");
-}
-
-#[test]
-fn weight_variable_bad_index() {
-    test_sack_sysfile("weight_variable_bad_index");
-}
-
-#[test]
-fn weight_variable_continuation() {
-    test_sack_sysfile("weight_variable_continuation");
-}
-
-#[test]
-fn multiple_documents_records() {
-    test_sack_sysfile("multiple_documents_records");
-}
-
-#[test]
-fn unknown_extension_record() {
-    test_sack_sysfile("unknown_extension_record");
-}
-
-#[test]
-fn extension_too_large() {
-    test_sack_sysfile("extension_too_large");
-}
-
-#[test]
-fn bad_machine_integer_info_count() {
-    test_sack_sysfile("bad_machine_integer_info_count");
-}
-
-#[test]
-fn bad_machine_integer_info_float_format() {
-    test_sack_sysfile("bad_machine_integer_info_float_format");
-}
-
-#[test]
-fn bad_machine_integer_info_endianness() {
-    test_sack_sysfile("bad_machine_integer_info_endianness");
-}
-
-#[test]
-fn bad_machine_float_info_size() {
-    test_sack_sysfile("bad_machine_float_info_size");
-}
-
-#[test]
-fn wrong_special_floats() {
-    test_sack_sysfile("wrong_special_floats");
-}
-
-#[test]
-fn variable_sets_unknown_variable() {
-    test_sack_sysfile("variable_sets_unknown_variable");
-}
-
-#[test]
-fn multiple_response_sets_bad_name() {
-    test_sack_sysfile("multiple_response_sets_bad_name");
-}
-
-#[test]
-fn multiple_response_sets_missing_space_after_c() {
-    test_sack_sysfile("multiple_response_sets_missing_space_after_c");
-}
-
-#[test]
-fn multiple_response_sets_missing_space_after_e() {
-    test_sack_sysfile("multiple_response_sets_missing_space_after_e");
-}
-
-#[test]
-fn multiple_response_sets_missing_label_source() {
-    test_sack_sysfile("multiple_response_sets_missing_label_source");
-}
-
-#[test]
-fn multiple_response_sets_unexpected_label_source() {
-    test_sack_sysfile("multiple_response_sets_unexpected_label_source");
-}
-
-#[test]
-fn multiple_response_sets_bad_counted_string() {
-    test_sack_sysfile("multiple_response_sets_bad_counted_string");
-}
-
-#[test]
-fn multiple_response_sets_counted_string_missing_space() {
-    test_sack_sysfile("multiple_response_sets_counted_string_missing_space");
-}
-
-#[test]
-fn multiple_response_sets_counted_string_bad_length() {
-    test_sack_sysfile("multiple_response_sets_counted_string_bad_length");
-}
-
-#[test]
-fn multiple_response_sets_missing_space_after_counted_string() {
-    test_sack_sysfile("multiple_response_sets_missing_space_after_counted_string");
-}
-
-#[test]
-fn multiple_response_sets_missing_newline_after_variable_name() {
-    test_sack_sysfile("multiple_response_sets_missing_newline_after_variable_name");
-}
-
-#[test]
-fn multiple_response_sets_duplicate_variable_name() {
-    test_sack_sysfile("multiple_response_sets_duplicate_variable_name");
-}
-
-#[test]
-fn mixed_variable_types_in_mrsets() {
-    test_sack_sysfile("mixed_variable_types_in_mrsets");
-}
-
-#[test]
-fn missing_newline_after_variable_name_in_mrsets() {
-    test_sack_sysfile("missing_newline_after_variable_name_in_mrsets");
-}
-
-#[test]
-fn zero_or_one_variable_in_mrset() {
-    test_sack_sysfile("zero_or_one_variable_in_mrset");
-}
-
-#[test]
-fn wrong_display_parameter_size() {
-    test_sack_sysfile("wrong_display_parameter_size");
-}
-
-#[test]
-fn wrong_display_parameter_count() {
-    test_sack_sysfile("wrong_display_parameter_count");
-}
-
-#[test]
-fn wrong_display_measurement_level() {
-    test_sack_sysfile("wrong_display_measurement_level");
-}
-
-#[test]
-fn wrong_display_alignment() {
-    test_sack_sysfile("wrong_display_alignment");
-}
-
-#[test]
-fn bad_variable_name_in_variable_value_pair() {
-    test_sack_sysfile("bad_variable_name_in_variable_value_pair");
-}
-
-#[test]
-fn duplicate_long_variable_name() {
-    test_sack_sysfile("duplicate_long_variable_name");
-}
-
-#[test]
-fn bad_very_long_string_length() {
-    test_sack_sysfile("bad_very_long_string_length");
-}
-
-#[test]
-fn bad_very_long_string_segment_width() {
-    test_sack_sysfile("bad_very_long_string_segment_width");
-}
-
-#[test]
-fn too_many_value_labels() {
-    test_sack_sysfile("too_many_value_labels");
-}
-
-#[test]
-fn missing_type_4_record() {
-    test_sack_sysfile("missing_type_4_record");
-}
-
-#[test]
-fn value_label_with_no_associated_variables() {
-    test_sack_sysfile("value_label_with_no_associated_variables");
-}
-
-#[test]
-fn type_4_record_names_long_string_variable() {
-    test_sack_sysfile("type_4_record_names_long_string_variable");
-}
-
-#[test]
-fn value_label_variable_indexes_must_be_in_correct_range() {
-    test_sack_sysfile("value_label_variable_indexes_must_be_in_correct_range");
-}
-
-#[test]
-fn value_label_variable_indexes_must_not_be_long_string_continuation() {
-    test_sack_sysfile("value_label_variable_indexes_must_not_be_long_string_continuation");
-}
-
-#[test]
-fn variables_for_value_label_must_all_be_same_type() {
-    test_sack_sysfile("variables_for_value_label_must_all_be_same_type");
-}
-
-#[test]
-fn duplicate_value_labels_type() {
-    test_sack_sysfile("duplicate_value_labels_type");
-}
-
-#[test]
-fn missing_attribute_value() {
-    test_sack_sysfile("missing_attribute_value");
-}
-
-#[test]
-fn unquoted_attribute_value() {
-    test_sack_sysfile("unquoted_attribute_value");
-}
-
-#[test]
-fn duplicate_attribute_name() {
-    test_sack_sysfile("duplicate_attribute_name");
-}
-
-#[test]
-fn bad_variable_name_in_long_string_value_label() {
-    test_sack_sysfile("bad_variable_name_in_long_string_value_label");
-}
-
-#[test]
-fn fewer_data_records_than_indicated_by_file_header() {
-    test_sack_sysfile("fewer_data_records_than_indicated_by_file_header");
-}
-
-#[test]
-fn more_data_records_than_indicated_by_file_header() {
-    test_sack_sysfile("more_data_records_than_indicated_by_file_header");
-}
-
-#[test]
-fn partial_data_record_between_variables() {
-    test_sack_sysfile("partial_data_record_between_variables");
-}
-
-#[test]
-fn partial_data_record_within_long_string() {
-    test_sack_sysfile("partial_data_record_within_long_string");
-}
-
-#[test]
-fn partial_compressed_data_record() {
-    test_sack_sysfile("partial_compressed_data_record");
-}
-
-#[test]
-fn zcompressed_data_bad_zheader_ofs() {
-    test_sack_sysfile("zcompressed_data_bad_zheader_ofs");
-}
-
-#[test]
-fn zcompressed_data_bad_ztrailer_ofs() {
-    test_sack_sysfile("zcompressed_data_bad_ztrailer_ofs");
-}
-
-#[test]
-fn zcompressed_data_invalid_ztrailer_len() {
-    test_sack_sysfile("zcompressed_data_invalid_ztrailer_len");
-}
-
-#[test]
-fn zcompressed_data_wrong_ztrailer_len() {
-    test_sack_sysfile("zcompressed_data_wrong_ztrailer_len");
-}
-
-#[test]
-fn zcompressed_data_wrong_ztrailer_bias() {
-    test_sack_sysfile("zcompressed_data_wrong_ztrailer_bias");
-}
-
-#[test]
-fn zcompressed_data_wrong_ztrailer_zero() {
-    test_sack_sysfile("zcompressed_data_wrong_ztrailer_zero");
-}
-
-#[test]
-fn zcompressed_data_wrong_block_size() {
-    test_sack_sysfile("zcompressed_data_wrong_block_size");
-}
-
-#[test]
-fn zcompressed_data_wrong_n_blocks() {
-    test_sack_sysfile("zcompressed_data_wrong_n_blocks");
-}
-
-#[test]
-fn zcompressed_data_wrong_uncompressed_ofs() {
-    test_sack_sysfile("zcompressed_data_wrong_uncompressed_ofs");
-}
-
-#[test]
-fn zcompressed_data_wrong_compressed_ofs() {
-    test_sack_sysfile("zcompressed_data_wrong_compressed_ofs");
-}
-
-#[test]
-fn zcompressed_data_compressed_sizes_dont_add_up() {
-    test_sack_sysfile("zcompressed_data_compressed_sizes_dont_add_up");
-}
-
-#[test]
-fn zcompressed_data_uncompressed_size_block_size() {
-    test_sack_sysfile("zcompressed_data_uncompressed_size_block_size");
-}
-
-#[test]
-fn zcompressed_data_compression_expands_data_too_much() {
-    test_sack_sysfile("zcompressed_data_compression_expands_data_too_much");
-}
-
-#[test]
-fn zcompressed_data_compressed_sizes_don_t_add_up() {
-    test_sack_sysfile("zcompressed_data_compressed_sizes_don_t_add_up");
-}
-
-/// CVE-2017-10791.
-/// See also https://bugzilla.redhat.com/show_bug.cgi?id=1467004.
-/// See also https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=866890.
-/// See also https://security-tracker.debian.org/tracker/CVE-2017-10791.
-/// Found by team OWL337, using the collAFL fuzzer.
-#[test]
-fn integer_overflows_in_long_string_missing_values() {
-    test_raw_sysfile("integer_overflows_in_long_string_missing_values");
-}
-
-/// CVE-2017-10792.
-/// See also https://bugzilla.redhat.com/show_bug.cgi?id=1467005.
-/// See also https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=866890.
-/// See also https://security-tracker.debian.org/tracker/CVE-2017-10792.
-/// Reported by team OWL337, with fuzzer collAFL.
-#[test]
-fn null_dereference_skipping_bad_extension_record_18() {
-    test_raw_sysfile("null_dereference_skipping_bad_extension_record_18");
-}
-
-/// Duplicate variable name handling negative test.
-///
-/// SPSS-generated system file can contain duplicate variable names (see bug
-/// #41475).
-#[test]
-fn duplicate_variable_name() {
-    test_sack_sysfile("duplicate_variable_name");
-}
-
-#[test]
-fn encrypted_file() {
-    test_encrypted_sysfile("test-encrypted.sav", "pspp");
-}
-
-#[test]
-fn encrypted_file_without_password() {
-    let error = ReadOptions::new(|_| {
-        panic!();
-    })
-    .open_file("src/crypto/testdata/test-encrypted.sav")
-    .unwrap_err();
-    assert!(matches!(
-        error.downcast::<raw::Error>().unwrap().details,
-        ErrorDetails::Encrypted
-    ));
-}
-
-/// Tests the most basic kind of writing a system file, just writing a few
-/// numeric variables and cases.
-fn write_numeric(compression: Option<Compression>, compression_string: &str) {
-    let mut dictionary = Dictionary::new(UTF_8);
-    for i in 0..4 {
-        let name = Identifier::new(format!("variable{i}")).unwrap();
-        dictionary
-            .add_var(Variable::new(name, VarWidth::Numeric, UTF_8))
-            .unwrap();
-    }
-    let mut cases = WriteOptions::reproducible(compression)
-        .write_writer(&dictionary, Cursor::new(Vec::new()))
-        .unwrap();
-    for case in [
-        [1, 1, 1, 2],
-        [1, 1, 2, 30],
-        [1, 2, 1, 8],
-        [1, 2, 2, 20],
-        [2, 1, 1, 2],
-        [2, 1, 2, 22],
-        [2, 2, 1, 1],
-        [2, 2, 2, 3],
-    ] {
-        cases
-            .write_case(
-                case.into_iter()
-                    .map(|number| Datum::<&str>::Number(Some(number as f64))),
-            )
-            .unwrap();
-    }
-    let sysfile = cases.finish().unwrap().unwrap().into_inner();
-    let expected_filename = PathBuf::from(&format!(
-        "src/sys/testdata/write-numeric-{compression_string}.expected"
-    ));
-    let expected = String::from_utf8(std::fs::read(&expected_filename).unwrap()).unwrap();
-    test_sysfile(Cursor::new(sysfile), &expected, &expected_filename);
-}
-
-#[test]
-fn write_numeric_uncompressed() {
-    write_numeric(None, "uncompressed");
-}
-
-#[test]
-fn write_numeric_simple() {
-    write_numeric(Some(Compression::Simple), "simple");
-}
-
-#[test]
-fn write_numeric_zlib() {
-    write_numeric(Some(Compression::ZLib), "zlib");
-}
-
-/// Tests writing string data.
-fn write_string(compression: Option<Compression>, compression_string: &str) {
-    let mut dictionary = Dictionary::new(UTF_8);
-    dictionary
-        .add_var(Variable::new(
-            Identifier::new("s1").unwrap(),
-            VarWidth::String(1),
-            UTF_8,
-        ))
-        .unwrap();
-
-    dictionary
-        .add_var(Variable::new(
-            Identifier::new("s2").unwrap(),
-            VarWidth::String(2),
-            UTF_8,
-        ))
-        .unwrap();
-
-    dictionary
-        .add_var(Variable::new(
-            Identifier::new("s3").unwrap(),
-            VarWidth::String(3),
-            UTF_8,
-        ))
-        .unwrap();
-
-    dictionary
-        .add_var(Variable::new(
-            Identifier::new("s4").unwrap(),
-            VarWidth::String(9),
-            UTF_8,
-        ))
-        .unwrap();
-
-    dictionary
-        .add_var(Variable::new(
-            Identifier::new("s566").unwrap(),
-            VarWidth::String(566),
-            UTF_8,
-        ))
-        .unwrap();
-
-    let mut cases = WriteOptions::reproducible(compression)
-        .write_writer(&dictionary, Cursor::new(Vec::new()))
-        .unwrap();
-    for case in [
-            ["1", "1", "1", "xyzzyquux", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\n"],
-            ["1", "2", "1", "8", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],
-        ] {
-            cases
-                .write_case(case.into_iter().map(|s| Datum::String(s)))
-                .unwrap();
-        }
-    let sysfile = cases.finish().unwrap().unwrap().into_inner();
-    let expected_filename = PathBuf::from(&format!(
-        "src/sys/testdata/write-string-{compression_string}.expected"
-    ));
-    let expected = String::from_utf8(std::fs::read(&expected_filename).unwrap()).unwrap();
-    test_sysfile(Cursor::new(sysfile), &expected, &expected_filename);
-}
-
-#[test]
-fn write_string_uncompressed() {
-    write_string(None, "uncompressed");
-}
-
-#[test]
-fn write_string_simple() {
-    write_string(Some(Compression::Simple), "simple");
-}
-
-#[test]
-fn write_string_zlib() {
-    write_string(Some(Compression::ZLib), "zlib");
-}
-
-fn test_raw_sysfile(name: &str) {
-    let input_filename = Path::new("src/sys/testdata")
-        .join(name)
-        .with_extension("sav");
-    let sysfile = BufReader::new(File::open(&input_filename).unwrap());
-    let expected_filename = input_filename.with_extension("expected");
-    let expected = String::from_utf8(std::fs::read(&expected_filename).unwrap()).unwrap();
-    test_sysfile(sysfile, &expected, &expected_filename);
-}
-
-fn test_encrypted_sysfile(name: &str, password: &str) {
-    let input_filename = Path::new("src/sys/testdata")
-        .join(name)
-        .with_extension("sav");
-    let sysfile = EncryptedFile::new(File::open(&input_filename).unwrap())
-        .unwrap()
-        .unlock(password.as_bytes())
-        .unwrap();
-    let expected_filename = input_filename.with_extension("expected");
-    let expected = String::from_utf8(std::fs::read(&expected_filename).unwrap()).unwrap();
-    test_sysfile(sysfile, &expected, &expected_filename);
-}
-
-fn test_sack_sysfile(name: &str) {
-    let input_filename = Path::new("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 [Endian::Big, Endian::Little] {
-        let expected = expected.replace(
-            "{endian}",
-            match endian {
-                Endian::Big => "1",
-                Endian::Little => "2",
-            },
-        );
-        let sysfile = sack(&input, Some(&input_filename), endian).unwrap();
-        test_sysfile(Cursor::new(sysfile), &expected, &expected_filename);
-    }
-}
-
-fn test_sysfile<R>(sysfile: R, expected: &str, expected_filename: &Path)
-where
-    R: BufRead + Seek + 'static,
-{
-    let mut warnings = Vec::new();
-    let output = match ReadOptions::new(|warning| warnings.push(warning)).open_reader(sysfile) {
-        Ok(system_file) => {
-            let (dictionary, metadata, cases) = system_file.into_parts();
-
-            let mut output = Vec::new();
-            output.extend(
-                warnings
-                    .into_iter()
-                    .map(|warning| Item::from(Text::new_log(warning.to_string()))),
-            );
-            output.push(PivotTable::from(&metadata).into());
-            output.extend(dictionary.all_pivot_tables().into_iter().map_into());
-            let variables =
-                Group::new("Variable").with_multiple(dictionary.variables.iter().map(|var| &**var));
-            let mut case_numbers = Group::new("Case").with_label_shown();
-            let mut data = Vec::new();
-            for case in cases {
-                match case {
-                    Ok(case) => {
-                        case_numbers
-                            .push(Value::new_integer(Some((case_numbers.len() + 1) as f64)));
-                        data.push(
-                            case.into_iter()
-                                .map(|datum| Value::new_datum(&datum))
-                                .collect::<Vec<_>>(),
-                        );
-                    }
-                    Err(error) => {
-                        output.push(Item::from(Text::new_log(error.to_string())));
-                    }
-                }
-            }
-            if !data.is_empty() {
-                let mut pt = PivotTable::new([
-                    (Axis3::X, Dimension::new(variables)),
-                    (Axis3::Y, Dimension::new(case_numbers)),
-                ]);
-                for (row_number, row) in data.into_iter().enumerate() {
-                    for (column_number, datum) in row.into_iter().enumerate() {
-                        pt.insert(&[column_number, row_number], datum);
-                    }
-                }
-                output.push(pt.into());
-            }
-            Item::new(Details::Group(output.into_iter().map(Arc::new).collect()))
-        }
-        Err(error) => Item::new(Details::Text(Box::new(Text::new_log(error.to_string())))),
-    };
-
-    let actual = output.to_string();
-    if expected != actual {
-        if std::env::var("PSPP_REFRESH_EXPECTED").is_ok() {
-            std::fs::write(expected_filename, actual).unwrap();
-            panic!("{}: refreshed output", expected_filename.display());
-        } else {
-            eprintln!("note: rerun with PSPP_REFRESH_EXPECTED=1 to refresh expected output");
-        }
-    }
-    assert_lines_eq(&expected, expected_filename.display(), &actual, "actual");
-}
diff --git a/rust/pspp/src/sys/tests.rs b/rust/pspp/src/sys/tests.rs
new file mode 100644 (file)
index 0000000..64750fe
--- /dev/null
@@ -0,0 +1,812 @@
+// PSPP - a program for statistical analysis.
+// Copyright (C) 2025 Free Software Foundation, Inc.
+//
+// This program is free software: you can redistribute it and/or modify it under
+// the terms of the GNU General Public License as published by the Free Software
+// Foundation, either version 3 of the License, or (at your option) any later
+// version.
+//
+// This program is distributed in the hope that it will be useful, but WITHOUT
+// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+// FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
+// details.
+//
+// You should have received a copy of the GNU General Public License along with
+// this program.  If not, see <http://www.gnu.org/licenses/>.
+
+use std::{
+    fs::File,
+    io::{BufRead, BufReader, Cursor, Seek},
+    path::{Path, PathBuf},
+    sync::Arc,
+};
+
+use binrw::Endian;
+use encoding_rs::UTF_8;
+use itertools::Itertools;
+
+use crate::{
+    crypto::EncryptedFile,
+    data::Datum,
+    dictionary::Dictionary,
+    identifier::Identifier,
+    output::{
+        pivot::{tests::assert_lines_eq, Axis3, Dimension, Group, PivotTable, Value},
+        Details, Item, Text,
+    },
+    sys::{
+        cooked::ReadOptions,
+        raw::{self, records::Compression, ErrorDetails},
+        sack::sack,
+        WriteOptions,
+    },
+    variable::{VarWidth, Variable},
+};
+
+#[test]
+fn variable_labels_and_missing_values() {
+    test_sack_sysfile("variable_labels_and_missing_values");
+}
+
+#[test]
+fn unspecified_number_of_variable_positions() {
+    test_sack_sysfile("unspecified_number_of_variable_positions");
+}
+
+#[test]
+fn wrong_variable_positions_but_v13() {
+    test_sack_sysfile("wrong_variable_positions_but_v13");
+}
+
+#[test]
+fn value_labels() {
+    test_sack_sysfile("value_labels");
+}
+
+#[test]
+fn documents() {
+    test_sack_sysfile("documents");
+}
+
+#[test]
+fn empty_document_record() {
+    test_sack_sysfile("empty_document_record");
+}
+
+#[test]
+fn variable_sets() {
+    test_sack_sysfile("variable_sets");
+}
+
+#[test]
+fn multiple_response_sets() {
+    test_sack_sysfile("multiple_response_sets");
+}
+
+#[test]
+fn extra_product_info() {
+    // Also checks for handling of CR-only line ends in file label and extra
+    // product info.
+    test_sack_sysfile("extra_product_info");
+}
+
+#[test]
+fn variable_display_without_width() {
+    test_sack_sysfile("variable_display_without_width");
+}
+
+#[test]
+fn variable_display_with_width() {
+    test_sack_sysfile("variable_display_with_width");
+}
+
+#[test]
+fn long_variable_names() {
+    test_sack_sysfile("long_variable_names");
+}
+
+#[test]
+fn very_long_strings() {
+    test_sack_sysfile("very_long_strings");
+}
+
+#[test]
+fn attributes() {
+    test_sack_sysfile("attributes");
+}
+
+#[test]
+fn variable_roles() {
+    test_sack_sysfile("variable_roles");
+}
+
+#[test]
+fn compressed_data() {
+    test_sack_sysfile("compressed_data");
+}
+
+#[test]
+fn compressed_data_zero_bias() {
+    test_sack_sysfile("compressed_data_zero_bias");
+}
+
+#[test]
+fn compressed_data_other_bias() {
+    test_sack_sysfile("compressed_data_other_bias");
+}
+
+#[test]
+fn zcompressed_data() {
+    test_sack_sysfile("zcompressed_data");
+}
+
+#[test]
+fn no_variables() {
+    test_sack_sysfile("no_variables");
+}
+
+#[test]
+fn unknown_encoding() {
+    test_sack_sysfile("unknown_encoding");
+}
+
+#[test]
+fn misplaced_type_4_record() {
+    test_sack_sysfile("misplaced_type_4_record");
+}
+
+#[test]
+fn bad_record_type() {
+    test_sack_sysfile("bad_record_type");
+}
+
+#[test]
+fn wrong_variable_positions() {
+    test_sack_sysfile("wrong_variable_positions");
+}
+
+#[test]
+fn invalid_variable_name() {
+    test_sack_sysfile("invalid_variable_name");
+}
+
+#[test]
+fn invalid_label_indicator() {
+    test_sack_sysfile("invalid_label_indicator");
+}
+
+#[test]
+fn invalid_missing_indicator() {
+    test_sack_sysfile("invalid_missing_indicator");
+}
+
+#[test]
+fn invalid_missing_indicator2() {
+    test_sack_sysfile("invalid_missing_indicator2");
+}
+
+#[test]
+fn missing_string_continuation() {
+    test_sack_sysfile("missing_string_continuation");
+}
+
+#[test]
+fn invalid_variable_format() {
+    test_sack_sysfile("invalid_variable_format");
+}
+
+#[test]
+fn invalid_long_string_missing_values() {
+    test_sack_sysfile("invalid_long_string_missing_values");
+}
+
+#[test]
+fn weight_must_be_numeric() {
+    test_sack_sysfile("weight_must_be_numeric");
+}
+
+#[test]
+fn weight_variable_bad_index() {
+    test_sack_sysfile("weight_variable_bad_index");
+}
+
+#[test]
+fn weight_variable_continuation() {
+    test_sack_sysfile("weight_variable_continuation");
+}
+
+#[test]
+fn multiple_documents_records() {
+    test_sack_sysfile("multiple_documents_records");
+}
+
+#[test]
+fn unknown_extension_record() {
+    test_sack_sysfile("unknown_extension_record");
+}
+
+#[test]
+fn extension_too_large() {
+    test_sack_sysfile("extension_too_large");
+}
+
+#[test]
+fn bad_machine_integer_info_count() {
+    test_sack_sysfile("bad_machine_integer_info_count");
+}
+
+#[test]
+fn bad_machine_integer_info_float_format() {
+    test_sack_sysfile("bad_machine_integer_info_float_format");
+}
+
+#[test]
+fn bad_machine_integer_info_endianness() {
+    test_sack_sysfile("bad_machine_integer_info_endianness");
+}
+
+#[test]
+fn bad_machine_float_info_size() {
+    test_sack_sysfile("bad_machine_float_info_size");
+}
+
+#[test]
+fn wrong_special_floats() {
+    test_sack_sysfile("wrong_special_floats");
+}
+
+#[test]
+fn variable_sets_unknown_variable() {
+    test_sack_sysfile("variable_sets_unknown_variable");
+}
+
+#[test]
+fn multiple_response_sets_bad_name() {
+    test_sack_sysfile("multiple_response_sets_bad_name");
+}
+
+#[test]
+fn multiple_response_sets_missing_space_after_c() {
+    test_sack_sysfile("multiple_response_sets_missing_space_after_c");
+}
+
+#[test]
+fn multiple_response_sets_missing_space_after_e() {
+    test_sack_sysfile("multiple_response_sets_missing_space_after_e");
+}
+
+#[test]
+fn multiple_response_sets_missing_label_source() {
+    test_sack_sysfile("multiple_response_sets_missing_label_source");
+}
+
+#[test]
+fn multiple_response_sets_unexpected_label_source() {
+    test_sack_sysfile("multiple_response_sets_unexpected_label_source");
+}
+
+#[test]
+fn multiple_response_sets_bad_counted_string() {
+    test_sack_sysfile("multiple_response_sets_bad_counted_string");
+}
+
+#[test]
+fn multiple_response_sets_counted_string_missing_space() {
+    test_sack_sysfile("multiple_response_sets_counted_string_missing_space");
+}
+
+#[test]
+fn multiple_response_sets_counted_string_bad_length() {
+    test_sack_sysfile("multiple_response_sets_counted_string_bad_length");
+}
+
+#[test]
+fn multiple_response_sets_missing_space_after_counted_string() {
+    test_sack_sysfile("multiple_response_sets_missing_space_after_counted_string");
+}
+
+#[test]
+fn multiple_response_sets_missing_newline_after_variable_name() {
+    test_sack_sysfile("multiple_response_sets_missing_newline_after_variable_name");
+}
+
+#[test]
+fn multiple_response_sets_duplicate_variable_name() {
+    test_sack_sysfile("multiple_response_sets_duplicate_variable_name");
+}
+
+#[test]
+fn mixed_variable_types_in_mrsets() {
+    test_sack_sysfile("mixed_variable_types_in_mrsets");
+}
+
+#[test]
+fn missing_newline_after_variable_name_in_mrsets() {
+    test_sack_sysfile("missing_newline_after_variable_name_in_mrsets");
+}
+
+#[test]
+fn zero_or_one_variable_in_mrset() {
+    test_sack_sysfile("zero_or_one_variable_in_mrset");
+}
+
+#[test]
+fn wrong_display_parameter_size() {
+    test_sack_sysfile("wrong_display_parameter_size");
+}
+
+#[test]
+fn wrong_display_parameter_count() {
+    test_sack_sysfile("wrong_display_parameter_count");
+}
+
+#[test]
+fn wrong_display_measurement_level() {
+    test_sack_sysfile("wrong_display_measurement_level");
+}
+
+#[test]
+fn wrong_display_alignment() {
+    test_sack_sysfile("wrong_display_alignment");
+}
+
+#[test]
+fn bad_variable_name_in_variable_value_pair() {
+    test_sack_sysfile("bad_variable_name_in_variable_value_pair");
+}
+
+#[test]
+fn duplicate_long_variable_name() {
+    test_sack_sysfile("duplicate_long_variable_name");
+}
+
+#[test]
+fn bad_very_long_string_length() {
+    test_sack_sysfile("bad_very_long_string_length");
+}
+
+#[test]
+fn bad_very_long_string_segment_width() {
+    test_sack_sysfile("bad_very_long_string_segment_width");
+}
+
+#[test]
+fn too_many_value_labels() {
+    test_sack_sysfile("too_many_value_labels");
+}
+
+#[test]
+fn missing_type_4_record() {
+    test_sack_sysfile("missing_type_4_record");
+}
+
+#[test]
+fn value_label_with_no_associated_variables() {
+    test_sack_sysfile("value_label_with_no_associated_variables");
+}
+
+#[test]
+fn type_4_record_names_long_string_variable() {
+    test_sack_sysfile("type_4_record_names_long_string_variable");
+}
+
+#[test]
+fn value_label_variable_indexes_must_be_in_correct_range() {
+    test_sack_sysfile("value_label_variable_indexes_must_be_in_correct_range");
+}
+
+#[test]
+fn value_label_variable_indexes_must_not_be_long_string_continuation() {
+    test_sack_sysfile("value_label_variable_indexes_must_not_be_long_string_continuation");
+}
+
+#[test]
+fn variables_for_value_label_must_all_be_same_type() {
+    test_sack_sysfile("variables_for_value_label_must_all_be_same_type");
+}
+
+#[test]
+fn duplicate_value_labels_type() {
+    test_sack_sysfile("duplicate_value_labels_type");
+}
+
+#[test]
+fn missing_attribute_value() {
+    test_sack_sysfile("missing_attribute_value");
+}
+
+#[test]
+fn unquoted_attribute_value() {
+    test_sack_sysfile("unquoted_attribute_value");
+}
+
+#[test]
+fn duplicate_attribute_name() {
+    test_sack_sysfile("duplicate_attribute_name");
+}
+
+#[test]
+fn bad_variable_name_in_long_string_value_label() {
+    test_sack_sysfile("bad_variable_name_in_long_string_value_label");
+}
+
+#[test]
+fn fewer_data_records_than_indicated_by_file_header() {
+    test_sack_sysfile("fewer_data_records_than_indicated_by_file_header");
+}
+
+#[test]
+fn more_data_records_than_indicated_by_file_header() {
+    test_sack_sysfile("more_data_records_than_indicated_by_file_header");
+}
+
+#[test]
+fn partial_data_record_between_variables() {
+    test_sack_sysfile("partial_data_record_between_variables");
+}
+
+#[test]
+fn partial_data_record_within_long_string() {
+    test_sack_sysfile("partial_data_record_within_long_string");
+}
+
+#[test]
+fn partial_compressed_data_record() {
+    test_sack_sysfile("partial_compressed_data_record");
+}
+
+#[test]
+fn zcompressed_data_bad_zheader_ofs() {
+    test_sack_sysfile("zcompressed_data_bad_zheader_ofs");
+}
+
+#[test]
+fn zcompressed_data_bad_ztrailer_ofs() {
+    test_sack_sysfile("zcompressed_data_bad_ztrailer_ofs");
+}
+
+#[test]
+fn zcompressed_data_invalid_ztrailer_len() {
+    test_sack_sysfile("zcompressed_data_invalid_ztrailer_len");
+}
+
+#[test]
+fn zcompressed_data_wrong_ztrailer_len() {
+    test_sack_sysfile("zcompressed_data_wrong_ztrailer_len");
+}
+
+#[test]
+fn zcompressed_data_wrong_ztrailer_bias() {
+    test_sack_sysfile("zcompressed_data_wrong_ztrailer_bias");
+}
+
+#[test]
+fn zcompressed_data_wrong_ztrailer_zero() {
+    test_sack_sysfile("zcompressed_data_wrong_ztrailer_zero");
+}
+
+#[test]
+fn zcompressed_data_wrong_block_size() {
+    test_sack_sysfile("zcompressed_data_wrong_block_size");
+}
+
+#[test]
+fn zcompressed_data_wrong_n_blocks() {
+    test_sack_sysfile("zcompressed_data_wrong_n_blocks");
+}
+
+#[test]
+fn zcompressed_data_wrong_uncompressed_ofs() {
+    test_sack_sysfile("zcompressed_data_wrong_uncompressed_ofs");
+}
+
+#[test]
+fn zcompressed_data_wrong_compressed_ofs() {
+    test_sack_sysfile("zcompressed_data_wrong_compressed_ofs");
+}
+
+#[test]
+fn zcompressed_data_compressed_sizes_dont_add_up() {
+    test_sack_sysfile("zcompressed_data_compressed_sizes_dont_add_up");
+}
+
+#[test]
+fn zcompressed_data_uncompressed_size_block_size() {
+    test_sack_sysfile("zcompressed_data_uncompressed_size_block_size");
+}
+
+#[test]
+fn zcompressed_data_compression_expands_data_too_much() {
+    test_sack_sysfile("zcompressed_data_compression_expands_data_too_much");
+}
+
+#[test]
+fn zcompressed_data_compressed_sizes_don_t_add_up() {
+    test_sack_sysfile("zcompressed_data_compressed_sizes_don_t_add_up");
+}
+
+/// CVE-2017-10791.
+/// See also https://bugzilla.redhat.com/show_bug.cgi?id=1467004.
+/// See also https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=866890.
+/// See also https://security-tracker.debian.org/tracker/CVE-2017-10791.
+/// Found by team OWL337, using the collAFL fuzzer.
+#[test]
+fn integer_overflows_in_long_string_missing_values() {
+    test_raw_sysfile("integer_overflows_in_long_string_missing_values");
+}
+
+/// CVE-2017-10792.
+/// See also https://bugzilla.redhat.com/show_bug.cgi?id=1467005.
+/// See also https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=866890.
+/// See also https://security-tracker.debian.org/tracker/CVE-2017-10792.
+/// Reported by team OWL337, with fuzzer collAFL.
+#[test]
+fn null_dereference_skipping_bad_extension_record_18() {
+    test_raw_sysfile("null_dereference_skipping_bad_extension_record_18");
+}
+
+/// Duplicate variable name handling negative test.
+///
+/// SPSS-generated system file can contain duplicate variable names (see bug
+/// #41475).
+#[test]
+fn duplicate_variable_name() {
+    test_sack_sysfile("duplicate_variable_name");
+}
+
+#[test]
+fn encrypted_file() {
+    test_encrypted_sysfile("test-encrypted.sav", "pspp");
+}
+
+#[test]
+fn encrypted_file_without_password() {
+    let error = ReadOptions::new(|_| {
+        panic!();
+    })
+    .open_file("src/crypto/testdata/test-encrypted.sav")
+    .unwrap_err();
+    assert!(matches!(
+        error.downcast::<raw::Error>().unwrap().details,
+        ErrorDetails::Encrypted
+    ));
+}
+
+/// Tests the most basic kind of writing a system file, just writing a few
+/// numeric variables and cases.
+fn write_numeric(compression: Option<Compression>, compression_string: &str) {
+    let mut dictionary = Dictionary::new(UTF_8);
+    for i in 0..4 {
+        let name = Identifier::new(format!("variable{i}")).unwrap();
+        dictionary
+            .add_var(Variable::new(name, VarWidth::Numeric, UTF_8))
+            .unwrap();
+    }
+    let mut cases = WriteOptions::reproducible(compression)
+        .write_writer(&dictionary, Cursor::new(Vec::new()))
+        .unwrap();
+    for case in [
+        [1, 1, 1, 2],
+        [1, 1, 2, 30],
+        [1, 2, 1, 8],
+        [1, 2, 2, 20],
+        [2, 1, 1, 2],
+        [2, 1, 2, 22],
+        [2, 2, 1, 1],
+        [2, 2, 2, 3],
+    ] {
+        cases
+            .write_case(
+                case.into_iter()
+                    .map(|number| Datum::<&str>::Number(Some(number as f64))),
+            )
+            .unwrap();
+    }
+    let sysfile = cases.finish().unwrap().unwrap().into_inner();
+    let expected_filename = PathBuf::from(&format!(
+        "src/sys/testdata/write-numeric-{compression_string}.expected"
+    ));
+    let expected = String::from_utf8(std::fs::read(&expected_filename).unwrap()).unwrap();
+    test_sysfile(Cursor::new(sysfile), &expected, &expected_filename);
+}
+
+#[test]
+fn write_numeric_uncompressed() {
+    write_numeric(None, "uncompressed");
+}
+
+#[test]
+fn write_numeric_simple() {
+    write_numeric(Some(Compression::Simple), "simple");
+}
+
+#[test]
+fn write_numeric_zlib() {
+    write_numeric(Some(Compression::ZLib), "zlib");
+}
+
+/// Tests writing string data.
+fn write_string(compression: Option<Compression>, compression_string: &str) {
+    let mut dictionary = Dictionary::new(UTF_8);
+    dictionary
+        .add_var(Variable::new(
+            Identifier::new("s1").unwrap(),
+            VarWidth::String(1),
+            UTF_8,
+        ))
+        .unwrap();
+
+    dictionary
+        .add_var(Variable::new(
+            Identifier::new("s2").unwrap(),
+            VarWidth::String(2),
+            UTF_8,
+        ))
+        .unwrap();
+
+    dictionary
+        .add_var(Variable::new(
+            Identifier::new("s3").unwrap(),
+            VarWidth::String(3),
+            UTF_8,
+        ))
+        .unwrap();
+
+    dictionary
+        .add_var(Variable::new(
+            Identifier::new("s4").unwrap(),
+            VarWidth::String(9),
+            UTF_8,
+        ))
+        .unwrap();
+
+    dictionary
+        .add_var(Variable::new(
+            Identifier::new("s566").unwrap(),
+            VarWidth::String(566),
+            UTF_8,
+        ))
+        .unwrap();
+
+    let mut cases = WriteOptions::reproducible(compression)
+        .write_writer(&dictionary, Cursor::new(Vec::new()))
+        .unwrap();
+    for case in [
+            ["1", "1", "1", "xyzzyquux", "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\nabcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789\n"],
+            ["1", "2", "1", "8", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx\nxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"],
+        ] {
+            cases
+                .write_case(case.into_iter().map(|s| Datum::String(s)))
+                .unwrap();
+        }
+    let sysfile = cases.finish().unwrap().unwrap().into_inner();
+    let expected_filename = PathBuf::from(&format!(
+        "src/sys/testdata/write-string-{compression_string}.expected"
+    ));
+    let expected = String::from_utf8(std::fs::read(&expected_filename).unwrap()).unwrap();
+    test_sysfile(Cursor::new(sysfile), &expected, &expected_filename);
+}
+
+#[test]
+fn write_string_uncompressed() {
+    write_string(None, "uncompressed");
+}
+
+#[test]
+fn write_string_simple() {
+    write_string(Some(Compression::Simple), "simple");
+}
+
+#[test]
+fn write_string_zlib() {
+    write_string(Some(Compression::ZLib), "zlib");
+}
+
+fn test_raw_sysfile(name: &str) {
+    let input_filename = Path::new("src/sys/testdata")
+        .join(name)
+        .with_extension("sav");
+    let sysfile = BufReader::new(File::open(&input_filename).unwrap());
+    let expected_filename = input_filename.with_extension("expected");
+    let expected = String::from_utf8(std::fs::read(&expected_filename).unwrap()).unwrap();
+    test_sysfile(sysfile, &expected, &expected_filename);
+}
+
+fn test_encrypted_sysfile(name: &str, password: &str) {
+    let input_filename = Path::new("src/sys/testdata")
+        .join(name)
+        .with_extension("sav");
+    let sysfile = EncryptedFile::new(File::open(&input_filename).unwrap())
+        .unwrap()
+        .unlock(password.as_bytes())
+        .unwrap();
+    let expected_filename = input_filename.with_extension("expected");
+    let expected = String::from_utf8(std::fs::read(&expected_filename).unwrap()).unwrap();
+    test_sysfile(sysfile, &expected, &expected_filename);
+}
+
+fn test_sack_sysfile(name: &str) {
+    let input_filename = Path::new("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 [Endian::Big, Endian::Little] {
+        let expected = expected.replace(
+            "{endian}",
+            match endian {
+                Endian::Big => "1",
+                Endian::Little => "2",
+            },
+        );
+        let sysfile = sack(&input, Some(&input_filename), endian).unwrap();
+        test_sysfile(Cursor::new(sysfile), &expected, &expected_filename);
+    }
+}
+
+fn test_sysfile<R>(sysfile: R, expected: &str, expected_filename: &Path)
+where
+    R: BufRead + Seek + 'static,
+{
+    let mut warnings = Vec::new();
+    let output = match ReadOptions::new(|warning| warnings.push(warning)).open_reader(sysfile) {
+        Ok(system_file) => {
+            let (dictionary, metadata, cases) = system_file.into_parts();
+
+            let mut output = Vec::new();
+            output.extend(
+                warnings
+                    .into_iter()
+                    .map(|warning| Item::from(Text::new_log(warning.to_string()))),
+            );
+            output.push(PivotTable::from(&metadata).into());
+            output.extend(dictionary.all_pivot_tables().into_iter().map_into());
+            let variables =
+                Group::new("Variable").with_multiple(dictionary.variables.iter().map(|var| &**var));
+            let mut case_numbers = Group::new("Case").with_label_shown();
+            let mut data = Vec::new();
+            for case in cases {
+                match case {
+                    Ok(case) => {
+                        case_numbers
+                            .push(Value::new_integer(Some((case_numbers.len() + 1) as f64)));
+                        data.push(
+                            case.into_iter()
+                                .map(|datum| Value::new_datum(&datum))
+                                .collect::<Vec<_>>(),
+                        );
+                    }
+                    Err(error) => {
+                        output.push(Item::from(Text::new_log(error.to_string())));
+                    }
+                }
+            }
+            if !data.is_empty() {
+                let mut pt = PivotTable::new([
+                    (Axis3::X, Dimension::new(variables)),
+                    (Axis3::Y, Dimension::new(case_numbers)),
+                ]);
+                for (row_number, row) in data.into_iter().enumerate() {
+                    for (column_number, datum) in row.into_iter().enumerate() {
+                        pt.insert(&[column_number, row_number], datum);
+                    }
+                }
+                output.push(pt.into());
+            }
+            Item::new(Details::Group(output.into_iter().map(Arc::new).collect()))
+        }
+        Err(error) => Item::new(Details::Text(Box::new(Text::new_log(error.to_string())))),
+    };
+
+    let actual = output.to_string();
+    if expected != actual {
+        if std::env::var("PSPP_REFRESH_EXPECTED").is_ok() {
+            std::fs::write(expected_filename, actual).unwrap();
+            panic!("{}: refreshed output", expected_filename.display());
+        } else {
+            eprintln!("note: rerun with PSPP_REFRESH_EXPECTED=1 to refresh expected output");
+        }
+    }
+    assert_lines_eq(&expected, expected_filename.display(), &actual, "actual");
+}