aboutsummaryrefslogtreecommitdiff
path: root/tools/tests/test_lint_project.py
blob: 31f8125ad29bceee08d7fe7ad098ede909aba224 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
# Author:  cirakg & claude ai
# Summary: Tests for tools/lint_project.py, the project policy linter.
"""Tests for tools/lint_project.py, the project policy linter."""

from __future__ import annotations

import json
from pathlib import Path

import lint_project as lp
from conftest import PROJECT_ROOT
from PIL import Image
from PIL.PngImagePlugin import PngInfo

GOOD_MANIFEST = "<iq:manifest><iq:permissions/></iq:manifest>\n"


def make_project(tmp_path: Path, files: dict[str, str]) -> Path:
    for name, text in {"manifest.xml": GOOD_MANIFEST, **files}.items():
        path = tmp_path / name
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(text, encoding="utf-8")
    return tmp_path


def codes(root: Path) -> list[tuple[str, str, int]]:
    """Findings of the code rules (MC001-MC005) only."""
    return [(str(p.path), p.code, p.line) for p in sorted(lp.lint_code(root))]


def test_clean_project_passes(tmp_path: Path) -> None:
    root = make_project(
        tmp_path,
        {
            "source/util/Diag.mc": '// Diag\nSystem.println("DIAG {}");\n',
            "source/otp/Base32.mc": (
                '// Base32\nconst ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; // lint: allow-literal\n'
            ),
            "test/T.mc": (
                "// T\n"
                'System.println("debug output in tests is allowed");\n'
                'const V = "GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ";\n'
            ),
        },
    )
    assert codes(root) == []


def test_each_rule_fires(tmp_path: Path) -> None:
    root = make_project(
        tmp_path,
        {
            "source/app/NoHeader.mc": "import Toybox.Lang;\n",
            "source/app/Log.mc": '// Log\nSystem.println("secret?");\n',
            "source/app/Todo.mc": "// Todo\nvar x = 1; // TODO finish\n",
            "source/app/Secret.mc": (
                '// Secret\nconst S = "JBSWY3DPEHPK3PXPJBSWY3DP";\nconst H = "0123456789abcdef0123456789abcdef";\n'
            ),
            "source/app/Net.mc": "// Net\nimport Toybox.Communications;\n",
        },
    )
    assert codes(root) == [
        ("source/app/Log.mc", "MC002", 2),
        ("source/app/Net.mc", "MC005", 2),
        ("source/app/NoHeader.mc", "MC001", 1),
        ("source/app/Secret.mc", "MC004", 2),
        ("source/app/Secret.mc", "MC004", 3),
        ("source/app/Todo.mc", "MC003", 2),
    ]


def test_println_in_comment_is_ignored(tmp_path: Path) -> None:
    root = make_project(tmp_path, {"source/app/A.mc": "// A\nvar a = 1; // System.println(a)\n"})
    assert codes(root) == []


def test_manifest_permissions_are_rejected(tmp_path: Path) -> None:
    root = make_project(
        tmp_path,
        {"manifest.xml": '<iq:permissions>\n<iq:uses-permission id="Communications"/>\n</iq:permissions>\n'},
    )
    assert codes(root) == [("manifest.xml", "MC005", 2)]


HEADER = "# Author:  cirakg & claude ai\n# Summary: A file with a proper header.\n"


def header_findings(root: Path) -> list[str]:
    return sorted(str(p.path) for p in lp.lint_headers(root))


def save_png(path: Path, author: str | None) -> None:
    info = PngInfo()
    if author is not None:
        info.add_text("Author", author)
        info.add_text("Description", "test image")
    path.parent.mkdir(parents=True, exist_ok=True)
    Image.new("RGB", (4, 4)).save(path, pnginfo=info)


def test_headers_are_required_in_every_file_type(tmp_path: Path) -> None:
    files = {
        "ok.sh": "#!/bin/sh\n" + HEADER,
        "late.txt": "\n" * 20 + HEADER,  # too far down
        "none.md": "# Title\n",
        "data/ok.json": json.dumps({"_author": lp.AUTHOR, "_summary": "x"}),
        "data/bad.json": json.dumps({"author": "someone else", "description": "x"}),
        "docker/docs/package-lock.json": "{}",  # generated, exempt
        "build/out.txt": "ignored folder",
    }
    for name, text in files.items():
        path = tmp_path / name
        path.parent.mkdir(parents=True, exist_ok=True)
        path.write_text(text, encoding="utf-8")
    save_png(tmp_path / "img/ok.png", lp.AUTHOR)
    save_png(tmp_path / "img/anon.png", None)
    assert header_findings(tmp_path) == ["data/bad.json", "img/anon.png", "late.txt", "none.md"]


def test_project_passes_its_own_rules() -> None:
    problems = lp.lint(PROJECT_ROOT)
    assert problems == [], "\n".join(map(str, problems))