alef 0.75.0

Opinionated polyglot binding generator for Rust libraries
Documentation
//! Verifies alef's generated Python e2e output survives a realistic consumer `ruff check`
//! untouched -- the class of defect fixed by task #528. Two symptoms were measured against a
//! real consumer regen: `from typing import Generator` is UP035-deprecated (autofixed to
//! `from collections.abc import Generator` on every `lint --fix`), and a pile of `# noqa: `
//! directives (`PLC0415`, `S603`, `S310`, `ARG001`, and — by far the largest volume — `S101`
//! on nearly every emitted `assert`) go RUF100-dirty under a consumer's own rule selection.
//! Either symptom makes a consumer's own `lint --fix` rewrite alef's stamped output, drifting
//! it off its `alef:hash` in between and eventually out of alef's ownership entirely.
//!
//! Unlike an isolated `ruff --isolated` probe (which uses ruff's *default* rules and proves
//! nothing about a real consumer config), these tests run `ruff check` against a config that
//! actually enables `UP` + `RUF` plus every opt-in family alef's generated code deliberately
//! suppresses per-construct (`S`, `PLC0415`, `ARG`, `ANN`, `A`, `T20`, `N`, `SIM`, `B`) and the
//! import sorter (`I`), with `S101` ignored for `test_*.py`/`conftest.py` — the standard
//! pytest+bandit convention. A `# noqa` is only safe when the rule it names is both enabled
//! *and* would otherwise fire on that exact line; this harness catches both directions: an
//! unselected/redundant noqa (RUF100) and a real, unsuppressed violation.
//!
//! Skips (rather than failing) when `ruff` is not on `PATH`, mirroring
//! `e2e::codegen::go::tests::main_test_go_gofmt_tests`'s formatter self-skip.

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;

/// See the module doc for why each family is here. `S101` is ignored the way virtually every
/// pytest project that also enables bandit rules configures it — and task #528's own probe
/// showed that even *that* standard exclusion makes a *present* `# noqa: S101` RUF100-dirty,
/// not just an absent one, since the per-file-ignore already suppresses the finding on its
/// own. ~keep
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()
}

/// Runs `ruff check` over `files` (filename -> content) under [`RUFF_CONFIG`]. Returns the
/// combined stdout+stderr and whether the check passed.
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)
}

/// Builds a mixed fixture set exercising every branch this fix touched: an HTTP fixture with
/// a JSON request+response body (hoisted `json` import, `S310` on `Request(...)`), an HTTP
/// fixture with a `uuid` header assertion (hoisted `re` import), a plain assertion, a
/// `matches_regex` assertion (`re.search`), a `not_empty` assertion, an `error` assertion
/// (`pytest.raises`/`B017`), a fixture skipped for python (`@pytest.mark.skip`), a fixture
/// with both a mock response and an env api key (the `sys.stdout.write` real/mock branch),
/// a fixture with an env api key and no mock (the `pytest.skip(...)` branch), and a visitor
/// fixture whose `visit_heading` callback needs the conditional `A002` suppression.
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()
    }
}

/// The measured defect, reproduced end to end: `render_conftest` and `render_test_file`'s
/// real output — not a hand-simplified stand-in — must not change under a `ruff check` that
/// enables the rule families the generator suppresses per-construct.
#[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}"
    );
}

/// Negative control: a deliberately deprecated import run through the exact same harness must
/// still be flagged. Without this, a silently-misconfigured `run_ruff_check` (wrong config
/// path, `ruff` swallowing an error, an empty `select`) would render the positive test above
/// as a pass no matter what it was fed — proving nothing.
#[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}"
    );
}