From: Ben Pfaff Date: Sun, 14 Sep 2025 21:02:59 +0000 (-0700) Subject: rust: Rename `test` modules to `tests` for consistency. X-Git-Url: https://pintos-os.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=937bf791e0da2137a4189b2e37bd3337449bccbd;p=pspp rust: Rename `test` modules to `tests` for consistency. --- diff --git a/rust/pspp/src/crypto.rs b/rust/pspp/src/crypto.rs index c2e86cdea1..0d1282f74c 100644 --- a/rust/pspp/src/crypto.rs +++ b/rust/pspp/src/crypto.rs @@ -581,7 +581,7 @@ impl EncodedPassword { } #[cfg(test)] -mod test { +mod tests { use std::{io::Cursor, path::Path}; use crate::crypto::{EncodedPassword, EncryptedFile, FileType}; diff --git a/rust/pspp/src/format/display.rs b/rust/pspp/src/format/display.rs index 5b3bbe2ccc..93cb275c9e 100644 --- a/rust/pspp/src/format/display.rs +++ b/rust/pspp/src/format/display.rs @@ -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 index 9ddd3047f2..0000000000 --- a/rust/pspp/src/format/display/test.rs +++ /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 . - -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::>(); - 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::() - .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::>::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::>::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::>::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::>(); - 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::() - .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::>::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 index 0000000000..9ddd3047f2 --- /dev/null +++ b/rust/pspp/src/format/display/tests.rs @@ -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 . + +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::>(); + 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::() + .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::>::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::>::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::>::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::>(); + 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::() + .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::>::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/parse.rs b/rust/pspp/src/format/parse.rs index f6a795ed2a..2161e3dc0a 100644 --- a/rust/pspp/src/format/parse.rs +++ b/rust/pspp/src/format/parse.rs @@ -910,7 +910,7 @@ fn nibble(b: u8) -> Result { } #[cfg(test)] -mod test { +mod tests { use std::{ fs::File, io::{BufRead, BufReader}, diff --git a/rust/pspp/src/lex/scan.rs b/rust/pspp/src/lex/scan.rs index fcb1bc3416..4a3d2193f3 100644 --- a/rust/pspp/src/lex/scan.rs +++ b/rust/pspp/src/lex/scan.rs @@ -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 index 6f0e582cda..0000000000 --- a/rust/pspp/src/lex/scan/test.rs +++ /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 . - -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]) { - let tokens = StringScanner::new(input, mode, false).collect::>(); - - 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 index 0000000000..6f0e582cda --- /dev/null +++ b/rust/pspp/src/lex/scan/tests.rs @@ -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 . + +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]) { + let tokens = StringScanner::new(input, mode, false).collect::>(); + + 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/segment.rs b/rust/pspp/src/lex/segment.rs index 5a568692b5..27313a7184 100644 --- a/rust/pspp/src/lex/segment.rs +++ b/rust/pspp/src/lex/segment.rs @@ -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 index 12b6591c1c..0000000000 --- a/rust/pspp/src/lex/segment/test.rs +++ /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 . - -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::>(), - 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 = (0..N) - .map(|i| format!("do repeat v{i}={i} thru {}.\n", i + 5)) - .collect(); - let end_repeat: Vec = (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 = (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 index 0000000000..12b6591c1c --- /dev/null +++ b/rust/pspp/src/lex/segment/tests.rs @@ -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 . + +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::>(), + 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 = (0..N) + .map(|i| format!("do repeat v{i}={i} thru {}.\n", i + 5)) + .collect(); + let end_repeat: Vec = (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 = (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/token.rs b/rust/pspp/src/lex/token.rs index 27cc2b1a55..be0cad9f87 100644 --- a/rust/pspp/src/lex/token.rs +++ b/rust/pspp/src/lex/token.rs @@ -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] diff --git a/rust/pspp/src/output/cairo.rs b/rust/pspp/src/output/cairo.rs index 0d6782f142..260e5c3e2c 100644 --- a/rust/pspp/src/output/cairo.rs +++ b/rust/pspp/src/output/cairo.rs @@ -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] diff --git a/rust/pspp/src/output/pivot.rs b/rust/pspp/src/output/pivot.rs index 92133e2c51..2c9f17b307 100644 --- a/rust/pspp/src/output/pivot.rs +++ b/rust/pspp/src/output/pivot.rs @@ -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] diff --git a/rust/pspp/src/output/pivot/look_xml.rs b/rust/pspp/src/output/pivot/look_xml.rs index adddcac90c..c935125e31 100644 --- a/rust/pspp/src/output/pivot/look_xml.rs +++ b/rust/pspp/src/output/pivot/look_xml.rs @@ -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 index a69f821532..0000000000 --- a/rust/pspp/src/output/pivot/test.rs +++ /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 . - -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) -> 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(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, - 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 index 0000000000..a69f821532 --- /dev/null +++ b/rust/pspp/src/output/pivot/tests.rs @@ -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 . + +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) -> 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(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, + 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/tlo.rs b/rust/pspp/src/output/pivot/tlo.rs index b51e551461..dfa292c911 100644 --- a/rust/pspp/src/output/pivot/tlo.rs +++ b/rust/pspp/src/output/pivot/tlo.rs @@ -560,7 +560,7 @@ impl Debug for U8String { } #[cfg(test)] -mod test { +mod tests { use crate::output::pivot::tlo::parse_tlo; #[test] diff --git a/rust/pspp/src/output/text.rs b/rust/pspp/src/output/text.rs index af00cfcc68..147f475d53 100644 --- a/rust/pspp/src/output/text.rs +++ b/rust/pspp/src/output/text.rs @@ -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; diff --git a/rust/pspp/src/output/text_line.rs b/rust/pspp/src/output/text_line.rs index 2911b63bc4..e4d7c5c370 100644 --- a/rust/pspp/src/output/text_line.rs +++ b/rust/pspp/src/output/text_line.rs @@ -330,7 +330,7 @@ pub fn clip_text<'a>( } #[cfg(test)] -mod test { +mod tests { use super::{Emphasis, TextLine}; use enum_iterator::all; diff --git a/rust/pspp/src/sys.rs b/rust/pspp/src/sys.rs index 4f59614100..1746781d69 100644 --- a/rust/pspp/src/sys.rs +++ b/rust/pspp/src/sys.rs @@ -42,7 +42,7 @@ use serde::Serializer; pub use write::{SystemFileVersion, WriteOptions, Writer}; #[cfg(test)] -mod test; +mod tests; fn serialize_endian(endian: &Endian, serializer: S) -> Result where diff --git a/rust/pspp/src/sys/sack.rs b/rust/pspp/src/sys/sack.rs index b23d0d83ea..348fda6bf5 100644 --- a/rust/pspp/src/sys/sack.rs +++ b/rust/pspp/src/sys/sack.rs @@ -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 index 9198a71e94..0000000000 --- a/rust/pspp/src/sys/test.rs +++ /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 . - -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::().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_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_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(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::>(), - ); - } - 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 index 0000000000..64750fed06 --- /dev/null +++ b/rust/pspp/src/sys/tests.rs @@ -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 . + +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::().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_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_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(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::>(), + ); + } + 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"); +}