persistant_core/
matrix.rs1use crate::{CapabilityId, Config, ParseError, Reason, Row};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6#[non_exhaustive]
7pub enum Lookup {
8 On,
10 Off(Reason),
12 NoRow,
14}
15
16#[derive(Debug, Clone, Default, PartialEq, Eq)]
18pub struct Matrix {
19 rows: Vec<Row>,
20}
21
22impl Matrix {
23 pub fn new() -> Self {
25 Matrix::default()
26 }
27
28 pub fn parse(text: &str) -> Result<Matrix, ParseError> {
34 crate::adapter::parse(text).map(|rows| Matrix { rows })
35 }
36
37 pub fn from_rows(rows: Vec<Row>) -> Matrix {
39 Matrix { rows }
40 }
41
42 pub fn merge(&mut self, other: Matrix) {
44 self.rows.extend(other.rows);
45 }
46
47 pub fn rows(&self) -> &[Row] {
49 &self.rows
50 }
51
52 pub fn to_tsv(&self) -> String {
57 crate::adapter::write(&self.rows)
58 }
59
60 pub fn lookup(
64 &self,
65 subject: &CapabilityId,
66 backend: &str,
67 version: &str,
68 actual: &Config,
69 ) -> Lookup {
70 let mut on = false;
71 let mut off = None;
72 for r in &self.rows {
73 if &r.subject == subject
74 && r.backend == backend
75 && r.version == version
76 && r.config.holds_in(actual)
77 {
78 if r.enabled {
79 on = true;
80 } else if off.is_none() {
81 off = r.reason;
82 }
83 }
84 }
85 match (on, off) {
86 (_, Some(reason)) => Lookup::Off(reason),
87 (true, None) => Lookup::On,
88 (false, None) => Lookup::NoRow,
89 }
90 }
91
92 pub fn enabled(
94 &self,
95 subject: &CapabilityId,
96 backend: &str,
97 version: &str,
98 actual: &Config,
99 ) -> bool {
100 self.lookup(subject, backend, version, actual) == Lookup::On
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107
108 const FILE: &str = "\
109# comment
110subject_kind\tsubject\tbackend\tversion\tconfig\tenabled\treason\tmetadata\tnote\tfrom
111construct\ttext_comparison\tmysql\t8.4.11\tcollation=utf8mb4_bin\tyes\t\ttest=a::b\t\tpostgres
112construct\ttext_comparison\tmysql\t8.4.11\tcollation=utf8mb4_0900_ai_ci\tno\twrong\ttest=a::b\tdiverges\tpostgres
113construct\tcast.to_uuid\tmysql\t8.4.11\t\tno\tmissing\ttest=c::d\t\tpostgres
114";
115
116 fn id(s: &str) -> CapabilityId {
117 CapabilityId::new(s).unwrap()
118 }
119
120 #[test]
121 fn reads_a_unisql_shaped_file_and_keeps_the_extra_column() {
122 let m = Matrix::parse(FILE).unwrap();
123 assert_eq!(m.rows().len(), 3);
124 assert_eq!(
125 m.rows()[0].extra.get("from").map(String::as_str),
126 Some("postgres")
127 );
128 let again = Matrix::parse(&m.to_tsv()).unwrap();
129 assert_eq!(again, m, "round trip");
130 }
131
132 #[test]
133 fn the_gate_is_on_only_for_a_matching_yes() {
134 let m = Matrix::parse(FILE).unwrap();
135 let bin = Config::parse("collation=utf8mb4_bin").unwrap();
136 let ci = Config::parse("collation=utf8mb4_0900_ai_ci").unwrap();
137 let t = id("text_comparison");
138 assert!(m.enabled(&t, "mysql", "8.4.11", &bin));
139 assert_eq!(
140 m.lookup(&t, "mysql", "8.4.11", &ci),
141 Lookup::Off(Reason::Wrong),
142 "known-bad row"
143 );
144 assert_eq!(
145 m.lookup(&t, "mysql", "9.0.0", &bin),
146 Lookup::NoRow,
147 "unknown version is off"
148 );
149 assert_eq!(
150 m.lookup(&t, "mysql", "8.4.11", &Config::new()),
151 Lookup::NoRow,
152 "config not readable is off"
153 );
154 assert_eq!(
155 m.lookup(&id("cast.to_uuid"), "mysql", "8.4.11", &bin),
156 Lookup::Off(Reason::Missing)
157 );
158 assert_eq!(
159 m.lookup(&id("never.heard_of_it"), "mysql", "8.4.11", &bin),
160 Lookup::NoRow
161 );
162 }
163
164 #[test]
165 fn off_wins_over_a_yes() {
166 let text = "subject_kind\tsubject\tbackend\tversion\tconfig\tenabled\treason\tmetadata\tnote\n\
167 capability\tk\tb\t1\t\tyes\t\t\t\n\
168 capability\tk\tb\t1\tx=1\tno\twrong\t\t\n";
169 let m = Matrix::parse(text).unwrap();
170 let k = id("k");
171 assert!(m.enabled(&k, "b", "1", &Config::new()));
172 assert!(!m.enabled(&k, "b", "1", &Config::new().with("x", "1")));
173 }
174
175 #[test]
176 fn malformed_files_are_errors_not_silence() {
177 assert_eq!(Matrix::parse(""), Err(ParseError::NoHeader));
178 assert!(matches!(
179 Matrix::parse("subject\tbackend\n"),
180 Err(ParseError::MissingColumn(_))
181 ));
182 let h =
183 "subject_kind\tsubject\tbackend\tversion\tconfig\tenabled\treason\tmetadata\tnote\n";
184 let bad_yes = format!("{h}c\tk\tb\t1\t\tmaybe\t\t\t\n");
185 assert!(matches!(
186 Matrix::parse(&bad_yes),
187 Err(ParseError::BadEnabled { line: 2, .. })
188 ));
189 let no_reason = format!("{h}c\tk\tb\t1\t\tno\t\t\t\n");
190 assert_eq!(
191 Matrix::parse(&no_reason),
192 Err(ParseError::ReasonMismatch { line: 2 })
193 );
194 let yes_reason = format!("{h}c\tk\tb\t1\t\tyes\twrong\t\t\n");
195 assert_eq!(
196 Matrix::parse(&yes_reason),
197 Err(ParseError::ReasonMismatch { line: 2 })
198 );
199 let short = format!("{h}c\tk\tb\n");
200 assert!(matches!(
201 Matrix::parse(&short),
202 Err(ParseError::FieldCount { line: 2, .. })
203 ));
204 }
205
206 #[test]
207 fn merge_concatenates() {
208 let mut a = Matrix::parse(FILE).unwrap();
209 a.merge(Matrix::parse(FILE).unwrap());
210 assert_eq!(a.rows().len(), 6);
211 }
212}