polyglot-code-guard 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Furox-Art
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,105 @@
1
+ Metadata-Version: 2.4
2
+ Name: polyglot-code-guard
3
+ Version: 0.1.0
4
+ Summary: Fail-closed, multi-language test verification for AI-generated code.
5
+ Author: Furox-Art
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Furox-Art/polyglot-code-guard
8
+ Project-URL: Repository, https://github.com/Furox-Art/polyglot-code-guard
9
+ Project-URL: Issues, https://github.com/Furox-Art/polyglot-code-guard/issues
10
+ Keywords: testing,code-verification,polyglot,ai-generated-code,quality-gate
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Software Development :: Testing
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Provides-Extra: test
20
+ Requires-Dist: pytest>=8.0; extra == "test"
21
+ Dynamic: license-file
22
+
23
+ # Polyguard
24
+
25
+ **Fail-closed verification for AI-generated code across multiple programming languages.**
26
+
27
+ Polyguard runs the real build and test tools for the languages it detects. It returns **PASS** only when every detected language has a recognizable test suite and all configured commands succeed. Missing tests, missing toolchains, timeouts, compilation errors, and failing tests return **FAIL**.
28
+
29
+ ## Install
30
+
31
+ python -m pip install polyglot-code-guard
32
+
33
+ ## Use
34
+
35
+ Run this inside the project directory:
36
+
37
+ polyguard verify
38
+
39
+ The normal verdict is intentionally one word:
40
+
41
+ PASS
42
+
43
+ or:
44
+
45
+ FAIL
46
+
47
+ For diagnostics:
48
+
49
+ polyguard verify --details
50
+
51
+ For automation:
52
+
53
+ polyguard verify --json
54
+
55
+ The process exits with code 0 for PASS and code 1 for FAIL.
56
+
57
+ ## Initial language support
58
+
59
+ | Language | Test command |
60
+ | --- | --- |
61
+ | Python | compileall, then pytest |
62
+ | Rust | cargo test --all-targets |
63
+ | Go | go test ./... |
64
+ | JavaScript / TypeScript | npm test; TypeScript projects also run tsc --noEmit |
65
+ | Java | Maven test or Gradle test |
66
+ | C# | dotnet test |
67
+ | PHP | PHPUnit |
68
+ | Ruby | RSpec or Rake test |
69
+ | Swift | swift test |
70
+ | C / C++ | CMake build and CTest when CTest is configured |
71
+
72
+ The project must contain a detectable test suite for each detected language. Polyguard deliberately does not treat the absence of tests as a successful result.
73
+
74
+ ## AI coding-agent skill
75
+
76
+ The repository includes SKILL.md with the mandatory agent workflow and output contract. The same skill instructions are included in the installed Python package as package data.
77
+
78
+ ## Important limitations
79
+
80
+ Polyguard is a test-execution gate, not a proof of program correctness. The quality of a PASS depends on the requirements and coverage of the tests. It does not generate comprehensive tests automatically in this first release.
81
+
82
+ The verifier executes project build scripts and tests. Do not run untrusted code on a machine containing secrets or sensitive files. Use a disposable container or virtual machine with restricted permissions and no privileged host mounts.
83
+
84
+ Language detection and test-runner conventions vary across ecosystems. This alpha release uses conservative heuristics; unsupported project layouts may require explicit configuration in a future release.
85
+
86
+ ## Development
87
+
88
+ python -m pip install -e ".[test]"
89
+ python -m pytest -q
90
+ python -m build
91
+ python -m twine check dist/*
92
+
93
+ ## Publishing
94
+
95
+ The publish workflow uses PyPI Trusted Publishing. Configure a pending/normal publisher in PyPI for:
96
+ - Owner: Furox-Art
97
+ - Repository: polyglot-code-guard
98
+ - Workflow: publish.yml
99
+ - Environment: pypi
100
+
101
+ Then create a GitHub release to publish. No long-lived PyPI API token is stored in the repository.
102
+
103
+ ## License
104
+
105
+ MIT
@@ -0,0 +1,83 @@
1
+ # Polyguard
2
+
3
+ **Fail-closed verification for AI-generated code across multiple programming languages.**
4
+
5
+ Polyguard runs the real build and test tools for the languages it detects. It returns **PASS** only when every detected language has a recognizable test suite and all configured commands succeed. Missing tests, missing toolchains, timeouts, compilation errors, and failing tests return **FAIL**.
6
+
7
+ ## Install
8
+
9
+ python -m pip install polyglot-code-guard
10
+
11
+ ## Use
12
+
13
+ Run this inside the project directory:
14
+
15
+ polyguard verify
16
+
17
+ The normal verdict is intentionally one word:
18
+
19
+ PASS
20
+
21
+ or:
22
+
23
+ FAIL
24
+
25
+ For diagnostics:
26
+
27
+ polyguard verify --details
28
+
29
+ For automation:
30
+
31
+ polyguard verify --json
32
+
33
+ The process exits with code 0 for PASS and code 1 for FAIL.
34
+
35
+ ## Initial language support
36
+
37
+ | Language | Test command |
38
+ | --- | --- |
39
+ | Python | compileall, then pytest |
40
+ | Rust | cargo test --all-targets |
41
+ | Go | go test ./... |
42
+ | JavaScript / TypeScript | npm test; TypeScript projects also run tsc --noEmit |
43
+ | Java | Maven test or Gradle test |
44
+ | C# | dotnet test |
45
+ | PHP | PHPUnit |
46
+ | Ruby | RSpec or Rake test |
47
+ | Swift | swift test |
48
+ | C / C++ | CMake build and CTest when CTest is configured |
49
+
50
+ The project must contain a detectable test suite for each detected language. Polyguard deliberately does not treat the absence of tests as a successful result.
51
+
52
+ ## AI coding-agent skill
53
+
54
+ The repository includes SKILL.md with the mandatory agent workflow and output contract. The same skill instructions are included in the installed Python package as package data.
55
+
56
+ ## Important limitations
57
+
58
+ Polyguard is a test-execution gate, not a proof of program correctness. The quality of a PASS depends on the requirements and coverage of the tests. It does not generate comprehensive tests automatically in this first release.
59
+
60
+ The verifier executes project build scripts and tests. Do not run untrusted code on a machine containing secrets or sensitive files. Use a disposable container or virtual machine with restricted permissions and no privileged host mounts.
61
+
62
+ Language detection and test-runner conventions vary across ecosystems. This alpha release uses conservative heuristics; unsupported project layouts may require explicit configuration in a future release.
63
+
64
+ ## Development
65
+
66
+ python -m pip install -e ".[test]"
67
+ python -m pytest -q
68
+ python -m build
69
+ python -m twine check dist/*
70
+
71
+ ## Publishing
72
+
73
+ The publish workflow uses PyPI Trusted Publishing. Configure a pending/normal publisher in PyPI for:
74
+ - Owner: Furox-Art
75
+ - Repository: polyglot-code-guard
76
+ - Workflow: publish.yml
77
+ - Environment: pypi
78
+
79
+ Then create a GitHub release to publish. No long-lived PyPI API token is stored in the repository.
80
+
81
+ ## License
82
+
83
+ MIT
@@ -0,0 +1,46 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "polyglot-code-guard"
7
+ version = "0.1.0"
8
+ description = "Fail-closed, multi-language test verification for AI-generated code."
9
+ readme = "README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ authors = [{ name = "Furox-Art" }]
13
+ keywords = ["testing", "code-verification", "polyglot", "ai-generated-code", "quality-gate"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "Operating System :: OS Independent",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: Software Development :: Testing",
20
+ ]
21
+ dependencies = []
22
+
23
+ [project.optional-dependencies]
24
+ test = ["pytest>=8.0"]
25
+
26
+ [project.scripts]
27
+ polyguard = "polyguard.cli:main"
28
+
29
+ [project.urls]
30
+ Homepage = "https://github.com/Furox-Art/polyglot-code-guard"
31
+ Repository = "https://github.com/Furox-Art/polyglot-code-guard"
32
+ Issues = "https://github.com/Furox-Art/polyglot-code-guard/issues"
33
+
34
+ [tool.setuptools]
35
+ package-dir = {"" = "src"}
36
+ include-package-data = true
37
+
38
+ [tool.setuptools.packages.find]
39
+ where = ["src"]
40
+
41
+ [tool.setuptools.package-data]
42
+ polyguard = ["SKILL.md"]
43
+
44
+ [tool.pytest.ini_options]
45
+ testpaths = ["tests"]
46
+ addopts = "-ra"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,105 @@
1
+ Metadata-Version: 2.4
2
+ Name: polyglot-code-guard
3
+ Version: 0.1.0
4
+ Summary: Fail-closed, multi-language test verification for AI-generated code.
5
+ Author: Furox-Art
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/Furox-Art/polyglot-code-guard
8
+ Project-URL: Repository, https://github.com/Furox-Art/polyglot-code-guard
9
+ Project-URL: Issues, https://github.com/Furox-Art/polyglot-code-guard/issues
10
+ Keywords: testing,code-verification,polyglot,ai-generated-code,quality-gate
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Topic :: Software Development :: Testing
16
+ Requires-Python: >=3.10
17
+ Description-Content-Type: text/markdown
18
+ License-File: LICENSE
19
+ Provides-Extra: test
20
+ Requires-Dist: pytest>=8.0; extra == "test"
21
+ Dynamic: license-file
22
+
23
+ # Polyguard
24
+
25
+ **Fail-closed verification for AI-generated code across multiple programming languages.**
26
+
27
+ Polyguard runs the real build and test tools for the languages it detects. It returns **PASS** only when every detected language has a recognizable test suite and all configured commands succeed. Missing tests, missing toolchains, timeouts, compilation errors, and failing tests return **FAIL**.
28
+
29
+ ## Install
30
+
31
+ python -m pip install polyglot-code-guard
32
+
33
+ ## Use
34
+
35
+ Run this inside the project directory:
36
+
37
+ polyguard verify
38
+
39
+ The normal verdict is intentionally one word:
40
+
41
+ PASS
42
+
43
+ or:
44
+
45
+ FAIL
46
+
47
+ For diagnostics:
48
+
49
+ polyguard verify --details
50
+
51
+ For automation:
52
+
53
+ polyguard verify --json
54
+
55
+ The process exits with code 0 for PASS and code 1 for FAIL.
56
+
57
+ ## Initial language support
58
+
59
+ | Language | Test command |
60
+ | --- | --- |
61
+ | Python | compileall, then pytest |
62
+ | Rust | cargo test --all-targets |
63
+ | Go | go test ./... |
64
+ | JavaScript / TypeScript | npm test; TypeScript projects also run tsc --noEmit |
65
+ | Java | Maven test or Gradle test |
66
+ | C# | dotnet test |
67
+ | PHP | PHPUnit |
68
+ | Ruby | RSpec or Rake test |
69
+ | Swift | swift test |
70
+ | C / C++ | CMake build and CTest when CTest is configured |
71
+
72
+ The project must contain a detectable test suite for each detected language. Polyguard deliberately does not treat the absence of tests as a successful result.
73
+
74
+ ## AI coding-agent skill
75
+
76
+ The repository includes SKILL.md with the mandatory agent workflow and output contract. The same skill instructions are included in the installed Python package as package data.
77
+
78
+ ## Important limitations
79
+
80
+ Polyguard is a test-execution gate, not a proof of program correctness. The quality of a PASS depends on the requirements and coverage of the tests. It does not generate comprehensive tests automatically in this first release.
81
+
82
+ The verifier executes project build scripts and tests. Do not run untrusted code on a machine containing secrets or sensitive files. Use a disposable container or virtual machine with restricted permissions and no privileged host mounts.
83
+
84
+ Language detection and test-runner conventions vary across ecosystems. This alpha release uses conservative heuristics; unsupported project layouts may require explicit configuration in a future release.
85
+
86
+ ## Development
87
+
88
+ python -m pip install -e ".[test]"
89
+ python -m pytest -q
90
+ python -m build
91
+ python -m twine check dist/*
92
+
93
+ ## Publishing
94
+
95
+ The publish workflow uses PyPI Trusted Publishing. Configure a pending/normal publisher in PyPI for:
96
+ - Owner: Furox-Art
97
+ - Repository: polyglot-code-guard
98
+ - Workflow: publish.yml
99
+ - Environment: pypi
100
+
101
+ Then create a GitHub release to publish. No long-lived PyPI API token is stored in the repository.
102
+
103
+ ## License
104
+
105
+ MIT
@@ -0,0 +1,14 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/polyglot_code_guard.egg-info/PKG-INFO
5
+ src/polyglot_code_guard.egg-info/SOURCES.txt
6
+ src/polyglot_code_guard.egg-info/dependency_links.txt
7
+ src/polyglot_code_guard.egg-info/entry_points.txt
8
+ src/polyglot_code_guard.egg-info/requires.txt
9
+ src/polyglot_code_guard.egg-info/top_level.txt
10
+ src/polyguard/SKILL.md
11
+ src/polyguard/__init__.py
12
+ src/polyguard/cli.py
13
+ src/polyguard/runner.py
14
+ tests/test_runner.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ polyguard = polyguard.cli:main
@@ -0,0 +1,35 @@
1
+ ---
2
+ name: polyglot-code-verification
3
+ description: Verify AI-generated code with real language test runners and fail closed when tests are missing or fail.
4
+ ---
5
+
6
+ # Polyglot Code Verification
7
+
8
+ Use this skill whenever code is generated, changed, refactored, or proposed as complete.
9
+
10
+ ## Mandatory gate
11
+
12
+ 1. Identify the project root and the languages present.
13
+ 2. Ensure the implementation matches the user's stated requirements.
14
+ 3. Run the verifier from the project root: polyguard verify --path . --details.
15
+ 4. Do not claim that code works unless the verifier exits with status code 0 and prints PASS.
16
+ 5. If any required test fails, the final verdict must be exactly FAIL. Do not label untested code as successful.
17
+ 6. Missing test suites, missing language toolchains, timeouts, compilation errors, and test-runner errors are failures under the default fail-closed policy.
18
+ 7. Inspect the diagnostic report, fix the implementation or tests, and rerun the complete verification suite.
19
+ 8. Never weaken, delete, skip, or rewrite a test merely to obtain PASS. Explain any necessary change to test expectations.
20
+ 9. Preserve the test output and command details when the user asks for evidence.
21
+
22
+ ## Output contract
23
+
24
+ - Successful verification: PASS
25
+ - Failed or incomplete verification: FAIL
26
+
27
+ Use --json when another tool or CI system needs the structured report. Use --details when debugging.
28
+
29
+ ## Security
30
+
31
+ The verifier executes project build and test commands. Treat source code, build scripts, dependencies, and tests as untrusted. Run untrusted projects in a disposable container or isolated virtual machine without secrets, privileged access, or sensitive host mounts. Polyguard is a quality gate, not a security sandbox.
32
+
33
+ ## Scope
34
+
35
+ The initial runner recognizes Python, Rust, Go, JavaScript/TypeScript, Java, C#, PHP, Ruby, Swift, and C/C++ CMake projects. A language is not marked PASS unless a test suite is detected and every configured command succeeds.
@@ -0,0 +1,3 @@
1
+ """Polyguard: fail-closed verification for multi-language projects."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,34 @@
1
+ """Command-line interface for Polyguard."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ from pathlib import Path
7
+
8
+ from .runner import render_report, run_checks
9
+
10
+
11
+ def build_parser() -> argparse.ArgumentParser:
12
+ parser = argparse.ArgumentParser(
13
+ prog="polyguard",
14
+ description="Run available language test suites and fail closed when proof is missing.",
15
+ )
16
+ parser.add_argument("verify", nargs="?", choices=["verify"], default="verify")
17
+ parser.add_argument("--path", default=".", help="Project directory to verify.")
18
+ parser.add_argument("--timeout", type=int, default=120, help="Timeout per command in seconds.")
19
+ parser.add_argument("--details", action="store_true", help="Show per-language diagnostics.")
20
+ parser.add_argument("--json", action="store_true", help="Emit a machine-readable JSON report.")
21
+ return parser
22
+
23
+
24
+ def main() -> int:
25
+ args = build_parser().parse_args()
26
+ if args.timeout < 1:
27
+ raise SystemExit("--timeout must be at least 1 second.")
28
+ report = run_checks(Path(args.path), timeout=args.timeout)
29
+ print(render_report(report, details=args.details, as_json=args.json))
30
+ return 0 if report["status"] == "PASS" else 1
31
+
32
+
33
+ if __name__ == "__main__":
34
+ raise SystemExit(main())
@@ -0,0 +1,271 @@
1
+ """Language discovery and fail-closed test execution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import re
7
+ import shutil
8
+ import subprocess
9
+ import sys
10
+ import time
11
+ from dataclasses import asdict, dataclass
12
+ from pathlib import Path
13
+ from typing import Iterable
14
+
15
+
16
+ IGNORED_DIRS = {
17
+ ".git", ".hg", ".svn", ".venv", "venv", "env", "node_modules",
18
+ "__pycache__", ".mypy_cache", ".pytest_cache", ".tox", "dist",
19
+ "build", ".polyguard-build", "target", ".next", ".nuxt",
20
+ }
21
+
22
+
23
+ @dataclass
24
+ class CheckResult:
25
+ language: str
26
+ status: str
27
+ commands: list[list[str]]
28
+ exit_code: int | None
29
+ elapsed_seconds: float
30
+ reason: str = ""
31
+ output: str = ""
32
+
33
+
34
+ def _project_files(root: Path) -> list[Path]:
35
+ found: list[Path] = []
36
+ for path in root.rglob("*"):
37
+ if any(part in IGNORED_DIRS for part in path.parts):
38
+ continue
39
+ if path.is_file():
40
+ found.append(path)
41
+ return found
42
+
43
+
44
+ def _in_test_tree(path: Path) -> bool:
45
+ return any(part.lower() in {"test", "tests", "spec", "specs"} for part in path.parts)
46
+
47
+
48
+ def _has_test(files: Iterable[Path], language: str) -> bool:
49
+ """Require a recognizable test declaration, not merely a file named like a test."""
50
+ all_files = list(files)
51
+ for path in all_files:
52
+ name = path.name.lower()
53
+ suffix = path.suffix.lower()
54
+ content = _safe_read(path)
55
+
56
+ if language == "python" and suffix == ".py":
57
+ is_test_file = _in_test_tree(path) or name.startswith("test_") or name.endswith("_test.py")
58
+ if is_test_file and re.search(r"\b(?:def\s+test_\w+\s*\(|class\s+Test\w*)", content):
59
+ return True
60
+ elif language == "rust" and suffix == ".rs":
61
+ if re.search(r"#\s*\[\s*(?:test|tokio::test|async_std::test)\s*\]", content):
62
+ return True
63
+ elif language == "javascript" and suffix in {".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"}:
64
+ is_test_file = _in_test_tree(path) or re.search(r"\.(test|spec)\.", name)
65
+ if is_test_file and re.search(r"\b(?:test|it|specify)\s*(?:\.\w+)?\s*\(", content):
66
+ return True
67
+ elif language == "go" and name.endswith("_test.go"):
68
+ if re.search(r"\bfunc\s+Test[A-Z]\w*\s*\(", content):
69
+ return True
70
+ elif language == "java" and suffix == ".java":
71
+ is_test_file = _in_test_tree(path) or name.endswith(("test.java", "tests.java"))
72
+ if is_test_file and re.search(r"@(?:Test|ParameterizedTest|RepeatedTest|TestFactory|TestTemplate)\b", content):
73
+ return True
74
+ elif language == "csharp" and suffix == ".cs":
75
+ if re.search(r"\[(?:Fact|Theory|Test|TestCase|TestMethod)\b", content):
76
+ return True
77
+ elif language == "php" and suffix == ".php":
78
+ is_test_file = _in_test_tree(path) or name.startswith("test")
79
+ if is_test_file and re.search(r"(?:function\s+test\w*\s*\(|#\[Test\])", content, re.I):
80
+ return True
81
+ elif language == "ruby" and suffix == ".rb":
82
+ is_test_file = _in_test_tree(path) or name.startswith("test_") or name.endswith("_spec.rb")
83
+ if is_test_file and re.search(r"\b(?:it|specify|example)\s+['\"].+?\bdo\b|\bdef\s+test_\w+", content, re.S):
84
+ return True
85
+ elif language == "swift" and suffix == ".swift":
86
+ if _in_test_tree(path) and re.search(r"(?:\bfunc\s+test\w*\s*\(|@Test\b)", content):
87
+ return True
88
+ elif language == "cpp" and suffix in {".c", ".cc", ".cpp", ".cxx", ".h", ".hpp"}:
89
+ if (_in_test_tree(path) or "test" in name) and re.search(r"\b(?:TEST|TEST_F|TEST_P|TEST_CASE)\s*\(", content):
90
+ cmake_files = [p for p in all_files if p.name.lower() == "cmakelists.txt"]
91
+ cmake_text = "\n".join(_safe_read(p) for p in cmake_files)
92
+ enabled = re.search(r"\b(?:enable_testing)\s*\(|\binclude\s*\(\s*CTest\b", cmake_text, re.I)
93
+ registered = re.search(r"\b(?:add_test|gtest_discover_tests|catch_discover_tests)\s*\(", cmake_text)
94
+ if enabled and registered:
95
+ return True
96
+ return False
97
+
98
+
99
+ def _safe_read(path: Path) -> str:
100
+ try:
101
+ return path.read_text(encoding="utf-8", errors="ignore")[:1_000_000]
102
+ except OSError:
103
+ return ""
104
+
105
+
106
+ def discover(root: Path) -> list[tuple[str, list[list[str]]]]:
107
+ """Return supported language plans. A detected language without tests is still a failure."""
108
+ files = _project_files(root)
109
+ names = {p.name for p in files}
110
+ plans: list[tuple[str, list[list[str]]]] = []
111
+
112
+ py_files = [p for p in files if p.suffix == ".py"]
113
+ if py_files:
114
+ plans.append(("python", [
115
+ [sys.executable, "-m", "compileall", "-q", "."],
116
+ [sys.executable, "-m", "pytest", "-q"],
117
+ ]))
118
+
119
+ if "Cargo.toml" in names or any(p.suffix == ".rs" for p in files):
120
+ plans.append(("rust", [["cargo", "test", "--all-targets"]]))
121
+
122
+ if "go.mod" in names:
123
+ plans.append(("go", [["go", "test", "./..."]]))
124
+
125
+ js_project = "package.json" in names and any(
126
+ p.suffix.lower() in {".js", ".jsx", ".mjs", ".cjs", ".ts", ".tsx"} for p in files
127
+ )
128
+ if js_project:
129
+ js_commands = [["npm", "test"]]
130
+ if "tsconfig.json" in names:
131
+ js_commands.append(["npx", "--no-install", "tsc", "--noEmit"])
132
+ plans.append(("javascript", js_commands))
133
+
134
+ java_build = names.intersection({"pom.xml", "build.gradle", "build.gradle.kts", "mvnw", "gradlew"})
135
+ if java_build:
136
+ if "pom.xml" in names:
137
+ cmd = ["./mvnw", "test"] if "mvnw" in names else ["mvn", "test"]
138
+ else:
139
+ cmd = ["./gradlew", "test"] if "gradlew" in names else ["gradle", "test"]
140
+ plans.append(("java", [cmd]))
141
+
142
+ cs_projects = [p for p in files if p.suffix.lower() in {".csproj", ".sln"}]
143
+ if cs_projects:
144
+ plans.append(("csharp", [["dotnet", "test", "--no-restore"]]))
145
+
146
+ if "composer.json" in names:
147
+ phpunit = root / "vendor" / "bin" / "phpunit"
148
+ plans.append(("php", [[str(phpunit)] if phpunit.exists() else ["phpunit"]]))
149
+
150
+ if "Gemfile" in names or any(p.suffix == ".gemspec" for p in files):
151
+ if any(p.parts[-2:-1] == ("spec",) for p in files):
152
+ plans.append(("ruby", [["bundle", "exec", "rspec"]]))
153
+ else:
154
+ plans.append(("ruby", [["bundle", "exec", "rake", "test"]]))
155
+
156
+ if "Package.swift" in names:
157
+ plans.append(("swift", [["swift", "test"]]))
158
+
159
+ if "CMakeLists.txt" in names:
160
+ cmake_text = _safe_read(root / "CMakeLists.txt")
161
+ if re.search(r"enable_testing\s*\(|include\s*\(\s*CTest", cmake_text, re.I):
162
+ plans.append(("cpp", [
163
+ ["cmake", "-S", ".", "-B", ".polyguard-build", "-DBUILD_TESTING=ON"],
164
+ ["cmake", "--build", ".polyguard-build", "--parallel"],
165
+ ["ctest", "--test-dir", ".polyguard-build", "--output-on-failure"],
166
+ ]))
167
+ else:
168
+ plans.append(("cpp", [["cmake", "-S", ".", "-B", ".polyguard-build"]]))
169
+
170
+ return plans
171
+
172
+
173
+ def run_checks(root: Path, timeout: int = 120) -> dict:
174
+ root = root.resolve()
175
+ if not root.is_dir():
176
+ return {
177
+ "status": "FAIL",
178
+ "results": [asdict(CheckResult("project", "FAIL", [], 2, 0.0, "Project directory does not exist."))],
179
+ }
180
+
181
+ plans = discover(root)
182
+ if not plans:
183
+ return {
184
+ "status": "FAIL",
185
+ "results": [asdict(CheckResult("project", "FAIL", [], 2, 0.0, "No supported language project was detected."))],
186
+ }
187
+
188
+ results: list[CheckResult] = []
189
+ files = _project_files(root)
190
+ for language, commands in plans:
191
+ if not _has_test(files, language):
192
+ results.append(CheckResult(
193
+ language, "FAIL", commands, 2, 0.0,
194
+ "No test suite was detected; fail-closed policy rejects untested code.",
195
+ ))
196
+ continue
197
+
198
+ started = time.monotonic()
199
+ last_code: int | None = 0
200
+ output_parts: list[str] = []
201
+ failure_reason = ""
202
+ for command in commands:
203
+ executable = command[0]
204
+ if executable.startswith("./"):
205
+ executable_path = root / executable[2:]
206
+ if not executable_path.exists():
207
+ last_code = 127
208
+ failure_reason = f"Required test runner not found: {executable}"
209
+ break
210
+ elif not Path(executable).is_absolute() and shutil.which(executable) is None:
211
+ last_code = 127
212
+ failure_reason = f"Required tool not installed: {executable}"
213
+ break
214
+
215
+ try:
216
+ completed = subprocess.run(
217
+ command,
218
+ cwd=root,
219
+ text=True,
220
+ stdout=subprocess.PIPE,
221
+ stderr=subprocess.STDOUT,
222
+ timeout=timeout,
223
+ check=False,
224
+ )
225
+ last_code = completed.returncode
226
+ if completed.stdout:
227
+ output_parts.append(completed.stdout[-12000:])
228
+ if last_code != 0:
229
+ failure_reason = f"Command failed with exit code {last_code}: {' '.join(command)}"
230
+ break
231
+ except subprocess.TimeoutExpired as exc:
232
+ last_code = 124
233
+ captured = exc.stdout or ""
234
+ if isinstance(captured, bytes):
235
+ captured = captured.decode("utf-8", errors="replace")
236
+ output_parts.append(str(captured)[-12000:])
237
+ failure_reason = f"Command timed out after {timeout} seconds: {' '.join(command)}"
238
+ break
239
+ except OSError as exc:
240
+ last_code = 126
241
+ failure_reason = f"Could not execute {' '.join(command)}: {exc}"
242
+
243
+ elapsed = time.monotonic() - started
244
+ passed = last_code == 0 and not failure_reason
245
+ results.append(CheckResult(
246
+ language,
247
+ "PASS" if passed else "FAIL",
248
+ commands,
249
+ last_code,
250
+ round(elapsed, 3),
251
+ failure_reason,
252
+ "\n".join(output_parts)[-12000:],
253
+ ))
254
+
255
+ status = "PASS" if results and all(item.status == "PASS" for item in results) else "FAIL"
256
+ return {"status": status, "root": str(root), "results": [asdict(item) for item in results]}
257
+
258
+
259
+ def render_report(report: dict, details: bool = False, as_json: bool = False) -> str:
260
+ if as_json:
261
+ return json.dumps(report, indent=2, ensure_ascii=False)
262
+ if not details:
263
+ return report["status"]
264
+ lines = [report["status"]]
265
+ for item in report["results"]:
266
+ lines.append(f"{item['language']}: {item['status']}")
267
+ if item["reason"]:
268
+ lines.append(f" {item['reason']}")
269
+ if item["output"]:
270
+ lines.append(item["output"])
271
+ return "\n".join(lines)
@@ -0,0 +1,74 @@
1
+ from pathlib import Path
2
+
3
+ from polyguard.runner import _has_test, discover, render_report, run_checks
4
+
5
+
6
+ def test_python_project_without_tests_fails_closed(tmp_path: Path) -> None:
7
+ (tmp_path / "app.py").write_text("print('hello')\n", encoding="utf-8")
8
+ report = run_checks(tmp_path)
9
+ assert report["status"] == "FAIL"
10
+ assert report["results"][0]["status"] == "FAIL"
11
+ assert "No test suite" in report["results"][0]["reason"]
12
+
13
+
14
+ def test_empty_python_test_file_is_not_accepted(tmp_path: Path) -> None:
15
+ empty_test = tmp_path / "test_app.py"
16
+ empty_test.write_text("# intentionally empty\n", encoding="utf-8")
17
+ assert not _has_test([empty_test], "python")
18
+
19
+
20
+ def test_python_test_file_is_detected(tmp_path: Path) -> None:
21
+ (tmp_path / "app.py").write_text("def add(a, b): return a + b\n", encoding="utf-8")
22
+ test_file = tmp_path / "test_app.py"
23
+ test_file.write_text("def test_add(): assert 1 + 1 == 2\n", encoding="utf-8")
24
+ assert _has_test([test_file], "python")
25
+ assert any(language == "python" for language, _ in discover(tmp_path))
26
+
27
+
28
+ def test_go_requires_a_test_function(tmp_path: Path) -> None:
29
+ empty_test = tmp_path / "main_test.go"
30
+ empty_test.write_text("package main\n", encoding="utf-8")
31
+ assert not _has_test([empty_test], "go")
32
+ empty_test.write_text("package main\nfunc TestAdd(t *testing.T) {}\n", encoding="utf-8")
33
+ assert _has_test([empty_test], "go")
34
+
35
+
36
+ def test_rust_test_annotation_is_detected(tmp_path: Path) -> None:
37
+ test_file = tmp_path / "lib.rs"
38
+ test_file.write_text("#[test]\nfn works() {}\n", encoding="utf-8")
39
+ assert _has_test([test_file], "rust")
40
+
41
+
42
+ def test_cpp_requires_registered_ctest_case(tmp_path: Path) -> None:
43
+ cmake = tmp_path / "CMakeLists.txt"
44
+ test_file = tmp_path / "test_math.cpp"
45
+ test_file.write_text("TEST(Math, Adds) { EXPECT_EQ(1 + 1, 2); }\n", encoding="utf-8")
46
+ cmake.write_text("enable_testing()\nadd_test(NAME math COMMAND math_tests)\n", encoding="utf-8")
47
+ assert _has_test([cmake, test_file], "cpp")
48
+ cmake.write_text("add_test(NAME math COMMAND math_tests)\n", encoding="utf-8")
49
+ assert not _has_test([cmake, test_file], "cpp")
50
+
51
+
52
+ def test_no_supported_language_fails_closed(tmp_path: Path) -> None:
53
+ (tmp_path / "notes.txt").write_text("not source code", encoding="utf-8")
54
+ assert run_checks(tmp_path)["status"] == "FAIL"
55
+
56
+
57
+ def test_report_default_is_one_word() -> None:
58
+ report = {"status": "FAIL", "results": []}
59
+ assert render_report(report) == "FAIL"
60
+
61
+
62
+ def test_json_report_is_machine_readable() -> None:
63
+ import json
64
+
65
+ report = {"status": "PASS", "results": []}
66
+ assert json.loads(render_report(report, as_json=True)) == report
67
+
68
+
69
+ def test_cli_accepts_verify_subcommand() -> None:
70
+ from polyguard.cli import build_parser
71
+
72
+ args = build_parser().parse_args(["verify", "--timeout", "12"])
73
+ assert args.verify == "verify"
74
+ assert args.timeout == 12