Skip to main content

persistant_core/
row.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2use crate::{CapabilityId, Config, ConfigError, IdError};
3use std::collections::BTreeMap;
4use std::fmt;
5
6/// Why a capability is off.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
8#[non_exhaustive]
9pub enum Reason {
10    /// The backend does not give it (or the test found nothing there).
11    Missing,
12    /// The backend gives something else: the result broke the contract.
13    Wrong,
14    /// No test ran for this `(backend, version, config)`.
15    Untested,
16}
17
18impl Reason {
19    /// The word used in the matrix file.
20    pub fn as_str(self) -> &'static str {
21        match self {
22            Reason::Missing => "missing",
23            Reason::Wrong => "wrong",
24            Reason::Untested => "untested",
25        }
26    }
27}
28
29impl fmt::Display for Reason {
30    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31        f.write_str(self.as_str())
32    }
33}
34
35/// Why matrix text could not be read.
36#[derive(Debug, Clone, PartialEq, Eq)]
37#[non_exhaustive]
38pub enum ParseError {
39    /// No header line.
40    NoHeader,
41    /// A required column is not in the header.
42    MissingColumn(&'static str),
43    /// A line has a different number of tab-separated fields than the header.
44    FieldCount {
45        line: usize,
46        want: usize,
47        got: usize,
48    },
49    /// `subject` is not a valid id.
50    BadId { line: usize, error: IdError },
51    /// `config` or `metadata` is not `k=v;k=v`.
52    BadConfig {
53        line: usize,
54        column: &'static str,
55        error: ConfigError,
56    },
57    /// `enabled` is not `yes` or `no`.
58    BadEnabled { line: usize, value: String },
59    /// `reason` is not `missing`, `wrong` or `untested`.
60    BadReason { line: usize, value: String },
61    /// `enabled=no` without a reason, or `enabled=yes` with one.
62    ReasonMismatch { line: usize },
63}
64
65impl fmt::Display for ParseError {
66    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
67        match self {
68            ParseError::NoHeader => write!(f, "matrix has no header line"),
69            ParseError::MissingColumn(c) => write!(f, "matrix header lacks column {c:?}"),
70            ParseError::FieldCount { line, want, got } => {
71                write!(f, "line {line}: {got} fields, header has {want}")
72            }
73            ParseError::BadId { line, error } => write!(f, "line {line}: subject: {error}"),
74            ParseError::BadConfig {
75                line,
76                column,
77                error,
78            } => write!(f, "line {line}: {column}: {error}"),
79            ParseError::BadEnabled { line, value } => {
80                write!(f, "line {line}: enabled is {value:?}, want yes or no")
81            }
82            ParseError::BadReason { line, value } => {
83                write!(f, "line {line}: reason {value:?} is unknown")
84            }
85            ParseError::ReasonMismatch { line } => {
86                write!(
87                    f,
88                    "line {line}: a no needs a reason and a yes must have none"
89                )
90            }
91        }
92    }
93}
94
95impl std::error::Error for ParseError {}
96
97/// The known columns, in the order they are written (`docs/DESIGN-KINDS.md` ยง2.4).
98pub(crate) const COLUMNS: [&str; 9] = [
99    "subject_kind",
100    "subject",
101    "backend",
102    "version",
103    "config",
104    "enabled",
105    "reason",
106    "metadata",
107    "note",
108];
109
110/// One recorded fact: a test ran, for this subject, on this backend, version and config.
111#[derive(Debug, Clone, PartialEq, Eq)]
112pub struct Row {
113    /// `capability`, `construct` or another word the writer chose; not interpreted here.
114    pub subject_kind: String,
115    /// The leaf id.
116    pub subject: CapabilityId,
117    /// The driver or dialect crate's backend name.
118    pub backend: String,
119    /// The backend (server) version string. No row for a version means off.
120    pub version: String,
121    /// Facts the gate can read at open, under which this row holds.
122    pub config: Config,
123    /// Boolean outcome.
124    pub enabled: bool,
125    /// Present exactly when `enabled` is false.
126    pub reason: Option<Reason>,
127    /// Environment, trial parameters, test id, tier, suite (`k=v;k=v`).
128    pub metadata: Config,
129    /// Free text; may carry `allowance=<name>` first.
130    pub note: String,
131    /// Columns this crate does not know (UniSQL writes `from`), kept so a rewrite loses nothing.
132    pub extra: BTreeMap<String, String>,
133}
134
135impl Row {
136    pub(crate) fn parse_fields(
137        header: &[&str],
138        fields: &[&str],
139        line: usize,
140    ) -> Result<Row, ParseError> {
141        let idx = |name: &'static str| -> Result<usize, ParseError> {
142            header
143                .iter()
144                .position(|h| *h == name)
145                .ok_or(ParseError::MissingColumn(name))
146        };
147        let get = |name: &'static str| -> Result<&str, ParseError> { Ok(fields[idx(name)?]) };
148        let cfg = |name: &'static str| -> Result<Config, ParseError> {
149            Config::parse(get(name)?).map_err(|error| ParseError::BadConfig {
150                line,
151                column: name,
152                error,
153            })
154        };
155        let enabled = match get("enabled")? {
156            "yes" => true,
157            "no" => false,
158            other => {
159                return Err(ParseError::BadEnabled {
160                    line,
161                    value: other.to_string(),
162                });
163            }
164        };
165        let reason = match get("reason")? {
166            "" => None,
167            "missing" => Some(Reason::Missing),
168            "wrong" => Some(Reason::Wrong),
169            "untested" => Some(Reason::Untested),
170            other => {
171                return Err(ParseError::BadReason {
172                    line,
173                    value: other.to_string(),
174                });
175            }
176        };
177        if enabled == reason.is_some() {
178            return Err(ParseError::ReasonMismatch { line });
179        }
180        let extra = header
181            .iter()
182            .zip(fields)
183            .filter(|(h, _)| !COLUMNS.contains(h))
184            .map(|(h, v)| (h.to_string(), v.to_string()))
185            .collect();
186        Ok(Row {
187            subject_kind: get("subject_kind")?.to_string(),
188            subject: CapabilityId::new(get("subject")?)
189                .map_err(|error| ParseError::BadId { line, error })?,
190            backend: get("backend")?.to_string(),
191            version: get("version")?.to_string(),
192            config: cfg("config")?,
193            enabled,
194            reason,
195            metadata: cfg("metadata")?,
196            note: get("note")?.to_string(),
197            extra,
198        })
199    }
200
201    pub(crate) fn field(&self, column: &str) -> String {
202        match column {
203            "subject_kind" => self.subject_kind.clone(),
204            "subject" => self.subject.to_string(),
205            "backend" => self.backend.clone(),
206            "version" => self.version.clone(),
207            "config" => self.config.to_string(),
208            "enabled" => if self.enabled { "yes" } else { "no" }.to_string(),
209            "reason" => self
210                .reason
211                .map(|r| r.as_str().to_string())
212                .unwrap_or_default(),
213            "metadata" => self.metadata.to_string(),
214            "note" => self.note.clone(),
215            other => self.extra.get(other).cloned().unwrap_or_default(),
216        }
217    }
218}