Skip to content

Commit 5c2d219

Browse files
committed
refactor: split eval fixture and feedback eval helpers
Break fixture loading and feedback-eval orchestration into smaller units so follow-on changes stay easier to isolate and verify. Made-with: Cursor
1 parent 8545be8 commit 5c2d219

9 files changed

Lines changed: 443 additions & 385 deletions

File tree

src/commands/eval/fixtures.rs

Lines changed: 14 additions & 183 deletions
Original file line numberDiff line numberDiff line change
@@ -1,192 +1,23 @@
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;
59

6-
use crate::core::eval_benchmarks::CommunityFixturePack;
10+
pub(super) use loading::{collect_eval_fixtures, load_eval_report};
711

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;
18414

18515
#[cfg(test)]
18616
mod tests {
187-
use super::*;
17+
use super::{collect_eval_fixtures, load_eval_fixtures_from_path};
18818
use crate::core::eval_benchmarks::{
189-
BenchmarkFixture, BenchmarkThresholds, Difficulty, ExpectedFinding, NegativeFinding,
19+
BenchmarkFixture, BenchmarkThresholds, CommunityFixturePack, Difficulty, ExpectedFinding,
20+
NegativeFinding,
19021
};
19122
use std::collections::HashMap;
19223
use tempfile::tempdir;
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
use anyhow::Result;
2+
use std::path::{Path, PathBuf};
3+
4+
pub(super) fn collect_fixture_paths(fixtures_dir: &Path) -> Result<Vec<PathBuf>> {
5+
if !fixtures_dir.exists() {
6+
anyhow::bail!("Fixtures directory not found: {}", fixtures_dir.display());
7+
}
8+
if !fixtures_dir.is_dir() {
9+
anyhow::bail!(
10+
"Fixtures path is not a directory: {}",
11+
fixtures_dir.display()
12+
);
13+
}
14+
15+
let mut paths = Vec::new();
16+
let mut stack = vec![fixtures_dir.to_path_buf()];
17+
18+
while let Some(dir) = stack.pop() {
19+
for entry in std::fs::read_dir(&dir)? {
20+
let entry = entry?;
21+
let path = entry.path();
22+
if path.is_dir() {
23+
stack.push(path);
24+
continue;
25+
}
26+
27+
let extension = path
28+
.extension()
29+
.and_then(|value| value.to_str())
30+
.map(|value| value.to_ascii_lowercase());
31+
if matches!(extension.as_deref(), Some("json" | "yml" | "yaml")) {
32+
paths.push(path);
33+
}
34+
}
35+
}
36+
37+
paths.sort();
38+
Ok(paths)
39+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
use anyhow::Result;
2+
use serde::Deserialize;
3+
use std::path::Path;
4+
5+
use crate::core::eval_benchmarks::CommunityFixturePack;
6+
7+
use super::super::{EvalFixture, EvalReport, LoadedEvalFixture};
8+
use super::discovery::collect_fixture_paths;
9+
use super::packs::expand_community_fixture_pack;
10+
use super::validation::validate_eval_fixture;
11+
12+
pub(in super::super) fn collect_eval_fixtures(
13+
fixtures_dir: &Path,
14+
) -> Result<Vec<LoadedEvalFixture>> {
15+
let mut fixtures = Vec::new();
16+
for path in collect_fixture_paths(fixtures_dir)? {
17+
fixtures.extend(load_eval_fixtures_from_path(&path)?);
18+
}
19+
fixtures.sort_by(|left, right| {
20+
left.fixture_path
21+
.cmp(&right.fixture_path)
22+
.then_with(|| left.fixture.name.cmp(&right.fixture.name))
23+
});
24+
Ok(fixtures)
25+
}
26+
27+
pub(super) fn load_eval_fixtures_from_path(path: &Path) -> Result<Vec<LoadedEvalFixture>> {
28+
let content = std::fs::read_to_string(path)?;
29+
30+
if let Ok(pack) = load_fixture_file::<CommunityFixturePack>(path, &content) {
31+
return expand_community_fixture_pack(path, pack);
32+
}
33+
34+
let fixture = load_eval_fixture_from_content(path, &content)?;
35+
Ok(vec![LoadedEvalFixture {
36+
fixture_path: path.to_path_buf(),
37+
fixture,
38+
suite_name: None,
39+
suite_thresholds: None,
40+
difficulty: None,
41+
}])
42+
}
43+
44+
pub(in super::super) fn load_eval_report(path: &Path) -> Result<EvalReport> {
45+
let content = std::fs::read_to_string(path)?;
46+
let report: EvalReport = serde_json::from_str(&content)?;
47+
Ok(report)
48+
}
49+
50+
fn load_eval_fixture_from_content(path: &Path, content: &str) -> Result<EvalFixture> {
51+
let fixture = load_fixture_file::<EvalFixture>(path, content)?;
52+
validate_eval_fixture(&fixture)?;
53+
Ok(fixture)
54+
}
55+
56+
fn load_fixture_file<T>(path: &Path, content: &str) -> Result<T>
57+
where
58+
T: for<'de> Deserialize<'de>,
59+
{
60+
let extension = path
61+
.extension()
62+
.and_then(|value| value.to_str())
63+
.map(|value| value.to_ascii_lowercase());
64+
match extension.as_deref() {
65+
Some("json") => Ok(serde_json::from_str(content)?),
66+
_ => match serde_yaml::from_str(content) {
67+
Ok(parsed) => Ok(parsed),
68+
Err(_) => Ok(serde_json::from_str(content)?),
69+
},
70+
}
71+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
use anyhow::Result;
2+
use std::path::Path;
3+
4+
use crate::core::eval_benchmarks::CommunityFixturePack;
5+
6+
use super::super::{EvalExpectations, EvalFixture, EvalPattern, LoadedEvalFixture};
7+
use super::validation::validate_eval_fixture;
8+
9+
pub(super) fn expand_community_fixture_pack(
10+
path: &Path,
11+
pack: CommunityFixturePack,
12+
) -> Result<Vec<LoadedEvalFixture>> {
13+
let pack_name = pack.name;
14+
let thresholds = pack.thresholds;
15+
pack.fixtures
16+
.into_iter()
17+
.map(|fixture| {
18+
let difficulty = fixture.difficulty.clone();
19+
let eval_fixture = EvalFixture {
20+
name: Some(format!("{}/{}", pack_name, fixture.name)),
21+
diff: Some(fixture.diff_content),
22+
diff_file: None,
23+
repo_path: None,
24+
expect: EvalExpectations {
25+
must_find: fixture
26+
.expected_findings
27+
.into_iter()
28+
.map(|finding| EvalPattern {
29+
file: finding.file_pattern,
30+
line: finding.line_hint,
31+
contains: finding.contains,
32+
severity: finding.severity,
33+
category: finding.category,
34+
rule_id: finding.rule_id.clone(),
35+
require_rule_id: finding.rule_id.is_some(),
36+
..Default::default()
37+
})
38+
.collect(),
39+
must_not_find: fixture
40+
.negative_findings
41+
.into_iter()
42+
.map(|finding| EvalPattern {
43+
file: finding.file_pattern,
44+
contains: finding.contains,
45+
..Default::default()
46+
})
47+
.collect(),
48+
min_total: None,
49+
max_total: None,
50+
},
51+
};
52+
validate_eval_fixture(&eval_fixture)?;
53+
54+
Ok(LoadedEvalFixture {
55+
fixture_path: path.to_path_buf(),
56+
fixture: eval_fixture,
57+
suite_name: Some(pack_name.clone()),
58+
suite_thresholds: thresholds.clone(),
59+
difficulty: Some(difficulty),
60+
})
61+
})
62+
.collect::<Result<Vec<_>>>()
63+
}

0 commit comments

Comments
 (0)