scripts.test_isolation_audit
Reference › scripts › test_isolation_audit
scripts/test_isolation_audit.py audits the test suite for isolation violations: shared state, hard-coded names and missing test_id. A human or the pre-commit review battery runs it, and it accepts individual test files so the battery can audit only the modules a change touched. It has no internal imports. See test-review, gate_parallel_safety and sweep_test_pollution, which clean up after what this audit looks for.
Each file is parsed with ast, and the checks return IsolationViolation records: check_cypher_queries (queries found in string literals, using extract_string_literals and skipping docstrings), check_fixture_scopes (fixtures that are not function-scoped), check_ast_patterns and check_module_level_state (module-level mutable state). A violation can be waived by a gate-ok comment on its line or the line above (_is_waived), by the T4 immutable opt-in marker, or file-wide by header markers in the first 30 lines (_file_waived_categories). Arguments: paths (directories or files, default tests) and --fix, which generates a fix report. A path that does not exist prints an error and exits 1; the report groups violations by file.
At a glance
| Source | scripts/test_isolation_audit.py |
| Lines of code | 615 |
| Classes | 1 |
| Functions | 13 |
| Imports (internal) | none |
| Imported by | none |
| Tests that import it | none |
Class diagram
Dependency diagram
No internal imports and no importers.
API
Classes
IsolationViolation
Line 114
| Method | Signature | Summary |
|---|---|---|
__init__ |
__init__(category: str, line: int, message: str, fix: str = '') |
|
__repr__ |
__repr__() |
Functions
| Function | Signature | Summary |
|---|---|---|
_is_waived |
_is_waived(lines: list[str], lineno: int) -> bool |
True if the 1-based lineno (or the line above) carries a gate-ok waiver. |
_is_t4_immutable_optin |
_is_t4_immutable_optin(lines: list[str], lineno: int) -> bool |
True if the 1-based lineno (or the line above) carries the T4 marker. |
_file_waived_categories |
_file_waived_categories(lines: list[str]) -> set[str] |
Categories waived file-wide via header markers (first 30 lines only). |
extract_string_literals |
extract_string_literals(tree: ast.AST) -> list[tuple[int, int, str]] |
Extract all string literals from AST with their line numbers. |
_line_is_inside_string_literal |
_line_is_inside_string_literal(string_ranges: list[tuple[int, int, str]], lineno: int) -> bool |
True iff lineno falls inside any AST string constant (1-based). |
_is_in_docstring |
_is_in_docstring(lines: list[str], line_idx: int) -> bool |
Check if a line is inside a docstring. |
check_cypher_queries |
check_cypher_queries(content: str, lines: list[str], string_ranges: list[tuple[int, int, str]] | None = None) -> list[IsolationViolation] |
Check Cypher queries in string literals for isolation issues. |
check_fixture_scopes |
check_fixture_scopes(content: str, lines: list[str]) -> list[IsolationViolation] |
Check for non-function-scoped fixtures. |
check_ast_patterns |
check_ast_patterns(tree: ast.AST, lines: list[str]) -> list[IsolationViolation] |
Check AST-level patterns for isolation violations. |
check_module_level_state |
check_module_level_state(content: str, lines: list[str]) -> list[IsolationViolation] |
Check for module-level mutable state. |
check_pytest_patterns |
check_pytest_patterns(content: str, lines: list[str]) -> list[IsolationViolation] |
Check for pytest anti-patterns. |
scan_file |
scan_file(py_file: Path) -> list[IsolationViolation] |
Scan a single test file for isolation violations. |
main |
main() -> int |
Source
#!/usr/bin/env python3
"""Audit test suite for isolation violations: shared state, hardcoded names, missing test_id.
Scans tests/ directory and reports violations that cause flaky failures under pytest -n auto.
Returns non-zero exit code if any violations are found.
Usage:
python scripts/test_isolation_audit.py [tests/path/] [--fix]
"""
import argparse
import ast
import re
import sys
from collections import defaultdict
from pathlib import Path
# Patterns for Cypher query analysis
CYPHER_CREATE_PATTERN = re.compile(
r"CREATE\s*\(\s*(\w+)\s*:\s*(\w+)\s*\{([^}]*)\}\s*\)",
re.IGNORECASE | re.MULTILINE,
)
CYPHER_MERGE_PATTERN = re.compile(
r"MERGE\s*\(\s*(\w+)\s*:\s*(\w+)\s*\{([^}]*)\}\s*\)",
re.IGNORECASE | re.MULTILINE,
)
HARDCODED_NAME_PATTERN = re.compile(
r"name\s*:\s*['\"]([^'\"]+)['\"]",
re.IGNORECASE,
)
TEST_ID_PATTERN = re.compile(
r"test_id\s*:|\$test_id|\$tid|test_id=|\{test_id\}",
re.IGNORECASE,
)
# A backtick-quoted label interpolated from a variable -- ``:`{seed_label}` `` --
# is the project's per-test throwaway-label discipline: the fixture derives the
# label from `test_id`, so every node created under it is unreachable by any
# other test, and a hardcoded `name:` inside is a PAYLOAD, not a collision key.
#
# The check exists for the genuinely unsafe shape, which is a LITERAL label plus
# a fixed name (`CREATE (:Volk {name: 'Junior Volk'})`) -- two concurrent tests
# writing that do collide. Without this, files that scope correctly by label
# reported violations they could not fix except by adding a redundant test_id to
# a property nothing queries, which trains readers to ignore the gate.
INTERPOLATED_LABEL_PATTERN = re.compile(r":\s*`\{[^}]+\}`")
HARDCODED_PORT = re.compile(
r"port\s*=\s*(?:['\"])?(\d{4,5})(?:['\"])?",
re.IGNORECASE,
)
FIXTURE_SCOPE_PATTERN = re.compile(
r"@pytest\.fixture\s*\(\s*scope\s*=\s*['\"](module|class|session)['\"]\s*\)",
re.IGNORECASE,
)
# ``# gate-ok: <reason>`` on the flagged line (or the line directly above it)
# waives that one finding — same convention as scripts/gate_parallel_safety.py,
# for fixture Cypher that is scanned as inert string data (never executed
# against the graph), e.g. the graph_test_preflight.py scanner's own test fixtures.
# The reason is mandatory — ``\S`` refuses a bare tag.
_WAIVER_RE = re.compile(r"#\s*gate-ok:\s*\S")
def _is_waived(lines: list[str], lineno: int) -> bool:
"""True if the 1-based *lineno* (or the line above) carries a gate-ok waiver."""
for idx in (lineno - 1, lineno - 2):
if 0 <= idx < len(lines) and _WAIVER_RE.search(lines[idx]):
return True
return False
# ``# isolation-audit-file-ok: CATEGORY: <reason>`` in a file's first 30 lines
# waives that CATEGORY for the WHOLE file. Reason mandatory. Exists for
# detector-test files whose fixtures are inert STRINGS containing the banned
# patterns (the flagged line sits inside a string literal, so a per-line
# ``gate-ok`` comment physically cannot reach it). Line-level waivers remain
# preferred everywhere a comment can sit next to the finding.
_FILE_WAIVER_RE = re.compile(r"#\s*isolation-audit-file-ok:\s*([A-Z_]+):\s*\S")
# ``# T4-immutable-module-scope: <why>`` directly above the decorator is the
# project's FORMAL opt-in for module scope (AGENTS.md § Test Standards T3/T4).
# `scripts/test-review.py` rule T4 already validates it properly — mandatory
# reason, return-only body, no yield, module scope ONLY.
#
# This auditor did not know the marker, so it re-reported seven fixtures that
# the other gate had already blessed and checked. Two gates disagreeing about
# the same rule is worse than either being absent: the isolation audit can
# never reach zero, so its output stops being read and a REAL violation lands
# unnoticed among the standing noise.
#
# Module scope only — class and session scope stay flagged whatever the comment
# says, matching T4. This is a cross-reference to the owning gate, not a second
# implementation of it: nothing here re-checks return-only-ness.
_T4_IMMUTABLE_RE = re.compile(r"#\s*T4-immutable-module-scope:\s*\S")
def _is_t4_immutable_optin(lines: list[str], lineno: int) -> bool:
"""True if the 1-based *lineno* (or the line above) carries the T4 marker."""
for idx in (lineno - 1, lineno - 2):
if 0 <= idx < len(lines) and _T4_IMMUTABLE_RE.search(lines[idx]):
return True
return False
def _file_waived_categories(lines: list[str]) -> set[str]:
"""Categories waived file-wide via header markers (first 30 lines only)."""
waived: set[str] = set()
for line in lines[:30]:
match = _FILE_WAIVER_RE.search(line)
if match:
waived.add(match.group(1))
return waived
class IsolationViolation:
def __init__(self, category: str, line: int, message: str, fix: str = ""):
self.category = category
self.line = line
self.message = message
self.fix = fix
def __repr__(self):
return f"IsolationViolation({self.category}, line {self.line}, {self.message})"
def extract_string_literals(tree: ast.AST) -> list[tuple[int, int, str]]:
"""Extract all string literals from AST with their line numbers.
Returns the full line-range of each string constant (multi-line strings
like ``"a\\n" "b\\n" "c"`` are reported with their start ``lineno`` and
``end_lineno`` so callers can skip them — tests deliberately embed code
samples inside string literals to feed them to ``_check()``, and those
embedded samples must NOT trigger HARDCODED_NAME / CREATE violations.
"""
strings = []
for node in ast.walk(tree):
if isinstance(node, ast.Constant) and isinstance(node.value, str):
lineno = node.lineno or 0
end_lineno = node.end_lineno or lineno
strings.append((lineno, end_lineno, node.value))
elif isinstance(node, ast.JoinedStr):
# f-strings: try to reconstruct
try:
lineno = node.lineno or 0
end_lineno = node.end_lineno or lineno
strings.append((lineno, end_lineno, ast.unparse(node)))
except Exception:
pass
return strings
def _line_is_inside_string_literal(
string_ranges: list[tuple[int, int, str]], lineno: int
) -> bool:
"""True iff *lineno* falls inside any AST string constant (1-based)."""
for start, end, _value in string_ranges:
if start <= lineno <= end:
return True
return False
def _is_in_docstring(lines: list[str], line_idx: int) -> bool:
"""Check if a line is inside a docstring."""
# Simple heuristic: count triple quotes before this line
triple_count = 0
for i in range(line_idx):
triple_count += lines[i].count('"""') + lines[i].count("'''")
return triple_count % 2 == 1
def check_cypher_queries(
content: str,
lines: list[str],
string_ranges: list[tuple[int, int, str]] | None = None,
) -> list[IsolationViolation]:
"""Check Cypher queries in string literals for isolation issues.
If *string_ranges* is provided (output of :func:`extract_string_literals`,
with each entry's second element being the AST string's ``end_lineno``),
any 1-based ``lineno`` that falls inside one of those string constants is
skipped — tests deliberately embed code samples inside string literals to
feed them to ``_check()``, and those embedded samples must NOT trigger
HARDCODED_NAME / CREATE violations here.
"""
string_ranges = string_ranges or []
violations = []
# Find all multi-line strings that might contain Cypher
for i, line in enumerate(lines, 1):
# Skip comment lines
stripped = line.strip()
if stripped.startswith("#") or _is_in_docstring(lines, i - 1):
continue
# Skip lines that live inside an AST string literal (embedded code
# sample fed to _check() — not real production Cypher).
if _line_is_inside_string_literal(string_ranges, i):
continue
# Check for CREATE statements
if re.search(r"CREATE\s*\(", line, re.IGNORECASE):
# Check if test_id is present in the same query block
context_start = max(0, i - 5)
context_end = min(len(lines), i + 5)
context = "\n".join(lines[context_start:context_end])
has_test_id = TEST_ID_PATTERN.search(context) or (
INTERPOLATED_LABEL_PATTERN.search(line)
)
# Check for hardcoded names
hardcoded = HARDCODED_NAME_PATTERN.findall(line)
for name in hardcoded:
if name.lower() in ("neo4j", "redis", "orchestrata", "project", "test"):
continue # Skip generic names
if not has_test_id and not _is_waived(lines, i):
violations.append(
IsolationViolation(
category="HARDCODED_NAME",
line=i,
message=f"Hardcoded resource name '{name}' without test_id in query",
fix=f"Add test_id to the resource name: '{name}-' + test_id",
)
)
# Check CREATE vs MERGE for File nodes (File.path has uniqueness constraint)
if re.search(r"CREATE\s*\(\s*\w+\s*:\s*File\b", line, re.IGNORECASE):
context_start = max(0, i - 5)
context_end = min(len(lines), i + 5)
context = "\n".join(lines[context_start:context_end])
# Skip if path is parameterized with a variable and test_id is in context
has_param_path = re.search(r"path\s*:\s*\$\w+", line, re.IGNORECASE)
has_test_id = TEST_ID_PATTERN.search(context)
if has_param_path and has_test_id:
# Path is a parameter (e.g. $path, $fp) and test_id is used in query → likely safe
continue
# Check for hardcoded path string on this line
hardcoded_path_match = re.search(
r"path\s*:\s*['\"]([^'\"]+)['\"]", line, re.IGNORECASE
)
if hardcoded_path_match:
hardcoded_path = hardcoded_path_match.group(1)
# If hardcoded path itself contains test_id or tmp_path, it's safe
if re.search(
r"test_id|tmp_path|\{test_id\}", hardcoded_path, re.IGNORECASE
):
continue
if _is_waived(lines, i):
continue
# Hardcoded path without per-test uniqueness → real violation
violations.append(
IsolationViolation(
category="CREATE_VS_MERGE",
line=i,
message=f"CREATE (f:File ...) with hardcoded path '{hardcoded_path}' — File.path has graph uniqueness constraint",
fix="Use a unique path per test: f'/tmp/{{test_id}}/filename.py'",
)
)
elif not has_test_id and not _is_waived(lines, i):
# No hardcoded path and no test_id in context → flag cautiously
violations.append(
IsolationViolation(
category="CREATE_VS_MERGE",
line=i,
message="CREATE (f:File ...) without test_id — File.path has graph uniqueness constraint",
fix="Add test_id filter or use MERGE instead of CREATE",
)
)
return violations
# Whitelist of read-only session fixtures that are safe to share
_READ_ONLY_FIXTURE_PATTERNS = [
re.compile(r"return\s+(?:int\s*\()?\s*os\.environ\["), # redis_host, redis_port
re.compile(r"GraphDatabase\.driver\("), # neo4j_driver
re.compile(r"AsyncGraphDatabase\.driver\("), # neo4j_async_driver
]
# Patterns indicating intentional bad-connection tests (not shared state)
_BAD_CONNECTION_PATTERNS = [
re.compile(r"bad\s+(?:client|redis|connection)", re.IGNORECASE),
re.compile(r"no\s+(?:listener|server)", re.IGNORECASE),
re.compile(r"invalid\s+(?:host|port)", re.IGNORECASE),
re.compile(r"non[-]?reachable", re.IGNORECASE),
re.compile(r"connection\s*(?:error|failure|refused)", re.IGNORECASE),
re.compile(r"pointing\s+at\s+a\s+port", re.IGNORECASE),
]
def check_fixture_scopes(content: str, lines: list[str]) -> list[IsolationViolation]:
"""Check for non-function-scoped fixtures."""
violations = []
for i, line in enumerate(lines, 1):
match = FIXTURE_SCOPE_PATTERN.search(line)
if match:
scope = match.group(1)
# Check if fixture body is read-only (env vars, connection drivers)
# Look ahead for the fixture body
fixture_body = "\n".join(lines[i : min(i + 15, len(lines))])
is_read_only = any(
p.search(fixture_body) for p in _READ_ONLY_FIXTURE_PATTERNS
)
if is_read_only:
continue # Skip read-only fixtures (env vars, connection drivers)
if _is_waived(lines, i):
continue # explicit `# gate-ok: <reason>` waiver
if scope.lower() == "module" and _is_t4_immutable_optin(lines, i):
continue # AGENTS.md T4 opt-in, validated by test-review.py
violations.append(
IsolationViolation(
category="FIXTURE_SCOPE",
line=i,
message=f"Fixture scope='{scope}' — should be 'function' for parallel safety",
fix="Change to @pytest.fixture(scope='function') or omit scope",
)
)
return violations
def check_ast_patterns(tree: ast.AST, lines: list[str]) -> list[IsolationViolation]:
"""Check AST-level patterns for isolation violations."""
violations = []
# OS_ENVIRON_MUTATION lived here until 2026-08-08. It is now
# `violation(env_mutation, ...)` in src/orchestrata_ultimate/prolog/
# gate_rules.pl, which infers from the AST snapshot instead of re-walking
# the tree, honours the same `# gate-ok:` waivers (core.gate_waivers), and
# covers src/ and scripts/ as well as tests/. Deleted only after the
# differential: both detectors over tests/ with waivers ignored found the
# SAME 32 sites — 32 agreed, 0 legacy-only, 0 prolog-only.
for node in ast.walk(tree):
# Check for monkeypatch fixture parameter
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
for arg in node.args.args + node.args.kwonlyargs:
if arg.arg == "monkeypatch":
violations.append(
IsolationViolation(
category="MONKEYPATCH",
line=node.lineno or 0,
message="monkeypatch fixture — forbidden by test standards",
fix="Remove monkeypatch, pass config explicitly",
)
)
# Check for hardcoded ports in function calls
if isinstance(node, ast.Call):
func_name = ""
if isinstance(node.func, ast.Name):
func_name = node.func.id
elif isinstance(node.func, ast.Attribute):
func_name = node.func.attr
for kw in node.keywords:
if kw.arg == "port" and isinstance(kw.value, ast.Constant):
if isinstance(kw.value.value, int) and kw.value.value > 1024:
port_val = kw.value.value
line_idx = (node.lineno or 1) - 1
# Check surrounding context for intentional bad-connection patterns
# Look back 20 lines to capture docstrings/comments above the call
context_start = max(0, line_idx - 20)
context_end = min(len(lines), line_idx + 4)
context = "\n".join(lines[context_start:context_end])
if any(p.search(context) for p in _BAD_CONNECTION_PATTERNS):
continue # Intentional bad connection for error handling tests
# Skip well-known service ports in client connection calls
if port_val in (6379, 7687):
# Redis/graph standard ports — likely intentional client connections
continue
# Skip config/dataclass constructors
if func_name and any(
x in func_name.lower()
for x in ("config", "settings", "options", "params")
):
continue
# Skip factory function tests that verify parameter passthrough
# (tests create clients but never send commands)
if func_name and any(
x in func_name.lower()
for x in (
"create_sync_redis",
"create_async_redis",
"redis",
)
):
if port_val not in (6379, 7687):
# Non-standard port in Redis factory call → likely explicit param test
continue
violations.append(
IsolationViolation(
category="HARDCODED_PORT",
line=node.lineno or 0,
message=f"Hardcoded port {port_val} — use dynamic allocation",
fix="Use socket.bind(('', 0)) for dynamic port allocation",
)
)
# Check for module-level mutable state
if isinstance(node, ast.Assign):
if isinstance(node.targets[0], ast.Name) if node.targets else False:
target = node.targets[0]
if isinstance(target, ast.Name) and target.id.isupper():
# Skip constants (UPPER_CASE is usually a constant)
continue
if isinstance(node.value, (ast.Dict, ast.List, ast.Set)):
# Check if this is at module level (no parent function/class)
# We can't easily check parent from ast.walk, so we'll use line-based heuristic
pass
return violations
def check_module_level_state(
content: str, lines: list[str]
) -> list[IsolationViolation]:
"""Check for module-level mutable state."""
violations = []
in_function = False
indent_level = 0
for i, line in enumerate(lines, 1):
stripped = line.lstrip()
current_indent = len(line) - len(stripped)
if (
stripped.startswith("def ")
or stripped.startswith("async def ")
or stripped.startswith("class ")
):
in_function = True
indent_level = current_indent
if (
in_function
and current_indent <= indent_level
and stripped
and not stripped.startswith("def ")
and not stripped.startswith("async def ")
and not stripped.startswith("class ")
):
in_function = False
if not in_function and current_indent == 0:
# Module level
if re.search(r"^\w+\s*=\s*(\{\}|\[\]|set\(\))", stripped):
violations.append(
IsolationViolation(
category="GLOBAL_MUTABLE_STATE",
line=i,
message="Module-level mutable state — use function-scope fixtures instead",
fix="Move to fixture or use factory function",
)
)
return violations
def check_pytest_patterns(content: str, lines: list[str]) -> list[IsolationViolation]:
"""Check for pytest anti-patterns."""
violations = []
for i, line in enumerate(lines, 1):
stripped = line.strip()
# Skip comments and docstrings
if stripped.startswith("#"):
continue
if _is_in_docstring(lines, i - 1):
continue
# Skip string literals that mention skip in documentation
if (
stripped.startswith('"""')
or stripped.startswith("'''")
or stripped.startswith('"')
or stripped.startswith("'")
):
continue
# Only flag actual decorators or function calls
if re.search(r"@pytest\.mark\.skip\b|pytest\.skip\s*\(", line):
violations.append(
IsolationViolation(
category="PYTEST_SKIP",
line=i,
message="pytest.skip / pytest.mark.skip — every test must run",
fix="Remove skip, fix the underlying issue",
)
)
return violations
def scan_file(py_file: Path) -> list[IsolationViolation]:
"""Scan a single test file for isolation violations."""
violations = []
try:
content = py_file.read_text(encoding="utf-8")
tree = ast.parse(content)
except Exception as e:
return [IsolationViolation("PARSE_ERROR", 0, f"Failed to parse: {e}")]
lines = content.splitlines()
string_ranges = extract_string_literals(tree)
violations.extend(check_cypher_queries(content, lines, string_ranges))
violations.extend(check_fixture_scopes(content, lines))
violations.extend(check_ast_patterns(tree, lines))
violations.extend(check_module_level_state(content, lines))
violations.extend(check_pytest_patterns(content, lines))
file_waived = _file_waived_categories(lines)
if file_waived:
violations = [v for v in violations if v.category not in file_waived]
return violations
def main() -> int:
parser = argparse.ArgumentParser(description="Audit tests for isolation violations")
parser.add_argument(
"paths",
nargs="*",
default=["tests"],
help="Test directories OR individual test files to audit "
"(default: tests). Files are accepted so the pre-commit review "
"battery can audit only the modules a change touched.",
)
parser.add_argument("--fix", action="store_true", help="Generate fix report")
args = parser.parse_args()
# Keep the caller's spelling for the error message: Path("tests/") prints
# as "tests", and echoing back something they did not type is harder to
# match against what they ran.
given = args.paths or ["tests"]
targets = [Path(p) for p in given]
missing = [raw for raw, t in zip(given, targets) if not t.exists()]
if missing:
for raw in missing:
print(f"ERROR: {raw} not found")
return 1
py_files: list[Path] = []
seen: set[Path] = set()
for target in targets:
found = sorted(target.rglob("*.py")) if target.is_dir() else [target]
for path in found:
if path not in seen:
seen.add(path)
py_files.append(path)
all_violations = defaultdict(list)
file_count = 0
total_violations = 0
for py_file in py_files:
if "__pycache__" in str(py_file):
continue
if "_generated_broken" in str(py_file) or "generated_recovery" in str(py_file):
continue
violations = scan_file(py_file)
if violations:
file_count += 1
# Best-effort display path relative to cwd — py_file need not be
# under cwd (e.g. an audited path outside the repo root), so fall
# back to the absolute path rather than crashing with ValueError.
rel_path = py_file
if py_file.is_absolute():
try:
rel_path = py_file.relative_to(Path(".").resolve())
except ValueError:
rel_path = py_file
all_violations[rel_path] = violations
total_violations += len(violations)
# Print report
if total_violations == 0:
print("✅ No isolation violations found!")
return 0
print(f"\n{'=' * 70}")
print("TEST ISOLATION AUDIT REPORT")
print(f"{'=' * 70}")
print(f"Files scanned: {len(py_files)}")
print(f"Files with violations: {file_count}")
print(f"Total violations: {total_violations}")
print(f"{'=' * 70}\n")
# Group by category
category_counts = defaultdict(int)
for violations in all_violations.values():
for v in violations:
category_counts[v.category] += 1
print("VIOLATIONS BY CATEGORY:")
for cat, count in sorted(category_counts.items(), key=lambda x: -x[1]):
print(f" {cat:25s}: {count:4d}")
print()
# Print details
for rel_path, violations in sorted(all_violations.items()):
print(f"\n{rel_path}")
print("-" * len(str(rel_path)))
for v in violations:
print(f" Line {v.line:4d} [{v.category:20s}] {v.message}")
if args.fix and v.fix:
print(f" Fix: {v.fix}")
print(f"\n{'=' * 70}")
print(f"SUMMARY: {file_count} files with {total_violations} total violations")
print(f"{'=' * 70}")
return 1 if total_violations > 0 else 0
if __name__ == "__main__":
sys.exit(main())