tests
authorBen Pfaff <blp@cs.stanford.edu>
Wed, 24 Dec 2025 21:51:52 +0000 (13:51 -0800)
committerBen Pfaff <blp@cs.stanford.edu>
Wed, 24 Dec 2025 21:55:50 +0000 (13:55 -0800)
rust/pspp/src/output/drivers/text.rs
rust/pspp/src/spv/read/html.rs
rust/pspp/src/spv/read/legacy_xml.rs
rust/pspp/src/spv/read/tests.rs
rust/pspp/src/spv/testdata/legacy7.expected [new file with mode: 0644]
rust/pspp/src/spv/testdata/legacy7.spv [new file with mode: 0644]

index 5827fef5f842570d8953cf74cebe9c45b97b9797..5a11d6190c64dbb2a9e7f9fda8dc1188fd281007 100644 (file)
@@ -385,20 +385,32 @@ impl TextDriver {
 }
 
 impl TextRenderer {
+    fn start_object<W>(&mut self, writer: &mut W) -> FmtResult
+    where
+        W: FmtWrite,
+    {
+        if self.n_objects > 0 {
+            writeln!(writer)?;
+        }
+        self.n_objects += 1;
+        Ok(())
+    }
+
     fn render<W>(&mut self, item: &Item, writer: &mut W) -> FmtResult
     where
         W: FmtWrite,
     {
-        for (index, item) in ItemRefIterator::without_hidden(item)
-            .filter(|item| !item.details.is_heading())
-            .enumerate()
+        for item in ItemRefIterator::without_hidden(item).filter(|item| !item.details.is_heading())
         {
-            if index > 0 {
-                writeln!(writer)?;
-            }
             match &item.details {
-                Details::Chart => writeln!(writer, "Omitting chart from text output")?,
-                Details::Image(_) => writeln!(writer, "Omitting image from text output")?,
+                Details::Chart => {
+                    self.start_object(writer)?;
+                    writeln!(writer, "Omitting chart from text output")?
+                }
+                Details::Image(_) => {
+                    self.start_object(writer)?;
+                    writeln!(writer, "Omitting image from text output")?
+                }
                 Details::Heading(_) => unreachable!(),
                 Details::Message(_diagnostic) => todo!(),
                 Details::PageBreak => (),
@@ -415,10 +427,8 @@ impl TextRenderer {
     where
         W: FmtWrite,
     {
-        for (index, layer_indexes) in table.layers(true).enumerate() {
-            if index > 0 {
-                writeln!(writer)?;
-            }
+        for layer_indexes in table.layers(true) {
+            self.start_object(writer)?;
 
             let mut pager = Pager::new(self, table, Some(layer_indexes.as_slice()));
             while pager.has_next(self).is_some() {
index 19dc7097669358e8b62c184232e56114ab92ef69..4cc4b12caf29cdf2ebf44a60ec46143cbadd5a56 100644 (file)
@@ -778,7 +778,9 @@ fn parse_nodes(nodes: &[Node]) -> Markup {
             }
             // SPSS often starts paragraphs with an initial `<BR>` that it
             // ignores, but it does honor `<br>`.  So weird.
-            Node::Element(br) if br.name == "br" => {
+            Node::Element(br)
+                if br.name.eq_ignore_ascii_case("br") && (br.name == "br" || i != 0) =>
+            {
                 add_markup(&mut retval, Markup::Text('\n'.into()));
             }
             Node::Element(element) => {
@@ -970,26 +972,6 @@ mod tests {
         );
     }
 
-    /*
-    #[test]
-    fn value() {
-        let value = parse_value(
-            r#"<b>bold</b><br><i>italic</i><BR><b><i>bold italic</i></b><br><font color="red" face="Serif">red serif</font><br><font size="7">big</font><br>"#,
-        );
-        assert_eq!(
-            value,
-            Value::new_markup(
-                r##"<b>bold</b>
-<i>italic</i>
-<b><i>bold italic</i></b>
-<span face="Serif" color="#ff0000">red serif</span>
-<span size="20480">big</span>
-"##
-            )
-            .with_font_style(FontStyle::default().with_size(10))
-        );
-    }*/
-
     /// From the corpus (also included in the documentation).
     #[test]
     fn header1() {
@@ -1090,6 +1072,32 @@ mod tests {
         );
     }
 
+    /// From the corpus, anonymized.
+    ///
+    /// This tests the unusual treatment of `<BR>` at the start of text (`<BR>`
+    /// is ignored at the start, but `<br>` is not).
+    #[test]
+    fn breaks() {
+        let text = r##"<xml>&lt;head>&lt;style type="text/css">p{color:0;font-family:Monospaced;font-size:13pt;font-style:normal;font-weight:normal;text-decoration:none}&lt;/style>&lt;/head>&lt;BR>USE ALL.&lt;BR>COMPUTE filter_$=(group = 1).&lt;BR>VARIABLE LABEL filter_$ 'group = 1 (FILTER)'.&lt;BR>VALUE LABELS filter_$ 0 'Not Selected' 1 'Selected'.&lt;BR>FORMAT filter_$ (f1.0).&lt;BR>FILTER BY filter_$.&lt;BR>EXECUTE.&lt;BR>NPAR TEST&lt;BR>  /WILCOXON=x WITH y&lt;BR>   z w (PAIRED)&lt;BR>  /MISSING ANALYSIS.</xml>"##;
+        let content = quick_xml::de::from_str::<String>(text).unwrap();
+        let html = Document::from_html(&content);
+        let s = html.into_value().display(()).to_string();
+        assert_eq!(
+            s,
+            r##"USE ALL.
+COMPUTE filter_$=(group = 1).
+VARIABLE LABEL filter_$ 'group = 1 (FILTER)'.
+VALUE LABELS filter_$ 0 'Not Selected' 1 'Selected'.
+FORMAT filter_$ (f1.0).
+FILTER BY filter_$.
+EXECUTE.
+NPAR TEST
+  /WILCOXON=x WITH y
+   z w (PAIRED)
+  /MISSING ANALYSIS."##
+        );
+    }
+
     /// Checks that the `escape-html` feature is enabled in [quick_xml], since
     /// we need that to resolve `&nbsp;` and other HTML entities.
     #[test]
index 5998f8bb3f3dcf96bca24b1d1a42bfe3033bcc08..5b4a5473f74aab812e52dc2190dd75a6c2d7309b 100644 (file)
@@ -17,6 +17,7 @@
 use std::{
     cell::{Cell, RefCell},
     collections::{BTreeMap, HashMap},
+    fmt::Debug,
     marker::PhantomData,
     mem::take,
     num::NonZeroUsize,
@@ -670,16 +671,10 @@ impl Visualization {
         let cell = series.get("cell").unwrap()/*XXX*/;
         let mut coords = Vec::with_capacity(dims.len());
         let (cell_formats, format_map) = graph.interval.labeling.decode_format_map(&series);
-        let cell_footnotes =
-            graph
-                .interval
-                .labeling
-                .children
-                .iter()
-                .find_map(|child| match child {
-                    LabelingChild::Footnotes(footnotes) => series.get(footnotes.variable.as_str()),
-                    _ => None,
-                });
+        let cell_footnotes = graph
+            .interval
+            .footnotes()
+            .and_then(|footnotes| series.get(footnotes.variable.as_str()));
         let mut data = HashMap::new();
         for (i, cell) in cell.values.iter().enumerate() {
             coords.clear();
@@ -718,9 +713,9 @@ impl Visualization {
                     for part in s.split(',') {
                         if let Ok(index) = part.parse::<usize>()
                             && let Some(index) = index.checked_sub(1)
-                            && let Some(footnote) = footnotes.get(index)
+                            && let Some(footnote) = dbg!(footnotes.get(index))
                         {
-                            value = value.with_footnote(footnote);
+                            value.add_footnote(footnote);
                         }
                     }
                 }
@@ -1021,7 +1016,7 @@ impl Visualization {
             .collect::<Vec<_>>();
         let mut pivot_table = PivotTable::new(dimensions)
             .with_look(Arc::new(look))
-            .with_footnotes(footnotes)
+            .with_footnotes(dbg!(footnotes))
             .with_data(data)
             .with_layer(&current_layer);
         let decimal = Decimal::for_lang(&self.lang);
@@ -1050,6 +1045,14 @@ struct Series {
     dimension_index: Cell<Option<usize>>,
 }
 
+impl Debug for Series {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("Series")
+            .field("name", &self.name)
+            .finish_non_exhaustive()
+    }
+}
+
 impl Series {
     fn new(name: String, values: Vec<DataValue>, map: Map) -> Self {
         Self {
@@ -1888,10 +1891,6 @@ impl Style {
         base_style: &AreaStyle,
     ) {
         if let Some(sf) = sf {
-            if sf.reset == Some(true) {
-                value.styling_mut().footnotes.clear();
-            }
-
             let format = match &sf.child {
                 Some(SetFormatChild::Format(format)) => Some(format.decode()),
                 Some(SetFormatChild::NumberFormat(format)) => {
@@ -2414,6 +2413,19 @@ struct Interval {
     footnotes: Option<Footnotes>,
 }
 
+impl Interval {
+    fn footnotes(&self) -> Option<&Footnotes> {
+        if let Some(footnotes) = &self.footnotes {
+            Some(footnotes)
+        } else {
+            self.labeling
+                .children
+                .iter()
+                .find_map(|child| child.as_footnotes())
+        }
+    }
+}
+
 #[derive(Deserialize, Debug)]
 #[serde(rename_all = "camelCase")]
 struct Labeling {
@@ -2459,6 +2471,15 @@ enum LabelingChild {
     Footnotes(Footnotes),
 }
 
+impl LabelingChild {
+    fn as_footnotes(&self) -> Option<&Footnotes> {
+        match self {
+            Self::Footnotes(footnotes) => Some(footnotes),
+            _ => None,
+        }
+    }
+}
+
 #[derive(Deserialize, Debug)]
 #[serde(rename_all = "camelCase")]
 struct Formatting {
index ee61dbc7829e0e93a30656a24a62de181b556b3b..bf122980109d8ce1340bcc868720d0cd0bdb77c9 100644 (file)
@@ -43,6 +43,12 @@ fn legacy6() {
     test_raw_spvfile("legacy6");
 }
 
+/// Regression test for `<setFormat reset="true">`.
+#[test]
+fn legacy7() {
+    test_raw_spvfile("legacy7");
+}
+
 fn test_raw_spvfile(name: &str) {
     let input_filename = Path::new("src/spv/testdata")
         .join(name)
diff --git a/rust/pspp/src/spv/testdata/legacy7.expected b/rust/pspp/src/spv/testdata/legacy7.expected
new file mode 100644 (file)
index 0000000..18283a8
--- /dev/null
@@ -0,0 +1,44 @@
+                                         Ranks
+╭───────────────────────────────────────────────────────┬─────┬─────────┬────────────╮
+│                                                       │  N  │Mean Rank│Sum of Ranks│
+├───────────────────────────────────────────────────────┼─────┼─────────┼────────────┤
+│xxxxxxxxxx - yyyyyyyyyyyyy               Negative Ranks│25[a]│    13,00│      325,00│
+│                                         Positive Ranks│ 0[b]│      ,00│         ,00│
+│                                         Ties          │ 0[c]│         │            │
+│                                         Total         │   25│         │            │
+├───────────────────────────────────────────────────────┼─────┼─────────┼────────────┤
+│xxxxxxxxxxxxx - yyyyyyyyyyyyyy           Negative Ranks│25[d]│    13,00│      325,00│
+│                                         Positive Ranks│ 0[e]│      ,00│         ,00│
+│                                         Ties          │ 0[f]│         │            │
+│                                         Total         │   25│         │            │
+├───────────────────────────────────────────────────────┼─────┼─────────┼────────────┤
+│xxxxxxxxxxxxxx - yyyyyyyyyyyyyyy         Negative Ranks│25[g]│    13,00│      325,00│
+│                                         Positive Ranks│ 0[h]│      ,00│         ,00│
+│                                         Ties          │ 0[i]│         │            │
+│                                         Total         │   25│         │            │
+├───────────────────────────────────────────────────────┼─────┼─────────┼────────────┤
+│xxxxxxxxxxxxxxxx - yyyyyyyyyyyyyyyyy     Negative Ranks│ 5[j]│     3,00│       15,00│
+│                                         Positive Ranks│ 0[k]│      ,00│         ,00│
+│                                         Ties          │20[l]│         │            │
+│                                         Total         │   25│         │            │
+├───────────────────────────────────────────────────────┼─────┼─────────┼────────────┤
+│xxxxxxxxxxxxxxxxxxx - yyyyyyyyyyyyyyyyyy Negative Ranks│ 0[m]│      ,00│         ,00│
+│                                         Positive Ranks│ 5[n]│     3,00│       15,00│
+│                                         Ties          │20[o]│         │            │
+│                                         Total         │   25│         │            │
+╰───────────────────────────────────────────────────────┴─────┴─────────┴────────────╯
+a. Footnote A
+b. Footnote B
+c. Footnote C
+d. Footnote D
+e. Footnote E
+f. Footnote F
+g. Footnote G
+h. Footnote H
+i. Footnote I
+j. Footnote J
+k. Footnote K
+l. Footnote L
+m. Footnote M
+n. Footnote N
+o. Footnote O
diff --git a/rust/pspp/src/spv/testdata/legacy7.spv b/rust/pspp/src/spv/testdata/legacy7.spv
new file mode 100644 (file)
index 0000000..77caf75
Binary files /dev/null and b/rust/pspp/src/spv/testdata/legacy7.spv differ