Coverage for src / ida_pro_mcp / test.py: 6%
80 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-03-07 00:26 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-03-07 00:26 +0000
1"""Standalone test runner for IDA Pro MCP using idalib.
3Usage:
4 ida-mcp-test tests/crackme03.elf
5 ida-mcp-test tests/crackme03.elf --category api_core
6 ida-mcp-test tests/crackme03.elf --pattern "*meta*"
8With coverage:
9 uv run coverage run -m ida_pro_mcp.test crackme03.elf
10 uv run coverage report
11 uv run coverage html
12"""
14import os
15import sys
16import argparse
17from pathlib import Path
19# idapro must go first to initialize idalib
20import idapro
21import ida_auto
24def main() -> int:
25 """Entry point for ida-mcp-test command."""
26 parser = argparse.ArgumentParser(
27 description="Run IDA Pro MCP tests using idalib",
28 formatter_class=argparse.RawDescriptionHelpFormatter,
29 epilog="""
30Examples:
31 ida-mcp-test tests/crackme03.elf
32 ida-mcp-test tests/crackme03.elf --category api_core
33 ida-mcp-test tests/crackme03.elf --pattern "*meta*"
34 ida-mcp-test tests/crackme03.elf --stop-on-failure
36With coverage:
37 uv run coverage run -m ida_pro_mcp.test crackme03.elf
38 uv run coverage report --show-missing
39 uv run coverage html && open htmlcov/index.html
40 """,
41 )
42 parser.add_argument("binary", type=Path, help="Path to binary file to analyze")
43 parser.add_argument(
44 "--pattern",
45 "-p",
46 default="*",
47 help="Glob pattern to filter test names (default: *)",
48 )
49 parser.add_argument(
50 "--category",
51 "-c",
52 default="*",
53 help="Filter by module category (default: *)",
54 )
55 parser.add_argument(
56 "--stop-on-failure",
57 "-x",
58 action="store_true",
59 help="Stop at first failure",
60 )
61 parser.add_argument(
62 "--quiet",
63 "-q",
64 action="store_true",
65 help="Quiet mode - only show summary",
66 )
67 parser.add_argument(
68 "--list",
69 "-l",
70 action="store_true",
71 help="List available tests without running them",
72 )
73 parser.add_argument(
74 "--verbose",
75 "-v",
76 action="store_true",
77 help="Show IDA console messages",
78 )
79 args = parser.parse_args()
81 # Check binary exists
82 if not args.binary.exists():
83 print(f"Error: Binary not found: {args.binary}", file=sys.stderr)
84 return 1
86 # Configure IDA console output
87 if args.verbose:
88 idapro.enable_console_messages(True)
89 else:
90 idapro.enable_console_messages(False)
92 # Open database
93 print(f"Opening database for: {args.binary}")
94 if idapro.open_database(str(args.binary), run_auto_analysis=True):
95 print("Error: Failed to open database", file=sys.stderr)
96 return 1
98 # Wait for auto-analysis
99 print("Waiting for auto-analysis...")
100 ida_auto.auto_wait()
101 print()
103 try:
104 # Import test framework AFTER idalib is initialized
105 from ida_pro_mcp.ida_mcp.framework import run_tests, TESTS
107 # Import all test modules to register @test decorators.
108 # Use pkgutil discovery so registration works even if tests package
109 # __init__ does not eagerly import submodules.
110 import importlib
111 import pkgutil
113 tests_pkg_name = "ida_pro_mcp.ida_mcp.tests"
114 tests_pkg = importlib.import_module(tests_pkg_name)
115 if hasattr(tests_pkg, "__path__"):
116 for mod in pkgutil.iter_modules(tests_pkg.__path__):
117 if mod.name.startswith("test_"):
118 importlib.import_module(f"{tests_pkg_name}.{mod.name}")
120 if not TESTS:
121 print(
122 "Warning: no tests were registered from ida_pro_mcp.ida_mcp.tests",
123 file=sys.stderr,
124 )
126 # Handle --list
127 if args.list:
128 print("Available tests:")
129 by_category: dict[str, list[str]] = {}
130 for name, info in sorted(TESTS.items()):
131 if info.module not in by_category:
132 by_category[info.module] = []
133 by_category[info.module].append(name)
135 for cat in sorted(by_category.keys()):
136 print(f"\n[{cat}]")
137 for name in by_category[cat]:
138 info = TESTS[name]
139 skip_marker = " (skip)" if info.skip else ""
140 print(f" {name}{skip_marker}")
141 return 0
143 # Run tests
144 in_ci = os.environ.get("CI", "").lower() not in ("", "0", "false", "no")
145 interactive_output = sys.stdout.isatty()
146 show_all_test_output = (not args.quiet) and (interactive_output or in_ci)
147 results = run_tests(
148 pattern=args.pattern,
149 category=args.category,
150 verbose=show_all_test_output,
151 stop_on_failure=args.stop_on_failure,
152 failures_only=(not args.quiet) and not show_all_test_output,
153 )
155 # No matched tests is likely a configuration/test-selection mistake
156 if not results.results:
157 print(
158 "Error: no tests matched the requested pattern/category",
159 file=sys.stderr,
160 )
161 return 1
163 # In quiet mode, print summary
164 if args.quiet:
165 status_parts = []
166 if results.passed:
167 status_parts.append(f"{results.passed} passed")
168 if results.failed:
169 status_parts.append(f"{results.failed} failed")
170 if results.skipped:
171 status_parts.append(f"{results.skipped} skipped")
172 print(f"Results: {', '.join(status_parts)} ({results.total_time:.2f}s)")
174 if results.failed:
175 print("\nFailed tests:")
176 for r in results.results:
177 if r.status == "failed":
178 print(f" {r.name}: {r.error}")
180 return 1 if results.failed > 0 else 0
182 finally:
183 # Close database
184 idapro.close_database()
187if __name__ == "__main__":
188 sys.exit(main())