diff --git a/src/discovery/resolver.rs b/src/discovery/resolver.rs index dd1ffb5..0acc0d9 100644 --- a/src/discovery/resolver.rs +++ b/src/discovery/resolver.rs @@ -93,6 +93,22 @@ pub struct RunnableTest { pub marker_info: Vec, } +impl RunnableTest { + /// Returns true if this test has `@pytest.mark.django_db(transaction=True)`. + /// + /// Transaction tests commit directly to the database, requiring process-level + /// isolation (toxic mode) instead of savepoint-based rollback. + pub fn has_django_transaction_marker(&self) -> bool { + self.marker_info.iter().any(|m| { + m.name == "django_db" + && m.args + .get("transaction") + .and_then(|v| v.as_bool()) + .unwrap_or(false) + }) + } +} + /// A resolved fixture with full context #[derive(Debug, Clone)] pub struct ResolvedFixture { @@ -450,7 +466,7 @@ impl<'a> Resolver<'a> { #[cfg(test)] mod tests { use super::*; - use crate::discovery::TestModule; + use crate::discovery::{MarkerInfo, TestModule}; /// Helper to create a fixture definition fn make_fixture(name: &str, deps: Vec<&str>) -> FixtureDefinition { @@ -875,4 +891,63 @@ mod tests { // Inner fixture has function scope (default) assert_eq!(runnable[0].fixtures[0].scope, FixtureScope::Function); } + + #[test] + fn test_has_django_transaction_marker_true() { + let test = RunnableTest { + file_path: PathBuf::from("test_views.py"), + test_name: "test_create_user".to_string(), + is_async: false, + fixtures: vec![], + is_toxic: false, + timeout_secs: None, + markers: vec!["django_db".to_string()], + marker_info: vec![MarkerInfo { + name: "django_db".to_string(), + args: { + let mut m = std::collections::HashMap::new(); + m.insert("transaction".to_string(), serde_json::Value::Bool(true)); + m + }, + }], + }; + assert!(test.has_django_transaction_marker()); + } + + #[test] + fn test_has_django_transaction_marker_false_when_no_marker() { + let test = RunnableTest { + file_path: PathBuf::from("test_utils.py"), + test_name: "test_parse".to_string(), + is_async: false, + fixtures: vec![], + is_toxic: false, + timeout_secs: None, + markers: vec![], + marker_info: vec![], + }; + assert!(!test.has_django_transaction_marker()); + } + + #[test] + fn test_has_django_transaction_marker_false_when_transaction_false() { + let test = RunnableTest { + file_path: PathBuf::from("test_views.py"), + test_name: "test_read_user".to_string(), + is_async: false, + fixtures: vec![], + is_toxic: false, + timeout_secs: None, + markers: vec!["django_db".to_string()], + marker_info: vec![MarkerInfo { + name: "django_db".to_string(), + args: { + let mut m = std::collections::HashMap::new(); + m.insert("transaction".to_string(), serde_json::Value::Bool(false)); + m + }, + }], + }; + assert!(!test.has_django_transaction_marker()); + } } diff --git a/src/main.rs b/src/main.rs index a8e85ed..f5b212d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -470,7 +470,8 @@ fn execute_session( let mut runnable_tests = runnable_tests; let mut toxic_test_count = 0; for test in &mut runnable_tests { - test.is_toxic = toxicity_graph.is_toxic(&test.file_path); + test.is_toxic = + toxicity_graph.is_toxic(&test.file_path) || test.has_django_transaction_marker(); if test.is_toxic { toxic_test_count += 1; } @@ -822,7 +823,8 @@ fn handle_dry_run_command( // --- TOXICITY TAGGING --- let mut runnable_tests = runnable_tests; for test in &mut runnable_tests { - test.is_toxic = toxicity_graph.is_toxic(&test.file_path); + test.is_toxic = + toxicity_graph.is_toxic(&test.file_path) || test.has_django_transaction_marker(); } // --- PATH FILTERING --- diff --git a/src/tach_harness.py b/src/tach_harness.py index 92ab057..ae74fcc 100644 --- a/src/tach_harness.py +++ b/src/tach_harness.py @@ -2784,8 +2784,13 @@ def _apply_django_db_isolation( "databases": None, } - # If transaction=True, skip isolation (test manages its own transactions) + # If transaction=True, the test manages its own transactions and may + # commit directly to the database. Savepoint-based isolation won't work + # because commits escape the savepoint. Instead, these tests are marked + # toxic so the worker exits after running — the Zygote's clean DB + # snapshot is restored on the next fork. if marker_args.get("transaction", False): + _close_django_connections() return [] # Determine which databases to isolate