run_tests.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. #!/usr/bin/env python3
  2. """
  3. Test runner script that mimics CI/CD pipeline behavior.
  4. This script runs tests with the same configuration as the GitHub Actions workflow,
  5. making it easier to reproduce CI issues locally.
  6. """
  7. import argparse
  8. import subprocess
  9. import sys
  10. from pathlib import Path
  11. def run_command(cmd: list[str], description: str, continue_on_error: bool = False) -> bool:
  12. """
  13. Run a command and handle its output.
  14. Args:
  15. cmd: Command to run as list of strings
  16. description: Description of what the command does
  17. continue_on_error: Whether to continue if command fails
  18. Returns:
  19. True if command succeeded, False otherwise
  20. """
  21. print(f"\n{'='*80}")
  22. print(f"Running: {description}")
  23. print(f"Command: {' '.join(cmd)}")
  24. print(f"{'='*80}\n")
  25. try:
  26. result = subprocess.run(cmd, check=True)
  27. print(f"\n✅ {description} - PASSED\n")
  28. return True
  29. except subprocess.CalledProcessError as e:
  30. print(f"\n❌ {description} - FAILED (exit code: {e.returncode})\n")
  31. if not continue_on_error:
  32. sys.exit(e.returncode)
  33. return False
  34. def main():
  35. """Main entry point for test runner."""
  36. parser = argparse.ArgumentParser(
  37. description="Run tests with CI/CD configuration",
  38. formatter_class=argparse.RawDescriptionHelpFormatter,
  39. epilog="""
  40. Examples:
  41. # Run all tests
  42. python scripts/run_tests.py
  43. # Run only unit tests
  44. python scripts/run_tests.py --unit
  45. # Run with coverage report
  46. python scripts/run_tests.py --coverage
  47. # Run linting only
  48. python scripts/run_tests.py --lint-only
  49. # Run everything (tests + lint + security)
  50. python scripts/run_tests.py --all
  51. """
  52. )
  53. parser.add_argument(
  54. "--unit",
  55. action="store_true",
  56. help="Run only unit tests"
  57. )
  58. parser.add_argument(
  59. "--integration",
  60. action="store_true",
  61. help="Run only integration tests"
  62. )
  63. parser.add_argument(
  64. "--e2e",
  65. action="store_true",
  66. help="Run only end-to-end tests"
  67. )
  68. parser.add_argument(
  69. "--coverage",
  70. action="store_true",
  71. help="Generate coverage report"
  72. )
  73. parser.add_argument(
  74. "--html",
  75. action="store_true",
  76. help="Generate HTML coverage report"
  77. )
  78. parser.add_argument(
  79. "--lint-only",
  80. action="store_true",
  81. help="Run only linting checks"
  82. )
  83. parser.add_argument(
  84. "--security-only",
  85. action="store_true",
  86. help="Run only security checks"
  87. )
  88. parser.add_argument(
  89. "--all",
  90. action="store_true",
  91. help="Run all checks (tests, lint, security)"
  92. )
  93. parser.add_argument(
  94. "--fast",
  95. action="store_true",
  96. help="Skip slow tests"
  97. )
  98. parser.add_argument(
  99. "--parallel",
  100. action="store_true",
  101. help="Run tests in parallel (requires pytest-xdist)"
  102. )
  103. parser.add_argument(
  104. "--verbose",
  105. "-v",
  106. action="store_true",
  107. help="Verbose output"
  108. )
  109. args = parser.parse_args()
  110. # Ensure we're in the project root
  111. project_root = Path(__file__).parent.parent
  112. # Create necessary directories
  113. (project_root / "tests" / "logs").mkdir(parents=True, exist_ok=True)
  114. (project_root / "logs").mkdir(parents=True, exist_ok=True)
  115. results = {
  116. "tests": None,
  117. "lint": None,
  118. "security": None
  119. }
  120. # Determine what to run
  121. run_tests = not (args.lint_only or args.security_only)
  122. run_lint = args.lint_only or args.all
  123. run_security = args.security_only or args.all
  124. # Build test command
  125. if run_tests:
  126. test_cmd = ["pytest"]
  127. # Add verbosity
  128. if args.verbose:
  129. test_cmd.append("-v")
  130. else:
  131. test_cmd.append("-v") # Always verbose in CI mode
  132. # Add parallel execution
  133. if args.parallel:
  134. test_cmd.extend(["-n", "auto"])
  135. # Add coverage
  136. if args.coverage or args.html:
  137. test_cmd.extend(["--cov=src"])
  138. if args.html:
  139. test_cmd.append("--cov-report=html")
  140. test_cmd.append("--cov-report=term")
  141. test_cmd.append("--cov-report=xml")
  142. # Add test selection
  143. if args.unit:
  144. test_cmd.extend(["tests/unit", "-m", "unit"])
  145. elif args.integration:
  146. test_cmd.extend(["tests/integration", "-m", "integration"])
  147. elif args.e2e:
  148. test_cmd.extend(["tests/e2e", "-m", "e2e"])
  149. # Add fast mode
  150. if args.fast:
  151. test_cmd.extend(["-m", "not slow"])
  152. # Run tests
  153. results["tests"] = run_command(
  154. test_cmd,
  155. "Test Suite",
  156. continue_on_error=args.all
  157. )
  158. # Run linting
  159. if run_lint:
  160. print("\n" + "="*80)
  161. print("LINTING CHECKS")
  162. print("="*80 + "\n")
  163. # flake8
  164. lint_results = []
  165. lint_results.append(run_command(
  166. ["flake8", "src", "tests", "--count", "--select=E9,F63,F7,F82",
  167. "--show-source", "--statistics"],
  168. "flake8 - Critical Errors",
  169. continue_on_error=True
  170. ))
  171. lint_results.append(run_command(
  172. ["flake8", "src", "tests", "--count", "--exit-zero",
  173. "--max-complexity=10", "--max-line-length=127", "--statistics"],
  174. "flake8 - Style Warnings",
  175. continue_on_error=True
  176. ))
  177. # black
  178. lint_results.append(run_command(
  179. ["black", "--check", "src", "tests"],
  180. "black - Code Formatting",
  181. continue_on_error=True
  182. ))
  183. # isort
  184. lint_results.append(run_command(
  185. ["isort", "--check-only", "src", "tests"],
  186. "isort - Import Sorting",
  187. continue_on_error=True
  188. ))
  189. # mypy
  190. lint_results.append(run_command(
  191. ["mypy", "src", "--ignore-missing-imports"],
  192. "mypy - Type Checking",
  193. continue_on_error=True
  194. ))
  195. results["lint"] = all(lint_results)
  196. # Run security checks
  197. if run_security:
  198. print("\n" + "="*80)
  199. print("SECURITY CHECKS")
  200. print("="*80 + "\n")
  201. security_results = []
  202. # safety
  203. security_results.append(run_command(
  204. ["safety", "check", "--json"],
  205. "safety - Dependency Vulnerabilities",
  206. continue_on_error=True
  207. ))
  208. # bandit
  209. security_results.append(run_command(
  210. ["bandit", "-r", "src", "-f", "json", "-o", "bandit-report.json"],
  211. "bandit - Security Issues",
  212. continue_on_error=True
  213. ))
  214. results["security"] = all(security_results)
  215. # Print summary
  216. print("\n" + "="*80)
  217. print("SUMMARY")
  218. print("="*80 + "\n")
  219. all_passed = True
  220. if results["tests"] is not None:
  221. status = "✅ PASSED" if results["tests"] else "❌ FAILED"
  222. print(f"Tests: {status}")
  223. all_passed = all_passed and results["tests"]
  224. if results["lint"] is not None:
  225. status = "✅ PASSED" if results["lint"] else "❌ FAILED"
  226. print(f"Lint: {status}")
  227. all_passed = all_passed and results["lint"]
  228. if results["security"] is not None:
  229. status = "✅ PASSED" if results["security"] else "❌ FAILED"
  230. print(f"Security: {status}")
  231. all_passed = all_passed and results["security"]
  232. print("\n" + "="*80)
  233. if args.coverage or args.html:
  234. print("\n📊 Coverage report generated:")
  235. if args.html:
  236. print(f" HTML: {project_root}/htmlcov/index.html")
  237. print(f" XML: {project_root}/coverage.xml")
  238. if all_passed:
  239. print("\n🎉 All checks passed!")
  240. sys.exit(0)
  241. else:
  242. print("\n💥 Some checks failed!")
  243. sys.exit(1)
  244. if __name__ == "__main__":
  245. main()