|
1 | | -use anyhow::Result; |
2 | | -use regex::Regex; |
3 | | -use serde::Deserialize; |
4 | | -use std::path::{Path, PathBuf}; |
| 1 | +#[path = "fixtures/discovery.rs"] |
| 2 | +mod discovery; |
| 3 | +#[path = "fixtures/loading.rs"] |
| 4 | +mod loading; |
| 5 | +#[path = "fixtures/packs.rs"] |
| 6 | +mod packs; |
| 7 | +#[path = "fixtures/validation.rs"] |
| 8 | +mod validation; |
5 | 9 |
|
6 | | -use crate::core::eval_benchmarks::CommunityFixturePack; |
| 10 | +pub(super) use loading::{collect_eval_fixtures, load_eval_report}; |
7 | 11 |
|
8 | | -use super::{EvalExpectations, EvalFixture, EvalPattern, EvalReport, LoadedEvalFixture}; |
9 | | - |
10 | | -pub(super) fn collect_fixture_paths(fixtures_dir: &Path) -> Result<Vec<PathBuf>> { |
11 | | - if !fixtures_dir.exists() { |
12 | | - anyhow::bail!("Fixtures directory not found: {}", fixtures_dir.display()); |
13 | | - } |
14 | | - if !fixtures_dir.is_dir() { |
15 | | - anyhow::bail!( |
16 | | - "Fixtures path is not a directory: {}", |
17 | | - fixtures_dir.display() |
18 | | - ); |
19 | | - } |
20 | | - |
21 | | - let mut paths = Vec::new(); |
22 | | - let mut stack = vec![fixtures_dir.to_path_buf()]; |
23 | | - |
24 | | - while let Some(dir) = stack.pop() { |
25 | | - for entry in std::fs::read_dir(&dir)? { |
26 | | - let entry = entry?; |
27 | | - let path = entry.path(); |
28 | | - if path.is_dir() { |
29 | | - stack.push(path); |
30 | | - continue; |
31 | | - } |
32 | | - |
33 | | - let extension = path |
34 | | - .extension() |
35 | | - .and_then(|value| value.to_str()) |
36 | | - .map(|value| value.to_ascii_lowercase()); |
37 | | - if matches!(extension.as_deref(), Some("json" | "yml" | "yaml")) { |
38 | | - paths.push(path); |
39 | | - } |
40 | | - } |
41 | | - } |
42 | | - |
43 | | - paths.sort(); |
44 | | - Ok(paths) |
45 | | -} |
46 | | - |
47 | | -pub(super) fn collect_eval_fixtures(fixtures_dir: &Path) -> Result<Vec<LoadedEvalFixture>> { |
48 | | - let mut fixtures = Vec::new(); |
49 | | - for path in collect_fixture_paths(fixtures_dir)? { |
50 | | - fixtures.extend(load_eval_fixtures_from_path(&path)?); |
51 | | - } |
52 | | - fixtures.sort_by(|left, right| { |
53 | | - left.fixture_path |
54 | | - .cmp(&right.fixture_path) |
55 | | - .then_with(|| left.fixture.name.cmp(&right.fixture.name)) |
56 | | - }); |
57 | | - Ok(fixtures) |
58 | | -} |
59 | | - |
60 | | -pub(super) fn load_eval_fixtures_from_path(path: &Path) -> Result<Vec<LoadedEvalFixture>> { |
61 | | - let content = std::fs::read_to_string(path)?; |
62 | | - |
63 | | - if let Ok(pack) = load_fixture_file::<CommunityFixturePack>(path, &content) { |
64 | | - return expand_community_fixture_pack(path, pack); |
65 | | - } |
66 | | - |
67 | | - let fixture = load_eval_fixture_from_content(path, &content)?; |
68 | | - Ok(vec![LoadedEvalFixture { |
69 | | - fixture_path: path.to_path_buf(), |
70 | | - fixture, |
71 | | - suite_name: None, |
72 | | - suite_thresholds: None, |
73 | | - difficulty: None, |
74 | | - }]) |
75 | | -} |
76 | | - |
77 | | -fn load_eval_fixture_from_content(path: &Path, content: &str) -> Result<EvalFixture> { |
78 | | - let fixture = load_fixture_file::<EvalFixture>(path, content)?; |
79 | | - validate_eval_fixture(&fixture)?; |
80 | | - Ok(fixture) |
81 | | -} |
82 | | - |
83 | | -fn load_fixture_file<T>(path: &Path, content: &str) -> Result<T> |
84 | | -where |
85 | | - T: for<'de> Deserialize<'de>, |
86 | | -{ |
87 | | - let extension = path |
88 | | - .extension() |
89 | | - .and_then(|value| value.to_str()) |
90 | | - .map(|value| value.to_ascii_lowercase()); |
91 | | - match extension.as_deref() { |
92 | | - Some("json") => Ok(serde_json::from_str(content)?), |
93 | | - _ => match serde_yaml::from_str(content) { |
94 | | - Ok(parsed) => Ok(parsed), |
95 | | - Err(_) => Ok(serde_json::from_str(content)?), |
96 | | - }, |
97 | | - } |
98 | | -} |
99 | | - |
100 | | -fn expand_community_fixture_pack( |
101 | | - path: &Path, |
102 | | - pack: CommunityFixturePack, |
103 | | -) -> Result<Vec<LoadedEvalFixture>> { |
104 | | - let pack_name = pack.name; |
105 | | - let thresholds = pack.thresholds; |
106 | | - pack.fixtures |
107 | | - .into_iter() |
108 | | - .map(|fixture| { |
109 | | - let difficulty = fixture.difficulty.clone(); |
110 | | - let eval_fixture = EvalFixture { |
111 | | - name: Some(format!("{}/{}", pack_name, fixture.name)), |
112 | | - diff: Some(fixture.diff_content), |
113 | | - diff_file: None, |
114 | | - repo_path: None, |
115 | | - expect: EvalExpectations { |
116 | | - must_find: fixture |
117 | | - .expected_findings |
118 | | - .into_iter() |
119 | | - .map(|finding| EvalPattern { |
120 | | - file: finding.file_pattern, |
121 | | - line: finding.line_hint, |
122 | | - contains: finding.contains, |
123 | | - severity: finding.severity, |
124 | | - category: finding.category, |
125 | | - rule_id: finding.rule_id.clone(), |
126 | | - require_rule_id: finding.rule_id.is_some(), |
127 | | - ..Default::default() |
128 | | - }) |
129 | | - .collect(), |
130 | | - must_not_find: fixture |
131 | | - .negative_findings |
132 | | - .into_iter() |
133 | | - .map(|finding| EvalPattern { |
134 | | - file: finding.file_pattern, |
135 | | - contains: finding.contains, |
136 | | - ..Default::default() |
137 | | - }) |
138 | | - .collect(), |
139 | | - min_total: None, |
140 | | - max_total: None, |
141 | | - }, |
142 | | - }; |
143 | | - validate_eval_fixture(&eval_fixture)?; |
144 | | - |
145 | | - Ok(LoadedEvalFixture { |
146 | | - fixture_path: path.to_path_buf(), |
147 | | - fixture: eval_fixture, |
148 | | - suite_name: Some(pack_name.clone()), |
149 | | - suite_thresholds: thresholds.clone(), |
150 | | - difficulty: Some(difficulty), |
151 | | - }) |
152 | | - }) |
153 | | - .collect::<Result<Vec<_>>>() |
154 | | -} |
155 | | - |
156 | | -fn validate_eval_fixture(fixture: &EvalFixture) -> Result<()> { |
157 | | - for pattern in fixture |
158 | | - .expect |
159 | | - .must_find |
160 | | - .iter() |
161 | | - .chain(fixture.expect.must_not_find.iter()) |
162 | | - { |
163 | | - if let Some(pattern_text) = pattern.matches_regex.as_deref().map(str::trim) { |
164 | | - if !pattern_text.is_empty() { |
165 | | - Regex::new(pattern_text).map_err(|error| { |
166 | | - anyhow::anyhow!( |
167 | | - "Invalid regex '{}' in fixture '{}': {}", |
168 | | - pattern_text, |
169 | | - fixture.name.as_deref().unwrap_or("<unnamed>"), |
170 | | - error |
171 | | - ) |
172 | | - })?; |
173 | | - } |
174 | | - } |
175 | | - } |
176 | | - Ok(()) |
177 | | -} |
178 | | - |
179 | | -pub(super) fn load_eval_report(path: &Path) -> Result<EvalReport> { |
180 | | - let content = std::fs::read_to_string(path)?; |
181 | | - let report: EvalReport = serde_json::from_str(&content)?; |
182 | | - Ok(report) |
183 | | -} |
| 12 | +#[cfg(test)] |
| 13 | +use loading::load_eval_fixtures_from_path; |
184 | 14 |
|
185 | 15 | #[cfg(test)] |
186 | 16 | mod tests { |
187 | | - use super::*; |
| 17 | + use super::{collect_eval_fixtures, load_eval_fixtures_from_path}; |
188 | 18 | use crate::core::eval_benchmarks::{ |
189 | | - BenchmarkFixture, BenchmarkThresholds, Difficulty, ExpectedFinding, NegativeFinding, |
| 19 | + BenchmarkFixture, BenchmarkThresholds, CommunityFixturePack, Difficulty, ExpectedFinding, |
| 20 | + NegativeFinding, |
190 | 21 | }; |
191 | 22 | use std::collections::HashMap; |
192 | 23 | use tempfile::tempdir; |
|
0 commit comments