Coverage Report

Created: 2025-07-30 16:11

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/build/source/nativelink-config/src/backcompat.rs
Line
Count
Source
1
use std::collections::HashMap;
2
3
use serde::{Deserialize, Deserializer, Serialize};
4
use tracing::warn;
5
6
use crate::cas_server::WithInstanceName;
7
8
#[derive(Debug, Deserialize)]
9
#[serde(untagged)]
10
enum WithInstanceNameBackCompat<T> {
11
    Map(HashMap<String, T>),
12
    Vec(Vec<WithInstanceName<T>>),
13
}
14
15
/// Use `#[serde(default, deserialize_with = "backcompat::opt_vec_named_config")]` for backwards
16
/// compatibility with map-based access. A deprecation warning will be written to stderr if the
17
/// old format is used.
18
25
pub(crate) fn opt_vec_with_instance_name<'de, D, T>(
19
25
    deserializer: D,
20
25
) -> Result<Option<Vec<WithInstanceName<T>>>, D::Error>
21
25
where
22
25
    D: Deserializer<'de>,
23
25
    T: Deserialize<'de> + Serialize,
24
{
25
25
    let Some(back_compat) = Option::deserialize(deserializer)
?0
else {
  Branch (25:9): [True: 0, False: 0]
  Branch (25:9): [True: 0, False: 0]
  Branch (25:9): [True: 6, False: 0]
  Branch (25:9): [True: 6, False: 0]
  Branch (25:9): [True: 5, False: 0]
  Branch (25:9): [True: 6, False: 0]
  Branch (25:9): [True: 2, False: 0]
26
0
        return Ok(None);
27
    };
28
29
25
    match back_compat {
30
1
        WithInstanceNameBackCompat::Map(map) => {
31
            // TODO(palfrey): ideally this would be serde_json5::to_string_pretty but that doesn't exist
32
            // JSON is close enough to be workable for now
33
1
            let serde_map = serde_json::to_string_pretty(&map).expect("valid map");
34
1
            let vec: Vec<WithInstanceName<T>> = map
35
1
                .into_iter()
36
1
                .map(|(instance_name, config)| WithInstanceName {
37
2
                    instance_name,
38
2
                    config,
39
2
                })
40
1
                .collect();
41
1
            warn!(
42
1
                r"WARNING: Using deprecated map format for services. Please migrate to the new array format:
43
1
// Old:
44
1
{}
45
1
// New:
46
1
{}
47
1
",
48
                serde_map,
49
                // TODO(palfrey): ideally this would be serde_json5::to_string_pretty but that doesn't exist
50
                // JSON is close enough to be workable for now
51
1
                serde_json::to_string_pretty(&vec).expect("valid new map")
52
            );
53
1
            Ok(Some(vec))
54
        }
55
24
        WithInstanceNameBackCompat::Vec(vec) => Ok(Some(vec)),
56
    }
57
25
}
58
59
#[cfg(test)]
60
mod tests {
61
    use serde_json::json;
62
    use tracing_test::traced_test;
63
64
    use super::*;
65
66
    #[derive(Debug, Deserialize, Serialize, PartialEq)]
67
    struct PartialConfig {
68
        store: String,
69
    }
70
71
    #[derive(Debug, Deserialize, Serialize, PartialEq)]
72
    struct FullConfig {
73
        #[serde(default, deserialize_with = "opt_vec_with_instance_name")]
74
        cas: Option<Vec<WithInstanceName<PartialConfig>>>,
75
    }
76
77
    #[test]
78
    #[traced_test]
79
1
    fn test_configs_deserialization() {
80
1
        let old_format = json!({
81
1
            "cas": {
82
1
                "foo": { "store": "foo_store" },
83
1
                "bar": { "store": "bar_store" }
84
            }
85
        });
86
87
1
        let new_format = json!({
88
1
            "cas": [
89
                {
90
1
                    "instance_name": "foo",
91
1
                    "store": "foo_store"
92
                },
93
                {
94
1
                    "instance_name": "bar",
95
1
                    "store": "bar_store"
96
                }
97
            ]
98
        });
99
100
1
        let mut old_format: FullConfig = serde_json::from_value(old_format).unwrap();
101
1
        let mut new_format: FullConfig = serde_json::from_value(new_format).unwrap();
102
103
        // Ensure deterministic ordering.
104
1
        if let Some(vec) = old_format.cas.as_mut() {
  Branch (104:16): [True: 1, False: 0]
105
1
            vec.sort_by(|a, b| a.instance_name.cmp(&b.instance_name));
106
0
        }
107
1
        if let Some(vec) = new_format.cas.as_mut() {
  Branch (107:16): [True: 1, False: 0]
108
1
            vec.sort_by(|a, b| a.instance_name.cmp(&b.instance_name));
109
0
        }
110
111
1
        assert_eq!(old_format, new_format);
112
113
1
        logs_assert(|lines: &[&str]| {
114
1
            if lines.len() != 1 {
  Branch (114:16): [True: 0, False: 1]
115
0
                return Err(format!("Expected 1 log line, got: {lines:?}"));
116
1
            }
117
1
            let line = lines[0];
118
            // TODO(palfrey): we should be checking the whole thing, but tracing-test is broken with multi-line items
119
            // See https://github.com/dbrgn/tracing-test/issues/48
120
1
            assert!(line.ends_with("WARNING: Using deprecated map format for services. Please migrate to the new array format:"));
121
1
            Ok(())
122
1
        });
123
1
    }
124
125
    #[test]
126
1
    fn test_deserialize_none() {
127
1
        let json = json!({});
128
129
1
        let value: FullConfig = serde_json::from_value(json).unwrap();
130
1
        assert_eq!(value.cas, None);
131
1
    }
132
}