use std::path::Path;
use super::parse_file;
macro_rules! python_parser_fixture {
($path:literal) => {
include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/tests/fixtures/python/",
$path
))
};
}
#[test]
fn test_python_parser_extracts_functions_imports_and_strings() {
let source = python_parser_fixture!("parser/async_calls_positive.txt");
let parsed =
parse_file(Path::new("pkg/service.py"), source).expect("python parsing should succeed");
assert_eq!(parsed.package_name.as_deref(), Some("service"));
assert_eq!(parsed.imports.len(), 2);
assert_eq!(parsed.pkg_strings.len(), 1);
assert_eq!(parsed.pkg_strings[0].name, "API_TOKEN");
assert_eq!(parsed.functions.len(), 3);
let fetch = &parsed.functions[0];
assert_eq!(fetch.fingerprint.name, "fetch_profile");
assert_eq!(fetch.fingerprint.kind, "async_function");
assert_eq!(fetch.local_strings.len(), 2);
assert_eq!(fetch.python_evidence().concat_loops, vec![15]);
assert!(fetch.python_evidence().exception_handlers.is_empty());
assert!(fetch.doc_comment.is_some());
assert!(
fetch
.calls
.iter()
.any(|call| call.receiver.as_deref() == Some("req") && call.name == "get")
);
let render = &parsed.functions[1];
assert_eq!(render.fingerprint.kind, "method");
assert_eq!(
render.fingerprint.receiver_type.as_deref(),
Some("Renderer")
);
let helper = &parsed.functions[2];
assert_eq!(helper.fingerprint.name, "helper");
assert!(helper.calls.iter().any(|call| call.name == "print"));
}
#[test]
fn test_python_parser_marks_syntax_errors() {
let source = python_parser_fixture!("parser/syntax_error_positive.txt");
let parsed = parse_file(Path::new("broken.py"), source)
.expect("python parsing should still return a parsed file");
assert!(parsed.syntax_error);
}
#[test]
fn test_python_test_detection() {
let source = python_parser_fixture!("parser/test_detection_positive.txt");
let parsed = parse_file(Path::new("tests/test_client.py"), source)
.expect("python parsing should succeed");
assert!(parsed.is_test_file);
assert!(parsed.functions[0].is_test_function);
assert!(parsed.functions[0].test_summary.is_some());
}
#[test]
fn test_python_exception_handler_evidence() {
let positive = parse_file(
Path::new("config.py"),
python_parser_fixture!("maintainability/exception_shapes_positive.txt"),
)
.expect("python parsing should succeed");
let negative = parse_file(
Path::new("config_safe.py"),
python_parser_fixture!("maintainability/exception_shapes_negative.txt"),
)
.expect("python parsing should succeed");
let load_config = &positive.functions[0];
assert_eq!(load_config.python_evidence().exception_handlers.len(), 1);
assert!(load_config.python_evidence().exception_handlers[0].is_broad);
assert!(load_config.python_evidence().exception_handlers[0].suppresses);
assert_eq!(
load_config.python_evidence().exception_handlers[0]
.action
.as_deref(),
Some("pass")
);
let recover_config = &negative.functions[0];
assert_eq!(recover_config.python_evidence().exception_handlers.len(), 1);
assert!(!recover_config.python_evidence().exception_handlers[0].is_broad);
}
#[test]
fn test_python_async_io_fixture_contract() {
let positive = parse_file(
Path::new("pkg/network_sync.py"),
python_parser_fixture!("performance/async_io_positive.txt"),
)
.expect("python parsing should succeed");
let negative = parse_file(
Path::new("pkg/network_async.py"),
python_parser_fixture!("performance/async_io_negative.txt"),
)
.expect("python parsing should succeed");
let sync_reports = &positive.functions[0];
assert!(sync_reports.fingerprint.kind.starts_with("async_"));
assert!(sync_reports.python_evidence().is_async);
assert!(sync_reports.calls.iter().any(|call| call.name == "get"));
let async_reports = &negative.functions[0];
assert!(async_reports.fingerprint.kind.starts_with("async_"));
assert!(async_reports.python_evidence().is_async);
assert!(async_reports.calls.iter().any(|call| call.name == "get"));
}
#[test]
fn test_python_type_hint_fixture_contract() {
let positive = parse_file(
Path::new("pkg/api_types.py"),
python_parser_fixture!("maintainability/type_hints_positive.txt"),
)
.expect("python parsing should succeed");
let negative = parse_file(
Path::new("pkg/api_types_partial.py"),
python_parser_fixture!("maintainability/type_hints_negative.txt"),
)
.expect("python parsing should succeed");
assert!(
positive.functions[0]
.python_evidence()
.has_complete_type_hints
);
assert!(
!negative.functions[0]
.python_evidence()
.has_complete_type_hints
);
}
#[test]
fn test_python_phase4_parser_evidence() {
let source = python_parser_fixture!("parser/class_summary_positive.txt");
let parsed =
parse_file(Path::new("pkg/service.py"), source).expect("python parsing should succeed");
let process_items = &parsed.functions[0];
assert_eq!(
process_items.python_evidence().none_comparison_lines,
vec![3]
);
assert_eq!(
process_items.python_evidence().redundant_return_none_lines,
vec![4]
);
assert_eq!(
process_items
.python_evidence()
.side_effect_comprehension_lines,
vec![6]
);
assert_eq!(
process_items.python_evidence().list_materialization_lines,
vec![7]
);
assert_eq!(
process_items.python_evidence().deque_operation_lines,
vec![9]
);
assert_eq!(
process_items.python_evidence().temp_collection_lines,
vec![13]
);
assert_eq!(
process_items.python_evidence().list_membership_loop_lines,
vec![15]
);
assert_eq!(
process_items.python_evidence().repeated_len_loop_lines,
vec![12]
);
assert_eq!(
process_items.python_evidence().builtin_candidate_lines,
vec![12]
);
assert!(process_items.python_evidence().has_varargs);
assert!(process_items.python_evidence().has_kwargs);
assert!(!process_items.python_evidence().has_complete_type_hints);
assert!(
process_items
.python_evidence()
.validation_signature
.is_some()
);
assert!(!process_items.python_evidence().normalized_body.is_empty());
let scan_tree = &parsed.functions[1];
assert_eq!(scan_tree.python_evidence().recursive_call_lines, vec![23]);
let read_config = &parsed.functions[2];
assert_eq!(
read_config.python_evidence().missing_context_manager_lines,
vec![26]
);
assert_eq!(parsed.class_summaries().len(), 2);
let payload_manager = &parsed.class_summaries()[1];
assert_eq!(payload_manager.name, "PayloadManager");
assert_eq!(payload_manager.method_count, 3);
assert_eq!(payload_manager.instance_attribute_count, 13);
assert_eq!(payload_manager.public_method_count, 2);
assert_eq!(payload_manager.base_classes, vec!["BaseManager"]);
assert_eq!(payload_manager.constructor_collaborator_count, 3);
}
#[test]
fn test_python_init_reexports_are_indexed_as_symbols() {
let source = python_parser_fixture!("parser/reexports_positive.txt");
let parsed = parse_file(Path::new("pkg/widgets/__init__.py"), source)
.expect("python parsing should succeed");
assert!(parsed.imports.iter().all(|import| import.is_public));
assert!(
parsed
.symbols
.iter()
.any(|symbol| symbol.name == "WidgetTemplate")
);
assert!(
parsed
.symbols
.iter()
.any(|symbol| symbol.name == "LayoutConfig")
);
assert!(parsed.symbols.iter().any(|symbol| symbol.name == "Heading"));
assert!(
parsed
.symbols
.iter()
.any(|symbol| symbol.name == "render_widget")
);
}
#[test]
fn test_python_non_init_relative_imports_do_not_become_public_reexports() {
let source = python_parser_fixture!("parser/reexports_negative.txt");
let parsed = parse_file(Path::new("pkg/widgets/helpers.py"), source)
.expect("python parsing should succeed");
assert!(parsed.imports.iter().all(|import| !import.is_public));
assert!(parsed.symbols.iter().all(|symbol| {
!matches!(
symbol.name.as_str(),
"WidgetTemplate" | "LayoutConfig" | "_HiddenWidget" | "render_widget"
)
}));
}
#[test]
fn test_python_parenthesized_from_import_ignores_inline_comments() {
let source = python_parser_fixture!("parser/parenthesized_imports_positive.txt");
let parsed = parse_file(Path::new("tests/test_widgets.py"), source)
.expect("python parsing should succeed");
assert!(
parsed
.imports
.iter()
.any(|import| import.alias == "WidgetTemplate")
);
assert!(
parsed
.imports
.iter()
.any(|import| import.alias == "LayoutConfig")
);
assert!(
parsed
.imports
.iter()
.any(|import| import.alias == "Heading")
);
}
#[test]
fn test_python_commentary_fixtures_extract_real_comments_only() {
let positive = parse_file(
Path::new("pkg/comments.py"),
python_parser_fixture!("ai_smells/commentary_positive.txt"),
)
.expect("python parsing should succeed");
let negative = parse_file(
Path::new("pkg/comments_safe.py"),
python_parser_fixture!("ai_smells/commentary_negative.txt"),
)
.expect("python parsing should succeed");
assert_eq!(positive.comments.len(), 2);
assert_eq!(positive.comments[0].text, "set the running total");
assert_eq!(positive.comments[1].text, "return the final count");
assert!(
positive
.comments
.iter()
.all(|comment| !comment.text.contains("not a comment"))
);
assert!(negative.comments.is_empty());
}
#[test]
fn test_python_boundary_fixture_contract() {
let network_positive = parse_file(
Path::new("pkg/network_sync.py"),
python_parser_fixture!("maintainability/boundary_network_positive.txt"),
)
.expect("python parsing should succeed");
let network_negative = parse_file(
Path::new("pkg/network_safe.py"),
python_parser_fixture!("maintainability/boundary_network_negative.txt"),
)
.expect("python parsing should succeed");
let config_positive = parse_file(
Path::new("pkg/config_loader.py"),
python_parser_fixture!("maintainability/boundary_config_positive.txt"),
)
.expect("python parsing should succeed");
let cli_positive = parse_file(
Path::new("pkg/cli.py"),
python_parser_fixture!("maintainability/boundary_cli_positive.txt"),
)
.expect("python parsing should succeed");
let cli_negative = parse_file(
Path::new("pkg/cli_safe.py"),
python_parser_fixture!("maintainability/boundary_cli_negative.txt"),
)
.expect("python parsing should succeed");
let sync_reports = &network_positive.functions[0];
assert!(
sync_reports
.calls
.iter()
.any(|call| { call.receiver.as_deref() == Some("requests") && call.name == "get" })
);
assert!(
!sync_reports
.body_text
.to_ascii_lowercase()
.contains("timeout=")
);
let safe_reports = &network_negative.functions[0];
assert!(
safe_reports
.calls
.iter()
.any(|call| { call.receiver.as_deref() == Some("requests") && call.name == "get" })
);
assert!(
safe_reports
.body_text
.to_ascii_lowercase()
.contains("timeout=5")
);
let load_runtime_config = &config_positive.functions[0];
assert!(
load_runtime_config
.calls
.iter()
.any(|call| { call.receiver.as_deref() == Some("os") && call.name == "getenv" })
);
let run_cli = &cli_positive.functions[0];
assert!(
run_cli
.calls
.iter()
.any(|call| { call.receiver.as_deref() == Some("json") && call.name == "loads" })
);
assert!(!run_cli.body_text.contains("len(sys.argv) < 2"));
let safe_cli = &cli_negative.functions[0];
assert!(
safe_cli
.calls
.iter()
.any(|call| { call.receiver.as_deref() == Some("json") && call.name == "loads" })
);
assert!(safe_cli.body_text.contains("len(sys.argv) < 2"));
}
#[test]
fn test_python_symbol_extraction_preserves_naming_styles() {
let source = python_parser_fixture!("ai_smells/naming_styles_positive.txt");
let parsed =
parse_file(Path::new("pkg/naming_mix.py"), source).expect("python parsing should succeed");
let symbol_names = parsed
.symbols
.iter()
.map(|symbol| symbol.name.as_str())
.collect::<Vec<_>>();
assert!(symbol_names.contains(&"HTTPClient"));
assert!(symbol_names.contains(&"widget_renderer"));
assert!(symbol_names.contains(&"render_json"));
assert!(symbol_names.contains(&"buildHTML"));
}
#[test]
fn test_python_advanceplan2_parser_evidence() {
let source = python_parser_fixture!("parser/advanceplan2_evidence.txt");
let parsed =
parse_file(Path::new("pkg/service.py"), source).expect("python parsing should succeed");
assert_eq!(parsed.module_scope_calls.len(), 1);
assert_eq!(parsed.module_scope_calls[0].name, "Client");
assert_eq!(parsed.top_level_bindings.len(), 1);
assert_eq!(parsed.top_level_bindings[0].name, "SETTINGS");
assert_eq!(parsed.python_models().len(), 2);
let payload = parsed
.python_models()
.iter()
.find(|model| model.name == "Payload")
.expect("expected dataclass summary");
assert!(payload.is_dataclass);
assert_eq!(payload.fields.len(), 2);
assert_eq!(payload.fields[0].default_text.as_deref(), Some("[]"));
let payload_shape = parsed
.python_models()
.iter()
.find(|model| model.name == "PayloadShape")
.expect("expected typeddict summary");
assert!(payload_shape.is_typed_dict);
assert_eq!(payload_shape.fields.len(), 2);
assert_eq!(
payload_shape.fields[1].annotation_text.as_deref(),
Some("NotRequired[str]")
);
let orchestrate = parsed
.functions
.iter()
.find(|function| function.fingerprint.name == "orchestrate")
.expect("expected async function summary");
assert!(orchestrate.python_evidence().is_async);
assert_eq!(
orchestrate.python_evidence().await_points,
vec![orchestrate.body_start_line + 2]
);
assert!(
orchestrate
.signature_text
.contains("async def orchestrate(lock):")
);
}