secubox-deb/tests/test_license_headers.py
CyberMind-FR b88b8ada95 fix(license): tighten detect_existing + missing-file = repo-wide (ref #81)
Two bug fixes to the CMSD-1.0 license-header tool:

1. detect_existing() previously matched the SPDX token anywhere in the
   first 10 lines, including inside docstrings and prose comments.
   Tightened the matcher to require comment markers (#, //, *, <!--)
   and whitespace before the SPDX token.

   Regression tests:
   - test_detect_existing_no_false_match_in_docstring
   - test_detect_existing_no_false_match_inline_comment_prose

2. _read_enrollment() now returns ["**"] when the allowlist file is
   absent, per spec §5.2 "missing file → repo-wide enforcement".
   Previously it returned [] (nothing enforced), making Phase C closure
   impossible without an additional file.

   Empty allowlist file still returns [] (Phase A initial), so
   test_main_empty_allowlist_passes_check is unaffected.

   New tests:
   - test_read_enrollment_missing_file_means_repo_wide
   - test_main_check_missing_allowlist_enforces_repo_wide

Suite: 49 → 53 passing. --check still exits 0 repo-wide.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 11:39:57 +02:00

464 lines
16 KiB
Python

# SPDX-License-Identifier: LicenseRef-CMSD-1.0
# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>
# Source-Disclosed License — All rights reserved except as expressly granted.
# See LICENCE-CMSD-1.0.md for terms.
# tests/test_license_headers.py
"""Tests for scripts/license-headers.py.
The tool's filename contains a hyphen so it is loaded via
importlib.util.spec_from_file_location rather than a normal import.
"""
from __future__ import annotations
import importlib.util
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parent.parent
_TOOL_PATH = REPO_ROOT / "scripts" / "license-headers.py"
_spec = importlib.util.spec_from_file_location("license_headers", _TOOL_PATH)
license_headers = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(license_headers)
def test_module_imports():
assert hasattr(license_headers, "main")
EXPECTED_HASH_HEADER = (
"# SPDX-License-Identifier: LicenseRef-CMSD-1.0\n"
"# Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>\n"
"# Source-Disclosed License — All rights reserved except as expressly granted.\n"
"# See LICENCE-CMSD-1.0.md for terms.\n"
)
def test_render_header_hash():
assert license_headers.render_header("hash") == EXPECTED_HASH_HEADER
EXPECTED_SLASH_HEADER = (
"// SPDX-License-Identifier: LicenseRef-CMSD-1.0\n"
"// Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>\n"
"// Source-Disclosed License — All rights reserved except as expressly granted.\n"
"// See LICENCE-CMSD-1.0.md for terms.\n"
)
EXPECTED_BLOCK_HEADER = (
"/*\n"
" * SPDX-License-Identifier: LicenseRef-CMSD-1.0\n"
" * Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>\n"
" * Source-Disclosed License — All rights reserved except as expressly granted.\n"
" * See LICENCE-CMSD-1.0.md for terms.\n"
" */\n"
)
EXPECTED_HTML_HEADER = (
"<!--\n"
" SPDX-License-Identifier: LicenseRef-CMSD-1.0\n"
" Copyright (c) 2026 CyberMind — Gérald Kerma <devel@cybermind.fr>\n"
" Source-Disclosed License — All rights reserved except as expressly granted.\n"
" See LICENCE-CMSD-1.0.md for terms.\n"
"-->\n"
)
def test_render_header_slash():
assert license_headers.render_header("slash") == EXPECTED_SLASH_HEADER
def test_render_header_block():
assert license_headers.render_header("block") == EXPECTED_BLOCK_HEADER
def test_render_header_html():
assert license_headers.render_header("html") == EXPECTED_HTML_HEADER
def test_detect_existing_none_on_empty():
assert license_headers.detect_existing("") == "NONE"
def test_detect_existing_none_on_plain_code():
assert license_headers.detect_existing("print('hello')\n") == "NONE"
def test_detect_existing_match_hash():
text = EXPECTED_HASH_HEADER + "\nprint('hello')\n"
assert license_headers.detect_existing(text) == "MATCH"
def test_detect_existing_match_slash():
text = EXPECTED_SLASH_HEADER + "\nconsole.log('hi');\n"
assert license_headers.detect_existing(text) == "MATCH"
def test_detect_existing_match_block():
text = EXPECTED_BLOCK_HEADER + "\nint main(void) { return 0; }\n"
assert license_headers.detect_existing(text) == "MATCH"
def test_detect_existing_match_html():
text = EXPECTED_HTML_HEADER + "\n<html></html>\n"
assert license_headers.detect_existing(text) == "MATCH"
def test_detect_existing_foreign_mit():
text = "# SPDX-License-Identifier: MIT\nprint('hello')\n"
assert license_headers.detect_existing(text) == "FOREIGN"
def test_detect_existing_foreign_gpl():
text = "// SPDX-License-Identifier: GPL-2.0-or-later\nint x;\n"
assert license_headers.detect_existing(text) == "FOREIGN"
def test_detect_existing_only_checks_first_10_lines():
"""A CMSD line buried deep in the file should NOT be treated as MATCH."""
text = "\n".join(["# unrelated"] * 20) + "\n# SPDX-License-Identifier: LicenseRef-CMSD-1.0\n"
assert license_headers.detect_existing(text) == "NONE"
def test_detect_existing_no_false_match_in_docstring():
"""Prose mentions of SPDX inside docstrings/comments should NOT match.
Regression: previously the regex matched any 'SPDX-License-Identifier:'
token anywhere in the first 10 lines, including inside Python docstrings
that *describe* what an SPDX header looks like.
"""
text = (
'"""License header tool.\n'
'\n'
'Adds the SPDX-License-Identifier: LicenseRef-CMSD-1.0 header.\n'
'"""\n'
'x = 1\n'
)
assert license_headers.detect_existing(text) == "NONE"
def test_detect_existing_no_false_match_inline_comment_prose():
"""`# Description mentioning SPDX-License-Identifier: ...` is NOT a license line."""
text = "# This module documents SPDX-License-Identifier: MIT compliance.\nx = 1\n"
assert license_headers.detect_existing(text) == "NONE"
def test_apply_python_plain():
src = '"""Docstring."""\nprint("hi")\n'
out = license_headers.apply(src, ".py")
assert out == EXPECTED_HASH_HEADER + "\n" + src
def test_apply_python_idempotent():
src = '"""Docstring."""\nprint("hi")\n'
once = license_headers.apply(src, ".py")
twice = license_headers.apply(once, ".py")
assert once == twice
def test_apply_foreign_python_unchanged():
src = "# SPDX-License-Identifier: MIT\nprint('x')\n"
out = license_headers.apply(src, ".py")
assert out == src
def test_apply_python_with_shebang():
src = '#!/usr/bin/env python3\nprint("x")\n'
out = license_headers.apply(src, ".py")
assert out.startswith("#!/usr/bin/env python3\n")
assert out.splitlines()[1] == "# SPDX-License-Identifier: LicenseRef-CMSD-1.0"
def test_apply_python_with_encoding():
src = '# -*- coding: utf-8 -*-\nprint("x")\n'
out = license_headers.apply(src, ".py")
assert out.startswith("# -*- coding: utf-8 -*-\n")
assert out.splitlines()[1] == "# SPDX-License-Identifier: LicenseRef-CMSD-1.0"
def test_apply_python_with_shebang_and_encoding():
src = '#!/usr/bin/env python3\n# -*- coding: utf-8 -*-\nprint("x")\n'
out = license_headers.apply(src, ".py")
lines = out.splitlines()
assert lines[0] == "#!/usr/bin/env python3"
assert lines[1] == "# -*- coding: utf-8 -*-"
assert lines[2] == "# SPDX-License-Identifier: LicenseRef-CMSD-1.0"
def test_apply_bash_with_shebang():
src = '#!/usr/bin/env bash\nset -euo pipefail\necho "hi"\n'
out = license_headers.apply(src, ".sh")
assert out.startswith("#!/usr/bin/env bash\n")
assert out.splitlines()[1] == "# SPDX-License-Identifier: LicenseRef-CMSD-1.0"
def test_apply_bash_without_shebang():
src = 'echo "hi"\n'
out = license_headers.apply(src, ".sh")
assert out.startswith("# SPDX-License-Identifier: LicenseRef-CMSD-1.0\n")
def test_apply_bash_idempotent():
src = '#!/usr/bin/env bash\nset -e\n'
once = license_headers.apply(src, ".sh")
twice = license_headers.apply(once, ".sh")
assert once == twice
def test_apply_javascript():
src = 'console.log("hi");\n'
out = license_headers.apply(src, ".js")
assert out == EXPECTED_SLASH_HEADER + "\n" + src
def test_apply_typescript():
src = 'const x: number = 1;\n'
out = license_headers.apply(src, ".ts")
assert out == EXPECTED_SLASH_HEADER + "\n" + src
def test_apply_css():
src = 'body { color: red; }\n'
out = license_headers.apply(src, ".css")
assert out == EXPECTED_BLOCK_HEADER + "\n" + src
def test_apply_c():
src = 'int main(void) { return 0; }\n'
out = license_headers.apply(src, ".c")
assert out == EXPECTED_BLOCK_HEADER + "\n" + src
def test_apply_idempotent_all_styles():
cases = [
(".js", 'console.log("x");\n'),
(".ts", 'const x = 1;\n'),
(".css", 'a { color: red; }\n'),
(".c", 'int x;\n'),
(".h", '#pragma once\n'),
]
for ext, src in cases:
once = license_headers.apply(src, ext)
twice = license_headers.apply(once, ext)
assert once == twice, f"non-idempotent for {ext}"
def test_apply_html_no_doctype():
src = '<html><body>hi</body></html>\n'
out = license_headers.apply(src, ".html")
assert out == EXPECTED_HTML_HEADER + "\n" + src
def test_apply_html_with_doctype():
src = '<!DOCTYPE html>\n<html><body>hi</body></html>\n'
out = license_headers.apply(src, ".html")
lines = out.splitlines(keepends=True)
assert lines[0] == "<!DOCTYPE html>\n"
assert lines[1] == "<!--\n"
assert "SPDX-License-Identifier: LicenseRef-CMSD-1.0" in lines[2]
def test_apply_html_idempotent():
src = '<!DOCTYPE html>\n<html></html>\n'
once = license_headers.apply(src, ".html")
twice = license_headers.apply(once, ".html")
assert once == twice
def test_apply_markdown_plain():
src = '# Title\n\nBody.\n'
out = license_headers.apply(src, ".md")
assert out == EXPECTED_HTML_HEADER + "\n" + src
def test_apply_markdown_with_frontmatter():
src = '---\ntitle: Foo\n---\n\n# Body\n'
out = license_headers.apply(src, ".md")
lines = out.splitlines(keepends=True)
assert lines[0] == "---\n"
assert lines[1] == "title: Foo\n"
assert lines[2] == "---\n"
assert lines[3] == "<!--\n"
def test_apply_markdown_idempotent_with_frontmatter():
src = '---\ntitle: Foo\n---\n\nBody.\n'
once = license_headers.apply(src, ".md")
twice = license_headers.apply(once, ".md")
assert once == twice
def test_walk_finds_python(tmp_path):
(tmp_path / "a.py").write_text("x = 1\n")
(tmp_path / "b.txt").write_text("ignore me\n")
result = list(license_headers.walk([tmp_path], enrolled=["**"]))
assert tmp_path / "a.py" in result
assert tmp_path / "b.txt" not in result
def test_walk_prunes_kernel_build(tmp_path):
(tmp_path / "kernel-build").mkdir()
(tmp_path / "kernel-build" / "a.c").write_text("int x;\n")
(tmp_path / "real.c").write_text("int y;\n")
result = list(license_headers.walk([tmp_path], enrolled=["**"]))
assert tmp_path / "real.c" in result
assert tmp_path / "kernel-build" / "a.c" not in result
def test_walk_prunes_node_modules(tmp_path):
(tmp_path / "node_modules").mkdir()
(tmp_path / "node_modules" / "lib.js").write_text("// vendor\n")
(tmp_path / "src.js").write_text("// app\n")
result = list(license_headers.walk([tmp_path], enrolled=["**"]))
assert tmp_path / "src.js" in result
assert tmp_path / "node_modules" / "lib.js" not in result
def test_walk_skips_minified(tmp_path):
(tmp_path / "app.min.js").write_text("// min\n")
(tmp_path / "app.js").write_text("// raw\n")
result = list(license_headers.walk([tmp_path], enrolled=["**"]))
assert tmp_path / "app.js" in result
assert tmp_path / "app.min.js" not in result
def test_walk_respects_empty_allowlist(tmp_path):
(tmp_path / "a.py").write_text("x = 1\n")
result = list(license_headers.walk([tmp_path], enrolled=[]))
assert result == []
def test_walk_respects_glob_allowlist(tmp_path):
(tmp_path / "common").mkdir()
(tmp_path / "common" / "a.py").write_text("x = 1\n")
(tmp_path / "other").mkdir()
(tmp_path / "other" / "b.py").write_text("y = 1\n")
result = list(license_headers.walk([tmp_path], enrolled=["common/**"]))
rel = sorted(p.relative_to(tmp_path).as_posix() for p in result)
assert rel == ["common/a.py"]
def test_walk_subdir_with_repo_root_arg(tmp_path):
"""Walking a subdirectory must match repo-relative allowlist patterns
(regression: discovered during Phase B pilot — user passes
`--fix packages/secubox-hub`, allowlist has `packages/secubox-hub/**`).
"""
(tmp_path / "packages" / "hub").mkdir(parents=True)
(tmp_path / "packages" / "hub" / "a.py").write_text("x = 1\n")
(tmp_path / "packages" / "other" / "b.py").parent.mkdir(parents=True)
(tmp_path / "packages" / "other" / "b.py").write_text("y = 1\n")
result = list(
license_headers.walk(
[tmp_path / "packages" / "hub"],
enrolled=["packages/hub/**"],
repo_root=tmp_path,
)
)
rel = sorted(p.resolve().relative_to(tmp_path.resolve()).as_posix() for p in result)
assert rel == ["packages/hub/a.py"]
def _write_enrolled(tmp_path: Path, patterns: list[str]) -> Path:
f = tmp_path / "scripts" / "license-headers-enrolled.txt"
f.parent.mkdir(parents=True, exist_ok=True)
f.write_text("\n".join(patterns) + "\n")
return f
def test_main_check_clean_repo(tmp_path, monkeypatch):
(tmp_path / ".git").mkdir()
(tmp_path / "scripts").mkdir(exist_ok=True)
_write_enrolled(tmp_path, ["**"])
(tmp_path / "a.py").write_text(EXPECTED_HASH_HEADER + "\nx = 1\n")
monkeypatch.chdir(tmp_path)
rc = license_headers.main(["--check"])
assert rc == 0
def test_main_check_dirty_repo(tmp_path, monkeypatch, capsys):
(tmp_path / ".git").mkdir()
(tmp_path / "scripts").mkdir(exist_ok=True)
_write_enrolled(tmp_path, ["**"])
(tmp_path / "a.py").write_text("x = 1\n")
monkeypatch.chdir(tmp_path)
rc = license_headers.main(["--check"])
assert rc == 1
out = capsys.readouterr().out
assert "a.py" in out
def test_main_fix_writes_files(tmp_path, monkeypatch):
(tmp_path / ".git").mkdir()
(tmp_path / "scripts").mkdir(exist_ok=True)
_write_enrolled(tmp_path, ["**"])
f = tmp_path / "a.py"
f.write_text("x = 1\n")
monkeypatch.chdir(tmp_path)
rc = license_headers.main(["--fix"])
assert rc == 0
text = f.read_text()
assert "SPDX-License-Identifier: LicenseRef-CMSD-1.0" in text
def test_main_list(tmp_path, monkeypatch, capsys):
(tmp_path / ".git").mkdir()
(tmp_path / "scripts").mkdir(exist_ok=True)
_write_enrolled(tmp_path, ["**"])
(tmp_path / "a.py").write_text("x = 1\n")
monkeypatch.chdir(tmp_path)
rc = license_headers.main(["--list"])
assert rc == 0
out = capsys.readouterr().out
assert "a.py" in out
def test_main_diff(tmp_path, monkeypatch, capsys):
(tmp_path / ".git").mkdir()
(tmp_path / "scripts").mkdir(exist_ok=True)
_write_enrolled(tmp_path, ["**"])
(tmp_path / "a.py").write_text("x = 1\n")
monkeypatch.chdir(tmp_path)
rc = license_headers.main(["--diff"])
assert rc == 0
out = capsys.readouterr().out
assert "SPDX-License-Identifier: LicenseRef-CMSD-1.0" in out
# File was not modified by --diff
assert (tmp_path / "a.py").read_text() == "x = 1\n"
def test_main_modes_mutually_exclusive():
rc = license_headers.main(["--check", "--fix"])
assert rc == 2
def test_main_requires_a_mode():
rc = license_headers.main([])
assert rc == 2
def test_main_empty_allowlist_passes_check(tmp_path, monkeypatch):
"""An empty enrollment file means nothing is checked — Phase A initial state."""
(tmp_path / ".git").mkdir()
(tmp_path / "scripts").mkdir(exist_ok=True)
_write_enrolled(tmp_path, [])
(tmp_path / "a.py").write_text("x = 1\n") # no header, but not enrolled
monkeypatch.chdir(tmp_path)
rc = license_headers.main(["--check"])
assert rc == 0
def test_read_enrollment_missing_file_means_repo_wide(tmp_path):
"""Spec §5.2: missing allowlist file = repo-wide enforcement (Phase C final)."""
assert license_headers._read_enrollment(tmp_path) == ["**"]
def test_main_check_missing_allowlist_enforces_repo_wide(tmp_path, monkeypatch):
"""With no allowlist file present, --check should fail on any unheadered file."""
(tmp_path / ".git").mkdir()
(tmp_path / "scripts").mkdir(exist_ok=True)
# No enrollment file written.
(tmp_path / "a.py").write_text("x = 1\n") # no header
monkeypatch.chdir(tmp_path)
rc = license_headers.main(["--check"])
assert rc == 1