LCOV - code coverage report
Current view: top level - pageserver/src/config - ignored_fields.rs (source / functions) Coverage Total Hit
Test: 1e20c4f2b28aa592527961bb32170ebbd2c9172f.info Lines: 99.2 % 118 117
Test Date: 2025-07-16 12:29:03 Functions: 100.0 % 11 11

            Line data    Source code
       1              : //! Check for fields in the on-disk config file that were ignored when
       2              : //! deserializing [`pageserver_api::config::ConfigToml`].
       3              : //!
       4              : //! This could have been part of the [`pageserver_api::config`] module,
       5              : //! but the way we identify unused fields in this module
       6              : //! is specific to the format (TOML) and the implementation of the
       7              : //! deserialization for that format ([`toml_edit`]).
       8              : 
       9              : use std::collections::HashSet;
      10              : 
      11              : use itertools::Itertools;
      12              : 
      13              : /// Pass in the user-specified config and the re-serialized [`pageserver_api::config::ConfigToml`].
      14              : /// The returned [`Paths`] contains the paths to the fields that were ignored by deserialization
      15              : /// of the [`pageserver_api::config::ConfigToml`].
      16            4 : pub fn find(user_specified: toml_edit::DocumentMut, reserialized: toml_edit::DocumentMut) -> Paths {
      17            4 :     let user_specified = paths(user_specified);
      18            4 :     let reserialized = paths(reserialized);
      19            8 :     fn paths(doc: toml_edit::DocumentMut) -> HashSet<String> {
      20            8 :         let mut out = Vec::new();
      21            8 :         let mut visitor = PathsVisitor::new(&mut out);
      22            8 :         visitor.visit_table_like(doc.as_table());
      23            8 :         HashSet::from_iter(out)
      24            8 :     }
      25              : 
      26            4 :     let mut ignored = HashSet::new();
      27              : 
      28              :     // O(n) because of HashSet
      29           12 :     for path in user_specified {
      30            8 :         if !reserialized.contains(&path) {
      31            4 :             ignored.insert(path);
      32            4 :         }
      33              :     }
      34              : 
      35            4 :     Paths {
      36            4 :         paths: ignored
      37            4 :             .into_iter()
      38            4 :             // sort lexicographically for deterministic output
      39            4 :             .sorted()
      40            4 :             .collect(),
      41            4 :     }
      42            4 : }
      43              : 
      44              : pub struct Paths {
      45              :     pub paths: Vec<String>,
      46              : }
      47              : 
      48              : struct PathsVisitor<'a> {
      49              :     stack: Vec<String>,
      50              :     out: &'a mut Vec<String>,
      51              : }
      52              : 
      53              : impl<'a> PathsVisitor<'a> {
      54            8 :     fn new(out: &'a mut Vec<String>) -> Self {
      55            8 :         Self {
      56            8 :             stack: Vec::new(),
      57            8 :             out,
      58            8 :         }
      59            8 :     }
      60              : 
      61           18 :     fn visit_table_like(&mut self, table_like: &dyn toml_edit::TableLike) {
      62           24 :         for (entry, item) in table_like.iter() {
      63           24 :             self.stack.push(entry.to_string());
      64           24 :             self.visit_item(item);
      65           24 :             self.stack.pop();
      66           24 :         }
      67           18 :     }
      68              : 
      69           24 :     fn visit_item(&mut self, item: &toml_edit::Item) {
      70           24 :         match item {
      71            0 :             toml_edit::Item::None => (),
      72           16 :             toml_edit::Item::Value(value) => self.visit_value(value),
      73            6 :             toml_edit::Item::Table(table) => {
      74            6 :                 self.visit_table_like(table);
      75            6 :             }
      76            2 :             toml_edit::Item::ArrayOfTables(array_of_tables) => {
      77            2 :                 for (i, table) in array_of_tables.iter().enumerate() {
      78            2 :                     self.stack.push(format!("[{i}]"));
      79            2 :                     self.visit_table_like(table);
      80            2 :                     self.stack.pop();
      81            2 :                 }
      82              :             }
      83              :         }
      84           24 :     }
      85              : 
      86           18 :     fn visit_value(&mut self, value: &toml_edit::Value) {
      87           18 :         match value {
      88              :             toml_edit::Value::String(_)
      89              :             | toml_edit::Value::Integer(_)
      90              :             | toml_edit::Value::Float(_)
      91              :             | toml_edit::Value::Boolean(_)
      92           14 :             | toml_edit::Value::Datetime(_) => self.out.push(self.stack.join(".")),
      93            2 :             toml_edit::Value::Array(array) => {
      94            2 :                 for (i, value) in array.iter().enumerate() {
      95            2 :                     self.stack.push(format!("[{i}]"));
      96            2 :                     self.visit_value(value);
      97            2 :                     self.stack.pop();
      98            2 :                 }
      99              :             }
     100            2 :             toml_edit::Value::InlineTable(inline_table) => self.visit_table_like(inline_table),
     101              :         }
     102           18 :     }
     103              : }
     104              : 
     105              : #[cfg(test)]
     106              : pub(crate) mod tests {
     107              : 
     108            4 :     fn test_impl(original: &str, parsed: &str, expect: [&str; 1]) {
     109            4 :         let original: toml_edit::DocumentMut = original.parse().expect("parse original config");
     110            4 :         let parsed: toml_edit::DocumentMut = parsed.parse().expect("parse re-serialized config");
     111              : 
     112            4 :         let super::Paths { paths: actual } = super::find(original, parsed);
     113            4 :         assert_eq!(actual, &expect);
     114            4 :     }
     115              : 
     116              :     #[test]
     117            1 :     fn top_level() {
     118            1 :         test_impl(
     119            1 :             r#"
     120            1 :                 [a]
     121            1 :                 b = 1
     122            1 :                 c = 2
     123            1 :                 d = 3
     124            1 :             "#,
     125            1 :             r#"
     126            1 :                 [a]
     127            1 :                 b = 1
     128            1 :                 c = 2
     129            1 :             "#,
     130            1 :             ["a.d"],
     131              :         );
     132            1 :     }
     133              : 
     134              :     #[test]
     135            1 :     fn nested() {
     136            1 :         test_impl(
     137            1 :             r#"
     138            1 :                 [a.b.c]
     139            1 :                 d = 23
     140            1 :             "#,
     141            1 :             r#"
     142            1 :                 [a]
     143            1 :                 e = 42
     144            1 :             "#,
     145            1 :             ["a.b.c.d"],
     146              :         );
     147            1 :     }
     148              : 
     149              :     #[test]
     150            1 :     fn array_of_tables() {
     151            1 :         test_impl(
     152            1 :             r#"
     153            1 :                 [[a]]
     154            1 :                 b = 1
     155            1 :                 c = 2
     156            1 :                 d = 3
     157            1 :             "#,
     158            1 :             r#"
     159            1 :                 [[a]]
     160            1 :                 b = 1
     161            1 :                 c = 2
     162            1 :             "#,
     163            1 :             ["a.[0].d"],
     164              :         );
     165            1 :     }
     166              : 
     167              :     #[test]
     168            1 :     fn array() {
     169            1 :         test_impl(
     170            1 :             r#"
     171            1 :             foo = [ {bar = 23} ]
     172            1 :             "#,
     173            1 :             r#"
     174            1 :             foo = [ { blup = 42 }]
     175            1 :             "#,
     176            1 :             ["foo.[0].bar"],
     177              :         );
     178            1 :     }
     179              : }
        

Generated by: LCOV version 2.1-beta