1use 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
16pub fn capant_provider(
20 id: &str,
21 backend: &str,
22 version: &str,
23 actual: &Config,
24) -> capant::Provider {
25 let config = actual
26 .iter()
27 .fold(capant::Config::new(), |acc, (k, v)| acc.with(k, v));
28 capant::Provider::new(id, vec![capant::Surface::new(backend, version, config)])
29 .expect("a provider with one surface has a surface")
30}
31
32#[derive(Debug, Clone, Default, PartialEq, Eq)]
34pub struct Matrix {
35 rows: Vec<Row>,
36}
37
38impl Matrix {
39 pub fn new() -> Self {
41 Matrix::default()
42 }
43
44 pub fn parse(text: &str) -> Result<Matrix, ParseError> {
50 crate::adapter::parse(text).map(|rows| Matrix { rows })
51 }
52
53 pub fn from_rows(rows: Vec<Row>) -> Matrix {
55 Matrix { rows }
56 }
57
58 pub fn merge(&mut self, other: Matrix) {
60 self.rows.extend(other.rows);
61 }
62
63 pub fn rows(&self) -> &[Row] {
65 &self.rows
66 }
67
68 pub fn to_tsv(&self) -> String {
73 crate::adapter::write(&self.rows)
74 }
75
76 pub fn to_capant(&self) -> capant::Matrix {
81 crate::adapter::view(&self.rows)
82 }
83
84 pub fn lookup(
88 &self,
89 subject: &CapabilityId,
90 backend: &str,
91 version: &str,
92 actual: &Config,
93 ) -> Lookup {
94 let mut on = false;
95 let mut off = None;
96 for r in &self.rows {
97 if &r.subject == subject
98 && r.backend == backend
99 && r.version == version
100 && r.config.holds_in(actual)
101 {
102 if r.enabled {
103 on = true;
104 } else if off.is_none() {
105 off = r.reason;
106 }
107 }
108 }
109 match (on, off) {
110 (_, Some(reason)) => Lookup::Off(reason),
111 (true, None) => Lookup::On,
112 (false, None) => Lookup::NoRow,
113 }
114 }
115
116 pub fn enabled(
118 &self,
119 subject: &CapabilityId,
120 backend: &str,
121 version: &str,
122 actual: &Config,
123 ) -> bool {
124 self.lookup(subject, backend, version, actual) == Lookup::On
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 const FILE: &str = "\
133# comment
134subject_kind\tsubject\tbackend\tversion\tconfig\tenabled\treason\tmetadata\tnote\tfrom
135construct\ttext_comparison\tmysql\t8.4.11\tcollation=utf8mb4_bin\tyes\t\ttest=a::b\t\tpostgres
136construct\ttext_comparison\tmysql\t8.4.11\tcollation=utf8mb4_0900_ai_ci\tno\twrong\ttest=a::b\tdiverges\tpostgres
137construct\tcast.to_uuid\tmysql\t8.4.11\t\tno\tmissing\ttest=c::d\t\tpostgres
138";
139
140 fn id(s: &str) -> CapabilityId {
141 CapabilityId::new(s).unwrap()
142 }
143
144 #[test]
145 fn reads_a_unisql_shaped_file_and_keeps_the_extra_column() {
146 let m = Matrix::parse(FILE).unwrap();
147 assert_eq!(m.rows().len(), 3);
148 assert_eq!(
149 m.rows()[0].extra.get("from").map(String::as_str),
150 Some("postgres")
151 );
152 let again = Matrix::parse(&m.to_tsv()).unwrap();
153 assert_eq!(again, m, "round trip");
154 }
155
156 #[test]
157 fn the_gate_is_on_only_for_a_matching_yes() {
158 let m = Matrix::parse(FILE).unwrap();
159 let bin = Config::parse("collation=utf8mb4_bin").unwrap();
160 let ci = Config::parse("collation=utf8mb4_0900_ai_ci").unwrap();
161 let t = id("text_comparison");
162 assert!(m.enabled(&t, "mysql", "8.4.11", &bin));
163 assert_eq!(
164 m.lookup(&t, "mysql", "8.4.11", &ci),
165 Lookup::Off(Reason::Wrong),
166 "known-bad row"
167 );
168 assert_eq!(
169 m.lookup(&t, "mysql", "9.0.0", &bin),
170 Lookup::NoRow,
171 "unknown version is off"
172 );
173 assert_eq!(
174 m.lookup(&t, "mysql", "8.4.11", &Config::new()),
175 Lookup::NoRow,
176 "config not readable is off"
177 );
178 assert_eq!(
179 m.lookup(&id("cast.to_uuid"), "mysql", "8.4.11", &bin),
180 Lookup::Off(Reason::Missing)
181 );
182 assert_eq!(
183 m.lookup(&id("never.heard_of_it"), "mysql", "8.4.11", &bin),
184 Lookup::NoRow
185 );
186 }
187
188 #[test]
189 fn off_wins_over_a_yes() {
190 let text = "subject_kind\tsubject\tbackend\tversion\tconfig\tenabled\treason\tmetadata\tnote\n\
191 capability\tk\tb\t1\t\tyes\t\t\t\n\
192 capability\tk\tb\t1\tx=1\tno\twrong\t\t\n";
193 let m = Matrix::parse(text).unwrap();
194 let k = id("k");
195 assert!(m.enabled(&k, "b", "1", &Config::new()));
196 assert!(!m.enabled(&k, "b", "1", &Config::new().with("x", "1")));
197 }
198
199 #[test]
200 fn malformed_files_are_errors_not_silence() {
201 assert_eq!(Matrix::parse(""), Err(ParseError::NoHeader));
202 assert!(matches!(
203 Matrix::parse("subject\tbackend\n"),
204 Err(ParseError::MissingColumn(_))
205 ));
206 let h =
207 "subject_kind\tsubject\tbackend\tversion\tconfig\tenabled\treason\tmetadata\tnote\n";
208 let bad_yes = format!("{h}c\tk\tb\t1\t\tmaybe\t\t\t\n");
209 assert!(matches!(
210 Matrix::parse(&bad_yes),
211 Err(ParseError::BadEnabled { line: 2, .. })
212 ));
213 let no_reason = format!("{h}c\tk\tb\t1\t\tno\t\t\t\n");
214 assert_eq!(
215 Matrix::parse(&no_reason),
216 Err(ParseError::ReasonMismatch { line: 2 })
217 );
218 let yes_reason = format!("{h}c\tk\tb\t1\t\tyes\twrong\t\t\n");
219 assert_eq!(
220 Matrix::parse(&yes_reason),
221 Err(ParseError::ReasonMismatch { line: 2 })
222 );
223 let short = format!("{h}c\tk\tb\n");
224 assert!(matches!(
225 Matrix::parse(&short),
226 Err(ParseError::FieldCount { line: 2, .. })
227 ));
228 }
229
230 #[test]
231 fn merge_concatenates() {
232 let mut a = Matrix::parse(FILE).unwrap();
233 a.merge(Matrix::parse(FILE).unwrap());
234 assert_eq!(a.rows().len(), 6);
235 }
236}