Merge pull request #573 from AIOSAI/dev
fix: Windows CI + drone status/diff --all flag
This commit is contained in:
@@ -37,6 +37,52 @@ jobs:
|
||||
pip install -e ".[dev]"
|
||||
- run: coverage run -m pytest -v --tb=short --rootdir=.
|
||||
|
||||
standards:
|
||||
name: seedgo-audit
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
- run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -e ".[dev]"
|
||||
- name: Run seedgo standards audit
|
||||
run: python -c "
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from aipass.seedgo.apps.handlers.audit.branch_audit import audit_branch
|
||||
from aipass.seedgo.apps.handlers.bypass.bypass_handler import load_bypass_rules
|
||||
|
||||
src = Path('src/aipass')
|
||||
pack = src / 'seedgo/apps/handlers/aipass_standards'
|
||||
|
||||
branches = []
|
||||
for d in sorted(src.iterdir()):
|
||||
if d.is_dir() and (d / 'apps').is_dir():
|
||||
entry = d / 'apps' / f'{d.name}.py'
|
||||
branches.append({'name': d.name, 'path': str(d),
|
||||
'entry_file': str(entry) if entry.exists() else ''})
|
||||
|
||||
failed = []
|
||||
for branch in branches:
|
||||
bypass_rules = load_bypass_rules(branch['path'])
|
||||
result = audit_branch(branch, bypass_rules, pack_path=pack)
|
||||
avg = result.get('average', 0)
|
||||
print(f\" {branch['name']:>12}: {avg:.0f}%\")
|
||||
if avg < 80:
|
||||
failed.append((branch['name'], avg))
|
||||
|
||||
if failed:
|
||||
print(f\"\nFAILED: {len(failed)} branch(es) below 80%\")
|
||||
for name, score in failed:
|
||||
print(f\" {name}: {score:.0f}%\")
|
||||
sys.exit(1)
|
||||
else:
|
||||
print(f\"\nAll {len(branches)} branches pass (>=80%)\")
|
||||
"
|
||||
|
||||
coverage:
|
||||
name: coverage
|
||||
needs: [test]
|
||||
|
||||
@@ -17,9 +17,12 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
import stat
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from aipass.api.apps.handlers.auth.env import create_env_template
|
||||
|
||||
|
||||
@@ -60,6 +63,7 @@ class TestCreateEnvTemplate:
|
||||
assert result is True
|
||||
assert target.read_text(encoding="utf-8") == "EXISTING=content\n"
|
||||
|
||||
@pytest.mark.skipif(sys.platform == "win32", reason="Windows does not support Unix file permissions")
|
||||
def test_file_permissions(self, tmp_path: Path) -> None:
|
||||
"""Created file has owner-only read/write (0o600)."""
|
||||
target = tmp_path / "secrets" / ".env"
|
||||
@@ -68,6 +72,7 @@ class TestCreateEnvTemplate:
|
||||
file_mode = stat.S_IMODE(os.stat(target).st_mode)
|
||||
assert file_mode == 0o600
|
||||
|
||||
@pytest.mark.skipif(sys.platform == "win32", reason="Windows does not support Unix file permissions")
|
||||
def test_directory_permissions(self, tmp_path: Path) -> None:
|
||||
"""Parent directory has owner-only access (0o700)."""
|
||||
secrets_dir = tmp_path / "secrets"
|
||||
|
||||
@@ -43,8 +43,8 @@ Three rules:
|
||||
3. Local files = source of truth.
|
||||
|
||||
```
|
||||
drone @git status # changes (all branches)
|
||||
drone @git diff # diff (all branches)
|
||||
drone @git status --all # changes (full repo)
|
||||
drone @git diff --all # diff (full repo)
|
||||
drone @git log # commits (all branches)
|
||||
drone @git branches # remote branches (all branches)
|
||||
drone @git commit "msg" --all # commit all (devpulse only)
|
||||
|
||||
@@ -55,13 +55,14 @@ def get_branch_diff(branch_dir: Path, staged: bool = False) -> dict:
|
||||
rel_dir = branch_dir
|
||||
|
||||
rel_prefix = rel_dir.as_posix() + "/"
|
||||
show_all = rel_dir == Path(".")
|
||||
|
||||
filtered_lines: list[str] = []
|
||||
include_block = False
|
||||
files_changed = 0
|
||||
for line in result.stdout.splitlines():
|
||||
if line.startswith("diff --git"):
|
||||
include_block = rel_prefix in line
|
||||
include_block = show_all or rel_prefix in line
|
||||
if include_block:
|
||||
files_changed += 1
|
||||
if include_block:
|
||||
|
||||
@@ -62,6 +62,7 @@ def get_branch_status(branch_dir: Path) -> dict:
|
||||
rel_dir = branch_dir
|
||||
|
||||
rel_prefix = rel_dir.as_posix() + "/"
|
||||
show_all = rel_dir == Path(".")
|
||||
|
||||
files = []
|
||||
for line in result.stdout.splitlines():
|
||||
@@ -69,7 +70,7 @@ def get_branch_status(branch_dir: Path) -> dict:
|
||||
continue
|
||||
status_code = line[:2].strip()
|
||||
file_path = line[3:]
|
||||
if file_path.startswith(rel_prefix) or file_path == str(rel_dir):
|
||||
if show_all or file_path.startswith(rel_prefix) or file_path == str(rel_dir):
|
||||
files.append({"status": status_code, "path": file_path})
|
||||
|
||||
total = len(files)
|
||||
|
||||
@@ -132,7 +132,7 @@ def handle_command(command: str | None = None, args: list[str] | None = None) ->
|
||||
if command in _GH_PASSTHROUGH_COMMANDS:
|
||||
return _handle_gh_passthrough(command, args)
|
||||
if command == "status":
|
||||
return _handle_status()
|
||||
return _handle_status(args)
|
||||
if command == "diff":
|
||||
return _handle_diff(args)
|
||||
if command == "log":
|
||||
@@ -344,8 +344,11 @@ def _handle_fix(args: list[str], caller: str) -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _handle_status() -> dict:
|
||||
"""Handle the status subcommand (global tier)."""
|
||||
def _handle_status(args: list[str] | None = None) -> dict:
|
||||
"""Handle the status subcommand (global tier). --all for repo-wide."""
|
||||
args = args or []
|
||||
show_all = "--all" in args
|
||||
|
||||
detected = _detect_branch_dir()
|
||||
if detected is None:
|
||||
return {
|
||||
@@ -355,7 +358,13 @@ def _handle_status() -> dict:
|
||||
}
|
||||
|
||||
_, branch_dir = detected
|
||||
result = status_handler.get_branch_status(branch_dir)
|
||||
|
||||
if show_all:
|
||||
repo_root = lock_handler.find_repo_root()
|
||||
result = status_handler.get_branch_status(repo_root)
|
||||
result["message"] = f"{result['total']} file(s) changed in repo"
|
||||
else:
|
||||
result = status_handler.get_branch_status(branch_dir)
|
||||
|
||||
lines = [result["message"]]
|
||||
for f in result["files"]:
|
||||
@@ -369,7 +378,7 @@ def _handle_status() -> dict:
|
||||
|
||||
|
||||
def _handle_diff(args: list[str]) -> dict:
|
||||
"""Handle the diff subcommand (global tier)."""
|
||||
"""Handle the diff subcommand (global tier). --staged, --all supported."""
|
||||
detected = _detect_branch_dir()
|
||||
if detected is None:
|
||||
return {
|
||||
@@ -380,7 +389,10 @@ def _handle_diff(args: list[str]) -> dict:
|
||||
|
||||
_, branch_dir = detected
|
||||
staged = "--staged" in args
|
||||
result = diff_handler.get_branch_diff(branch_dir, staged=staged)
|
||||
show_all = "--all" in args
|
||||
|
||||
target_dir = lock_handler.find_repo_root() if show_all else branch_dir
|
||||
result = diff_handler.get_branch_diff(target_dir, staged=staged)
|
||||
|
||||
return {
|
||||
"stdout": result["diff"] if result["diff"] else result["message"],
|
||||
@@ -532,12 +544,13 @@ def get_help(command: str | None = None) -> str:
|
||||
if command == "pr":
|
||||
return "git pr — DEPRECATED. Agent PRs are no longer supported. Devpulse handles git.\n"
|
||||
if command == "status":
|
||||
return "git status — Show git status filtered to your branch directory [global]\n"
|
||||
return "git status [--all] — Show git status filtered to your branch (--all for repo-wide) [global]\n"
|
||||
if command == "diff":
|
||||
return (
|
||||
"git diff [--staged] — Show git diff filtered to your branch directory [global]\n"
|
||||
"git diff [--staged] [--all] — Show git diff filtered to your branch [global]\n"
|
||||
" Options:\n"
|
||||
" --staged Show staged changes only.\n"
|
||||
" --all Show all repo changes (not just your branch).\n"
|
||||
)
|
||||
if command == "log":
|
||||
return "git log [count] — Show recent git log entries (default: 10) [global]\n"
|
||||
|
||||
@@ -1,26 +1,12 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: windows_compat_check.py
|
||||
# Description: Windows Compatibility Standards Checker Handler
|
||||
# Version: 1.0.0
|
||||
# Version: 1.1.0
|
||||
# Created: 2026-05-10
|
||||
# Modified: 2026-05-10
|
||||
# Modified: 2026-05-14
|
||||
# =============================================
|
||||
|
||||
"""
|
||||
Windows Compatibility Standards Checker Handler
|
||||
|
||||
Detects POSIX-only patterns that will crash or behave incorrectly on
|
||||
Windows. Scans for:
|
||||
|
||||
1. Unguarded POSIX-only imports (fcntl, pwd, grp, termios, resource)
|
||||
2. Unguarded POSIX-only constants (os.WNOHANG, signal.SIGPIPE)
|
||||
3. Unguarded POSIX-only calls (os.fork, os.setpgid, os.killpg, os.waitpid)
|
||||
4. os.kill() without try/except catching OSError
|
||||
|
||||
Detection uses AST parsing to identify violations and check whether they
|
||||
sit inside a platform guard (if sys.platform / if os.name / try-except
|
||||
ImportError).
|
||||
"""
|
||||
"""Windows Compatibility Standards Checker Handler."""
|
||||
|
||||
import ast
|
||||
from pathlib import Path
|
||||
@@ -48,6 +34,15 @@ _GUARDED_EXCEPT_TYPES = frozenset(
|
||||
}
|
||||
)
|
||||
|
||||
_TEST_POSIX_CALLS: dict[tuple[str, str], str] = {
|
||||
("os", "chmod"): "os.chmod() — Windows ignores permission bits",
|
||||
("os", "symlink"): "os.symlink() — Windows needs privileges",
|
||||
("os", "getuid"): "os.getuid() — not available on Windows",
|
||||
("os", "getgid"): "os.getgid() — not available on Windows",
|
||||
("os", "chown"): "os.chown() — not available on Windows",
|
||||
("stat", "S_IMODE"): "stat.S_IMODE() — Unix permission assertion",
|
||||
}
|
||||
|
||||
|
||||
def _is_platform_guard(node: ast.expr) -> bool:
|
||||
"""Return True if the test expression is a sys.platform or os.name comparison."""
|
||||
@@ -172,6 +167,111 @@ def _find_os_kill_violations(tree: ast.Module, guarded: set[int]) -> list[tuple[
|
||||
return violations
|
||||
|
||||
|
||||
def _is_test_file(path: Path) -> bool:
|
||||
return "tests" in path.parts or path.name.startswith("test_") or path.name.endswith("_test.py")
|
||||
|
||||
|
||||
def _is_pytest_mark(node: ast.expr) -> bool:
|
||||
return (
|
||||
isinstance(node, ast.Attribute)
|
||||
and node.attr == "mark"
|
||||
and isinstance(node.value, ast.Name)
|
||||
and node.value.id == "pytest"
|
||||
)
|
||||
|
||||
|
||||
def _references_platform(node: ast.expr) -> bool:
|
||||
for child in ast.walk(node):
|
||||
if isinstance(child, ast.Attribute) and isinstance(child.value, ast.Name):
|
||||
if (child.value.id == "sys" and child.attr == "platform") or (
|
||||
child.value.id == "os" and child.attr == "name"
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _has_platform_skipif(decorators: list[ast.expr]) -> bool:
|
||||
for dec in decorators:
|
||||
if isinstance(dec, ast.Attribute) and dec.attr == "skip" and _is_pytest_mark(dec.value):
|
||||
return True
|
||||
if not isinstance(dec, ast.Call) or not isinstance(dec.func, ast.Attribute):
|
||||
continue
|
||||
func = dec.func
|
||||
if func.attr == "skip" and _is_pytest_mark(func.value):
|
||||
return True
|
||||
if func.attr == "skipif" and _is_pytest_mark(func.value):
|
||||
if any(_references_platform(arg) for arg in dec.args):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _match_posix_call(node: ast.AST) -> str | None:
|
||||
if not isinstance(node, ast.Call) or not isinstance(node.func, ast.Attribute):
|
||||
return None
|
||||
if not isinstance(node.func.value, ast.Name):
|
||||
return None
|
||||
key = (node.func.value.id, node.func.attr)
|
||||
return _TEST_POSIX_CALLS.get(key)
|
||||
|
||||
|
||||
def _match_stat_constant(node: ast.AST) -> str | None:
|
||||
if not isinstance(node, ast.Attribute) or not isinstance(node.value, ast.Name):
|
||||
return None
|
||||
if node.value.id == "stat" and node.attr.startswith("S_I") and node.attr != "S_IMODE":
|
||||
return f"stat.{node.attr} — Unix permission constant"
|
||||
return None
|
||||
|
||||
|
||||
def _scan_test_body(
|
||||
func_node: ast.FunctionDef | ast.AsyncFunctionDef,
|
||||
guarded: set[int],
|
||||
violations: list[tuple[int, str]],
|
||||
) -> None:
|
||||
seen_lines: set[int] = set()
|
||||
for node in ast.walk(func_node):
|
||||
lineno = getattr(node, "lineno", None)
|
||||
if lineno is None or lineno in guarded or lineno in seen_lines:
|
||||
continue
|
||||
|
||||
desc = _match_posix_call(node) or _match_stat_constant(node)
|
||||
if desc:
|
||||
violations.append((lineno, desc))
|
||||
seen_lines.add(lineno)
|
||||
|
||||
|
||||
def _collect_unguarded_tests(
|
||||
parent: ast.Module | ast.ClassDef,
|
||||
class_skipif: bool = False,
|
||||
) -> list[ast.FunctionDef | ast.AsyncFunctionDef]:
|
||||
results: list[ast.FunctionDef | ast.AsyncFunctionDef] = []
|
||||
for node in ast.iter_child_nodes(parent):
|
||||
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||
continue
|
||||
if not node.name.startswith("test_"):
|
||||
continue
|
||||
if not class_skipif and not _has_platform_skipif(node.decorator_list):
|
||||
results.append(node)
|
||||
return results
|
||||
|
||||
|
||||
def _find_test_platform_violations(
|
||||
tree: ast.Module,
|
||||
guarded: set[int],
|
||||
) -> list[tuple[int, str]]:
|
||||
violations: list[tuple[int, str]] = []
|
||||
targets: list[ast.FunctionDef | ast.AsyncFunctionDef] = []
|
||||
|
||||
targets.extend(_collect_unguarded_tests(tree))
|
||||
for node in ast.iter_child_nodes(tree):
|
||||
if isinstance(node, ast.ClassDef):
|
||||
targets.extend(_collect_unguarded_tests(node, _has_platform_skipif(node.decorator_list)))
|
||||
|
||||
for func in targets:
|
||||
_scan_test_body(func, guarded, violations)
|
||||
|
||||
return violations
|
||||
|
||||
|
||||
def check_module(module_path: str, bypass_rules: list | None = None) -> Dict:
|
||||
"""Check a Python file for Windows-incompatible patterns."""
|
||||
path = Path(module_path)
|
||||
@@ -260,6 +360,9 @@ def check_module(module_path: str, bypass_rules: list | None = None) -> Dict:
|
||||
all_violations.extend(_find_posix_call_violations(tree, guarded))
|
||||
all_violations.extend(_find_os_kill_violations(tree, guarded))
|
||||
|
||||
if _is_test_file(path):
|
||||
all_violations.extend(_find_test_platform_violations(tree, guarded))
|
||||
|
||||
non_bypassed = [
|
||||
(ln, desc) for ln, desc in all_violations if not is_bypassed(module_path, "windows_compat", ln, bypass_rules)
|
||||
]
|
||||
|
||||
@@ -0,0 +1,343 @@
|
||||
# =================== AIPass ====================
|
||||
# Name: test_windows_compat.py
|
||||
# Description: Tests for windows_compat_check.py
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-05-14
|
||||
# Modified: 2026-05-14
|
||||
# =============================================
|
||||
|
||||
"""Tests for windows_compat_check — both POSIX-import detection and test-file skipif enforcement."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _mock_infrastructure(monkeypatch):
|
||||
import sys
|
||||
|
||||
mock_logger = MagicMock()
|
||||
mock_json_handler = MagicMock()
|
||||
mock_json_handler.log_operation = MagicMock(return_value=True)
|
||||
|
||||
prax_mod = MagicMock()
|
||||
prax_mod.logger = mock_logger
|
||||
monkeypatch.setitem(sys.modules, "aipass.prax", prax_mod)
|
||||
|
||||
json_pkg = MagicMock()
|
||||
json_pkg.json_handler = mock_json_handler
|
||||
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.json", json_pkg)
|
||||
json_mod = MagicMock()
|
||||
json_mod.log_operation = mock_json_handler.log_operation
|
||||
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.json.json_handler", json_mod)
|
||||
|
||||
bypass_pkg = MagicMock()
|
||||
bypass_ignore = MagicMock()
|
||||
bypass_ignore.get_template_ignore_patterns = MagicMock(return_value=[])
|
||||
from aipass.seedgo.apps.handlers.bypass.utils import is_bypassed as real_is_bypassed
|
||||
|
||||
bypass_utils = MagicMock()
|
||||
bypass_utils.is_bypassed = real_is_bypassed
|
||||
bypass_pkg.utils = bypass_utils
|
||||
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.bypass", bypass_pkg)
|
||||
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.bypass.ignore_handler", bypass_ignore)
|
||||
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.bypass.utils", bypass_utils)
|
||||
|
||||
for mod_name in ["aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check"]:
|
||||
monkeypatch.delitem(sys.modules, mod_name, raising=False)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Existing POSIX-import detection
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_clean_file_passes(tmp_path):
|
||||
f = tmp_path / "clean.py"
|
||||
f.write_text("import os\nx = os.path.join('a', 'b')\n")
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
|
||||
|
||||
result = check_module(str(f))
|
||||
assert result["passed"] is True
|
||||
assert result["score"] == 100
|
||||
|
||||
|
||||
def test_unguarded_fcntl_import_fails(tmp_path):
|
||||
f = tmp_path / "bad.py"
|
||||
f.write_text("import fcntl\nfcntl.flock(0, 0)\n")
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
|
||||
|
||||
result = check_module(str(f))
|
||||
assert result["passed"] is False
|
||||
assert "import fcntl" in result["checks"][0]["message"]
|
||||
|
||||
|
||||
def test_guarded_fcntl_import_passes(tmp_path):
|
||||
f = tmp_path / "guarded.py"
|
||||
f.write_text("import sys\ntry:\n import fcntl\nexcept ImportError:\n fcntl = None\n")
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
|
||||
|
||||
result = check_module(str(f))
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_platform_guarded_os_kill_passes(tmp_path):
|
||||
f = tmp_path / "guarded_kill.py"
|
||||
f.write_text("import os, sys\nif sys.platform != 'win32':\n os.kill(1, 9)\n")
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
|
||||
|
||||
result = check_module(str(f))
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_unguarded_os_kill_fails(tmp_path):
|
||||
f = tmp_path / "bad_kill.py"
|
||||
f.write_text("import os\nos.kill(1, 9)\n")
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
|
||||
|
||||
result = check_module(str(f))
|
||||
assert result["passed"] is False
|
||||
assert "os.kill()" in result["checks"][0]["message"]
|
||||
|
||||
|
||||
def test_init_file_skipped(tmp_path):
|
||||
f = tmp_path / "__init__.py"
|
||||
f.write_text("import fcntl\n")
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
|
||||
|
||||
result = check_module(str(f))
|
||||
assert result["passed"] is True
|
||||
assert result["checks"][0]["message"] == "File skipped (non-target)"
|
||||
|
||||
|
||||
def test_posix_only_call_fork_fails(tmp_path):
|
||||
f = tmp_path / "forker.py"
|
||||
f.write_text("import os\npid = os.fork()\n")
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
|
||||
|
||||
result = check_module(str(f))
|
||||
assert result["passed"] is False
|
||||
assert "os.fork()" in result["checks"][0]["message"]
|
||||
|
||||
|
||||
def test_posix_constant_wnohang_fails(tmp_path):
|
||||
f = tmp_path / "waiter.py"
|
||||
f.write_text("import os\nflags = os.WNOHANG\n")
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
|
||||
|
||||
result = check_module(str(f))
|
||||
assert result["passed"] is False
|
||||
assert "os.WNOHANG" in result["checks"][0]["message"]
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Test-file skipif enforcement (new)
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_unguarded_chmod_in_test_file_fails(tmp_path):
|
||||
tests_dir = tmp_path / "tests"
|
||||
tests_dir.mkdir()
|
||||
f = tests_dir / "test_perms.py"
|
||||
f.write_text("import os\n\ndef test_permissions():\n os.chmod('file', 0o600)\n")
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
|
||||
|
||||
result = check_module(str(f))
|
||||
assert result["passed"] is False
|
||||
assert "os.chmod()" in result["checks"][0]["message"]
|
||||
|
||||
|
||||
def test_skipif_guarded_chmod_passes(tmp_path):
|
||||
tests_dir = tmp_path / "tests"
|
||||
tests_dir.mkdir()
|
||||
f = tests_dir / "test_perms.py"
|
||||
f.write_text(
|
||||
"import os, sys, pytest\n\n"
|
||||
'@pytest.mark.skipif(sys.platform == "win32", reason="no perms")\n'
|
||||
"def test_permissions():\n"
|
||||
" os.chmod('file', 0o600)\n"
|
||||
)
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
|
||||
|
||||
result = check_module(str(f))
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_unguarded_stat_imode_in_test_fails(tmp_path):
|
||||
tests_dir = tmp_path / "tests"
|
||||
tests_dir.mkdir()
|
||||
f = tests_dir / "test_perms.py"
|
||||
f.write_text(
|
||||
"import os, stat\n\n"
|
||||
"def test_file_mode():\n"
|
||||
" mode = stat.S_IMODE(os.stat('file').st_mode)\n"
|
||||
" assert mode == 0o600\n"
|
||||
)
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
|
||||
|
||||
result = check_module(str(f))
|
||||
assert result["passed"] is False
|
||||
assert "stat.S_IMODE()" in result["checks"][0]["message"]
|
||||
|
||||
|
||||
def test_unguarded_stat_constant_in_test_fails(tmp_path):
|
||||
tests_dir = tmp_path / "tests"
|
||||
tests_dir.mkdir()
|
||||
f = tests_dir / "test_perms.py"
|
||||
f.write_text("import stat\n\ndef test_check_bits():\n expected = stat.S_IRUSR | stat.S_IWUSR\n")
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
|
||||
|
||||
result = check_module(str(f))
|
||||
assert result["passed"] is False
|
||||
assert "stat.S_IRUSR" in result["checks"][0]["message"]
|
||||
|
||||
|
||||
def test_unguarded_symlink_in_test_fails(tmp_path):
|
||||
tests_dir = tmp_path / "tests"
|
||||
tests_dir.mkdir()
|
||||
f = tests_dir / "test_links.py"
|
||||
f.write_text("import os\n\ndef test_symlink():\n os.symlink('a', 'b')\n")
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
|
||||
|
||||
result = check_module(str(f))
|
||||
assert result["passed"] is False
|
||||
assert "os.symlink()" in result["checks"][0]["message"]
|
||||
|
||||
|
||||
def test_unguarded_getuid_in_test_fails(tmp_path):
|
||||
tests_dir = tmp_path / "tests"
|
||||
tests_dir.mkdir()
|
||||
f = tests_dir / "test_user.py"
|
||||
f.write_text("import os\n\ndef test_uid():\n uid = os.getuid()\n")
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
|
||||
|
||||
result = check_module(str(f))
|
||||
assert result["passed"] is False
|
||||
assert "os.getuid()" in result["checks"][0]["message"]
|
||||
|
||||
|
||||
def test_class_level_skipif_guards_all_methods(tmp_path):
|
||||
tests_dir = tmp_path / "tests"
|
||||
tests_dir.mkdir()
|
||||
f = tests_dir / "test_perms.py"
|
||||
f.write_text(
|
||||
"import os, sys, pytest\n\n"
|
||||
'@pytest.mark.skipif(sys.platform == "win32", reason="unix only")\n'
|
||||
"class TestPermissions:\n"
|
||||
" def test_chmod(self):\n"
|
||||
" os.chmod('file', 0o600)\n"
|
||||
" def test_getuid(self):\n"
|
||||
" os.getuid()\n"
|
||||
)
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
|
||||
|
||||
result = check_module(str(f))
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_method_level_skipif_in_unguarded_class(tmp_path):
|
||||
tests_dir = tmp_path / "tests"
|
||||
tests_dir.mkdir()
|
||||
f = tests_dir / "test_mixed.py"
|
||||
f.write_text(
|
||||
"import os, sys, pytest\n\n"
|
||||
"class TestMixed:\n"
|
||||
' @pytest.mark.skipif(sys.platform == "win32", reason="unix")\n'
|
||||
" def test_guarded(self):\n"
|
||||
" os.chmod('file', 0o600)\n"
|
||||
" def test_unguarded(self):\n"
|
||||
" os.chmod('file', 0o700)\n"
|
||||
)
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
|
||||
|
||||
result = check_module(str(f))
|
||||
assert result["passed"] is False
|
||||
assert "os.chmod()" in result["checks"][0]["message"]
|
||||
|
||||
|
||||
def test_pytest_mark_skip_unconditional_passes(tmp_path):
|
||||
tests_dir = tmp_path / "tests"
|
||||
tests_dir.mkdir()
|
||||
f = tests_dir / "test_skipped.py"
|
||||
f.write_text(
|
||||
"import os, pytest\n\n@pytest.mark.skip(reason=\"not yet\")\ndef test_perms():\n os.chmod('file', 0o600)\n"
|
||||
)
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
|
||||
|
||||
result = check_module(str(f))
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_non_test_file_ignores_chmod(tmp_path):
|
||||
f = tmp_path / "handler.py"
|
||||
f.write_text("import os\n\ndef set_perms():\n os.chmod('file', 0o600)\n")
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
|
||||
|
||||
result = check_module(str(f))
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_inline_platform_guard_in_test_passes(tmp_path):
|
||||
tests_dir = tmp_path / "tests"
|
||||
tests_dir.mkdir()
|
||||
f = tests_dir / "test_guarded_inline.py"
|
||||
f.write_text(
|
||||
"import os, sys\n\ndef test_perms():\n if sys.platform != \"win32\":\n os.chmod('file', 0o600)\n"
|
||||
)
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
|
||||
|
||||
result = check_module(str(f))
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_os_name_skipif_passes(tmp_path):
|
||||
tests_dir = tmp_path / "tests"
|
||||
tests_dir.mkdir()
|
||||
f = tests_dir / "test_osname.py"
|
||||
f.write_text(
|
||||
"import os, pytest\n\n"
|
||||
'@pytest.mark.skipif(os.name != "posix", reason="posix only")\n'
|
||||
"def test_chmod():\n"
|
||||
" os.chmod('file', 0o600)\n"
|
||||
)
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
|
||||
|
||||
result = check_module(str(f))
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_non_test_function_in_test_file_ignored(tmp_path):
|
||||
tests_dir = tmp_path / "tests"
|
||||
tests_dir.mkdir()
|
||||
f = tests_dir / "test_helpers.py"
|
||||
f.write_text(
|
||||
"import os\n\ndef helper_setup():\n os.chmod('file', 0o600)\n\ndef test_clean():\n assert True\n"
|
||||
)
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
|
||||
|
||||
result = check_module(str(f))
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_chown_in_test_fails(tmp_path):
|
||||
tests_dir = tmp_path / "tests"
|
||||
tests_dir.mkdir()
|
||||
f = tests_dir / "test_owner.py"
|
||||
f.write_text("import os\n\ndef test_ownership():\n os.chown('file', 1000, 1000)\n")
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
|
||||
|
||||
result = check_module(str(f))
|
||||
assert result["passed"] is False
|
||||
assert "os.chown()" in result["checks"][0]["message"]
|
||||
|
||||
|
||||
def test_getgid_in_test_fails(tmp_path):
|
||||
tests_dir = tmp_path / "tests"
|
||||
tests_dir.mkdir()
|
||||
f = tests_dir / "test_gid.py"
|
||||
f.write_text("import os\n\ndef test_group():\n gid = os.getgid()\n")
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.windows_compat_check import check_module
|
||||
|
||||
result = check_module(str(f))
|
||||
assert result["passed"] is False
|
||||
assert "os.getgid()" in result["checks"][0]["message"]
|
||||
Reference in New Issue
Block a user