use std::path::Path;
use serde::Deserialize;
use crate::FerroError;
pub const CASES_PATH: &str = "tests/fixtures/case-harvest/cases.json";
pub const WINDOWS_PATH: &str = "tests/fixtures/case-harvest/reference-windows.json";
pub const SPEC_DIR: &str = "assets/hgvs-nomenclature";
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Fixture {
pub description: String,
pub spec_commit: String,
pub cases: Vec<Case>,
}
impl Fixture {
pub fn from_json_path(path: &Path) -> Result<Self, FerroError> {
let content = std::fs::read_to_string(path)?;
Ok(serde_json::from_str(&content)?)
}
pub fn confluence_classes(&self) -> Vec<&str> {
let mut seen: Vec<&str> = Vec::new();
for case in &self.cases {
if let Some(class) = case.confluence_class.as_deref() {
if !seen.contains(&class) {
seen.push(class);
}
}
}
seen
}
pub fn members_of(&self, class: &str) -> Vec<&Case> {
self.cases
.iter()
.filter(|case| case.confluence_class.as_deref() == Some(class))
.collect()
}
}
#[derive(Debug, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Case {
pub input: String,
pub expected: Option<String>,
#[serde(default)]
pub confluence_class: Option<String>,
pub citation: Citation,
pub note: String,
#[serde(default)]
pub defect: Option<Defect>,
}
impl Case {
pub fn is_red(&self) -> bool {
self.defect.is_some()
}
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Defect {
pub issue: String,
pub kind: DefectKind,
pub summary: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum DefectKind {
Confluence,
AdjacentMembers,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Citation {
pub clause: String,
pub quote: String,
}
impl Citation {
pub fn verify(&self, spec_dir: &Path) -> Result<(), String> {
let (path, first, last) = self.parse_clause()?;
let full = spec_dir.join(path);
let text = std::fs::read_to_string(&full)
.map_err(|e| format!("citation {:?}: read {}: {e}", self.clause, full.display()))?;
let lines: Vec<&str> = text.lines().collect();
if first == 0 || last > lines.len() || first > last {
return Err(format!(
"citation {:?}: {} has {} lines",
self.clause,
full.display(),
lines.len()
));
}
let cited = lines[first - 1..last].join("\n");
if cited.contains(&self.quote) {
return Ok(());
}
Err(format!(
"citation {:?} does not quote the spec. Cited text:\n{cited}\nQuote:\n{}",
self.clause, self.quote
))
}
fn parse_clause(&self) -> Result<(&str, usize, usize), String> {
let (path, lines) = self
.clause
.rsplit_once(':')
.ok_or_else(|| format!("citation {:?} is not `<path>:<line>`", self.clause))?;
let (first, last) = match lines.split_once('-') {
Some((a, b)) => (a, b),
None => (lines, lines),
};
let parse = |s: &str| {
s.parse::<usize>()
.map_err(|_| format!("citation {:?} has a non-numeric line", self.clause))
};
Ok((path, parse(first)?, parse(last)?))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn citation(clause: &str, quote: &str) -> Citation {
Citation {
clause: clause.to_string(),
quote: quote.to_string(),
}
}
#[test]
fn a_clause_without_a_line_is_rejected() {
let err = citation("docs/recommendations/general.md", "anything")
.parse_clause()
.unwrap_err();
assert!(err.contains("is not `<path>:<line>`"), "{err}");
}
#[test]
fn a_single_line_clause_parses_as_a_one_line_range() {
let cited = citation("a/b.md:41", "q");
let parsed = cited.parse_clause().unwrap();
assert_eq!(parsed, ("a/b.md", 41, 41));
}
#[test]
fn a_range_clause_parses_both_endpoints() {
let cited = citation("a/b.md:21-22", "q");
let parsed = cited.parse_clause().unwrap();
assert_eq!(parsed, ("a/b.md", 21, 22));
}
#[test]
fn a_non_numeric_line_is_rejected() {
let err = citation("a/b.md:top", "q").parse_clause().unwrap_err();
assert!(err.contains("non-numeric line"), "{err}");
}
#[test]
fn recording_a_defect_classifies_a_row_and_exempts_it_from_nothing() {
let mut case = Case {
input: "x".to_string(),
expected: None,
confluence_class: None,
citation: citation("a/b.md:1", "q"),
note: "n".to_string(),
defect: None,
};
assert!(!case.is_red());
for kind in [DefectKind::Confluence, DefectKind::AdjacentMembers] {
case.defect = Some(Defect {
issue: "#1".to_string(),
kind,
summary: "s".to_string(),
});
assert!(case.is_red(), "{kind:?} marks the row red");
}
case.defect = None;
assert!(!case.is_red());
}
}