use std::process::{Command, Stdio};
use crate::core::config::ResolvedCrateConfig;
use crate::e2e::config::{CallConfig, CallOverride, E2eConfig};
use crate::e2e::fixture::{Fixture, FixtureGroup};
use super::super::config::render_conftest;
use super::render_test_file;
const RUFF_CONFIG: &str = r#"
[tool.ruff.lint]
select = ["S", "PLC0415", "ARG", "UP", "RUF", "I", "F", "N", "SIM", "B", "ANN", "A", "T20"]
[tool.ruff.lint.per-file-ignores]
"test_*.py" = ["S101"]
"conftest.py" = ["S101"]
"#;
fn ruff_available() -> bool {
which::which("ruff").is_ok()
}
fn run_ruff_check(files: &[(&str, &str)]) -> (bool, String) {
let dir = tempfile::tempdir().expect("tempdir");
std::fs::write(dir.path().join("pyproject.toml"), RUFF_CONFIG).expect("write pyproject.toml");
for (name, content) in files {
std::fs::write(dir.path().join(name), content).expect("write python file");
}
let output = Command::new("ruff")
.args(["check", "--config", "pyproject.toml", "--no-cache", "."])
.current_dir(dir.path())
.stdin(Stdio::null())
.output()
.expect("run ruff check");
let combined = format!(
"{}{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
(output.status.success(), combined)
}
fn mixed_fixtures() -> Vec<Fixture> {
let specs = [
serde_json::json!({
"id": "http_json_roundtrip",
"description": "Round-trips a JSON body",
"http": {
"handler": {"route": "/widgets", "method": "POST"},
"request": {"method": "POST", "path": "/widgets", "body": {"name": "alef"}},
"expected_response": {"status_code": 200, "body": {"ok": true}}
}
}),
serde_json::json!({
"id": "http_uuid_header",
"description": "Checks a uuid response header",
"http": {
"handler": {"route": "/widgets", "method": "GET"},
"request": {"method": "GET", "path": "/widgets"},
"expected_response": {"status_code": 200, "headers": {"x-request-id": "<<uuid>>"}}
}
}),
serde_json::json!({
"id": "basic_call",
"description": "Calls the function",
"input": null,
"assertions": [{"type": "not_empty", "field": "items"}]
}),
serde_json::json!({
"id": "regex_match",
"description": "Field matches a pattern",
"assertions": [{"type": "matches_regex", "field": "id", "value": "^[a-z]+$"}]
}),
serde_json::json!({
"id": "invalid_input_errors",
"description": "Raises on invalid input",
"assertions": [{"type": "error"}]
}),
serde_json::json!({
"id": "skipped_case",
"description": "Skipped for python",
"skip": {"languages": ["python"], "reason": "not supported"},
"assertions": [{"type": "not_empty", "field": "items"}]
}),
serde_json::json!({
"id": "real_or_mock",
"description": "Uses the real API when a key is configured",
"env": {"api_key_var": "MY_API_KEY"},
"mock_response": {"status": 200, "body": {}},
"assertions": [{"type": "not_empty", "field": "items"}]
}),
serde_json::json!({
"id": "requires_real_api",
"description": "Requires a live API key",
"env": {"api_key_var": "MY_API_KEY"},
"assertions": [{"type": "not_empty", "field": "items"}]
}),
serde_json::json!({
"id": "visitor_heading",
"description": "Visits headings",
"visitor": {"callbacks": {"visit_heading": {"action": "skip"}}},
"assertions": [{"type": "not_empty", "field": "items"}]
}),
];
specs
.into_iter()
.map(|spec| serde_json::from_value(spec).expect("fixture must parse"))
.collect()
}
fn e2e_config_with_client_factory() -> E2eConfig {
let mut overrides = std::collections::HashMap::new();
overrides.insert(
"python".to_string(),
CallOverride {
client_factory: Some("create_client".to_string()),
..Default::default()
},
);
E2eConfig {
call: CallConfig {
function: "do_thing".to_string(),
module: "mypackage".to_string(),
result_var: "result".to_string(),
overrides,
..Default::default()
},
..Default::default()
}
}
#[test]
fn conftest_and_test_file_survive_a_realistic_ruff_lint_pass() {
if !ruff_available() {
return;
}
let fixtures = mixed_fixtures();
let e2e_config = e2e_config_with_client_factory();
let groups = vec![FixtureGroup {
category: "basic".to_string(),
fixtures: fixtures.clone(),
}];
let conftest_py = render_conftest(&e2e_config, &groups);
let fixture_refs: Vec<&Fixture> = fixtures.iter().collect();
let config = ResolvedCrateConfig::default();
let test_basic_py = render_test_file("basic", &fixture_refs, &e2e_config, &config, &[], &[], &[], false);
let (success, output) = run_ruff_check(&[
("conftest.py", conftest_py.as_str()),
("test_basic.py", test_basic_py.as_str()),
]);
assert!(
success,
"generated Python must survive a realistic ruff lint pass untouched; ruff reported:\n{output}\n\
--- conftest.py ---\n{conftest_py}\n--- test_basic.py ---\n{test_basic_py}"
);
}
#[test]
fn the_harness_still_flags_a_deliberately_deprecated_import() {
if !ruff_available() {
return;
}
let bad = "def f() -> None:\n from typing import Generator\n x: Generator = iter([])\n assert x\n";
let (success, output) = run_ruff_check(&[("test_negative_control.py", bad)]);
assert!(!success, "the harness must be able to fail; ruff reported:\n{output}");
assert!(
output.contains("UP035"),
"expected a UP035 (deprecated `typing.Generator` import) finding, got:\n{output}"
);
}