Skip to content

Commit c7dfc85

Browse files
committed
[red] Require safe public GitHub import examples
Signed-off-by: Viwat Vchirawongkwin <viwat.v@chula.ac.th>
1 parent 1a4b142 commit c7dfc85

1 file changed

Lines changed: 170 additions & 0 deletions

File tree

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
# SPDX-License-Identifier: MIT
2+
# Part of PyBLE (https://pyble.dev) — see /LICENSE.
3+
4+
from __future__ import annotations
5+
6+
import re
7+
import unittest
8+
from pathlib import Path
9+
10+
REPO_ROOT = Path(__file__).resolve().parents[2]
11+
FIXTURE_ROOT = REPO_ROOT / "examples" / "github-import"
12+
README_PATH = FIXTURE_ROOT / "README.md"
13+
14+
MAX_FILE_BYTES = 256 * 1024
15+
MAX_BATCH_BYTES = 1024 * 1024
16+
SAFE_EXAMPLE_NAME = re.compile(r"^[a-z][a-z0-9_]*\.py$")
17+
18+
FORBIDDEN_BOARD_ASSUMPTIONS = {
19+
"fixed Pin construction": re.compile(r"\b(?:machine\s*\.\s*)?Pin\s*\("),
20+
"fixed pin/GPIO assignment": re.compile(
21+
r"\b(?:[a-z0-9]+_)*(?:pin|gpio)(?:_[a-z0-9]+)*\s*=\s*"
22+
r"(?:\d+|['\"][^'\"]+['\"])",
23+
re.IGNORECASE,
24+
),
25+
"numbered GPIO": re.compile(r"\bgpio[\s_-]*\d+\b", re.IGNORECASE),
26+
"target or board name": re.compile(
27+
r"\b(?:esp8266|esp32(?:-s3|-c3)?|rp2|rp2040|rp2350|stm32|nrf52|"
28+
r"samd|pico(?:\s+2\s+w)?|waveshare)\b",
29+
re.IGNORECASE,
30+
),
31+
"PyBLE board profile": re.compile(
32+
r"\b(?:esp32-4mb|esp32-s3-n16r8|waveshare-esp32-s3-lcd-147b|"
33+
r"esp32-c3-4mb|rpi-pico2-w)\b",
34+
re.IGNORECASE,
35+
),
36+
"board-profile routing": re.compile(
37+
r"\b(?:board_profile|profile_id|pyble_st7789)\b",
38+
re.IGNORECASE,
39+
),
40+
}
41+
42+
43+
class GithubImportExamplesPolicyTest(unittest.TestCase):
44+
def require_fixture_root(self) -> None:
45+
if not FIXTURE_ROOT.is_dir():
46+
self.skipTest(
47+
"examples/github-import is absent; the presence test owns "
48+
"the expected red failure"
49+
)
50+
51+
def python_examples(self) -> list[Path]:
52+
return sorted(
53+
path
54+
for path in FIXTURE_ROOT.iterdir()
55+
if path.is_file() and path.suffix == ".py"
56+
)
57+
58+
def strict_text(self, path: Path) -> tuple[bytes, str]:
59+
payload = path.read_bytes()
60+
self.assertNotIn(
61+
b"\x00",
62+
payload,
63+
f"NUL byte in {path.relative_to(REPO_ROOT)}",
64+
)
65+
try:
66+
text = payload.decode("utf-8", errors="strict")
67+
except UnicodeDecodeError as error:
68+
self.fail(
69+
f"non-UTF-8 fixture {path.relative_to(REPO_ROOT)}: {error}"
70+
)
71+
return payload, text
72+
73+
def test_fixture_directory_readme_and_examples_exist(self) -> None:
74+
self.assertTrue(
75+
FIXTURE_ROOT.is_dir(),
76+
"root examples/github-import must contain the public import fixtures",
77+
)
78+
self.assertTrue(
79+
README_PATH.is_file(),
80+
"examples/github-import/README.md must document the public fixtures",
81+
)
82+
self.assertGreaterEqual(
83+
len(self.python_examples()),
84+
2,
85+
"the public fixture folder must contain multiple lowercase .py examples",
86+
)
87+
88+
def test_fixture_folder_is_flat_source_only_and_safely_named(self) -> None:
89+
self.require_fixture_root()
90+
entries = sorted(FIXTURE_ROOT.rglob("*"))
91+
offenders = [
92+
path.relative_to(REPO_ROOT).as_posix()
93+
for path in entries
94+
if path.is_symlink()
95+
or not path.is_file()
96+
or (
97+
path != README_PATH
98+
and not SAFE_EXAMPLE_NAME.fullmatch(path.name)
99+
)
100+
]
101+
self.assertEqual(
102+
offenders,
103+
[],
104+
"fixtures must be one README plus flat, lowercase, safe .py files",
105+
)
106+
107+
names = [path.name for path in self.python_examples()]
108+
self.assertEqual(
109+
len(names),
110+
len(set(names)),
111+
"GitHub-import fixture basenames must be unique",
112+
)
113+
self.assertEqual(
114+
len(names),
115+
len({name.casefold() for name in names}),
116+
"fixture basenames must remain unique on case-folding filesystems",
117+
)
118+
forbidden_compiled = [
119+
path.relative_to(REPO_ROOT).as_posix()
120+
for path in entries
121+
if path.is_file() and path.suffix.casefold() in {".mpy", ".pyc"}
122+
]
123+
self.assertEqual(
124+
forbidden_compiled,
125+
[],
126+
"public examples must never ship compiled .mpy or .pyc files",
127+
)
128+
129+
def test_fixture_files_are_strict_utf8_nul_free_and_mit(self) -> None:
130+
self.require_fixture_root()
131+
for path in [README_PATH, *self.python_examples()]:
132+
self.assertTrue(path.is_file(), f"missing {path.relative_to(REPO_ROOT)}")
133+
_, text = self.strict_text(path)
134+
header = "\n".join(text.splitlines()[:5])
135+
self.assertIn(
136+
"SPDX-License-Identifier: MIT",
137+
header,
138+
f"missing SPDX MIT header in {path.relative_to(REPO_ROOT)}",
139+
)
140+
141+
def test_python_examples_fit_import_file_and_batch_limits(self) -> None:
142+
self.require_fixture_root()
143+
sizes: list[int] = []
144+
for path in self.python_examples():
145+
size = len(path.read_bytes())
146+
self.assertLessEqual(
147+
size,
148+
MAX_FILE_BYTES,
149+
f"{path.relative_to(REPO_ROOT)} exceeds the 256 KiB file limit",
150+
)
151+
sizes.append(size)
152+
self.assertLessEqual(
153+
sum(sizes),
154+
MAX_BATCH_BYTES,
155+
"the complete public .py fixture batch exceeds 1 MiB",
156+
)
157+
158+
def test_python_examples_make_no_board_pin_or_profile_assumption(self) -> None:
159+
self.require_fixture_root()
160+
for path in self.python_examples():
161+
_, text = self.strict_text(path)
162+
for assumption, pattern in FORBIDDEN_BOARD_ASSUMPTIONS.items():
163+
self.assertIsNone(
164+
pattern.search(text),
165+
f"{path.relative_to(REPO_ROOT)} contains {assumption}",
166+
)
167+
168+
169+
if __name__ == "__main__":
170+
unittest.main()

0 commit comments

Comments
 (0)