Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 76 additions & 1 deletion src/discovery/resolver.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,22 @@ pub struct RunnableTest {
pub marker_info: Vec<crate::discovery::MarkerInfo>,
}

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 {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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());
}
}
6 changes: 4 additions & 2 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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 ---
Expand Down
7 changes: 6 additions & 1 deletion src/tach_harness.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down