Skip to content

Commit 1caadf8

Browse files
committed
Add lint and unit tests to Docker CI
1 parent 2f59abd commit 1caadf8

File tree

4 files changed

+127
-1
lines changed

4 files changed

+127
-1
lines changed

Dockerfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ RUN pacman -Syyu arm-none-eabi-newlib --noconfirm
55
RUN pacman -Syyu git --noconfirm
66
RUN pacman -Syyu python-pip --noconfirm
77
RUN pacman -Syyu python-crcmod --noconfirm
8+
RUN pacman -Syyu python-pytest --noconfirm
9+
RUN pacman -Syyu cppcheck --noconfirm
810
WORKDIR /app
911
COPY . .
1012

ci/run.sh

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
4+
ROOT="/app"
5+
ARTIFACT_DIR="${ROOT}/compiled-firmware"
6+
7+
mkdir -p "${ARTIFACT_DIR}"
8+
9+
echo "Running cppcheck lint..."
10+
cppcheck \
11+
--enable=warning,style \
12+
--std=c11 \
13+
--inline-suppr \
14+
--error-exitcode=1 \
15+
--suppress=missingIncludeSystem \
16+
--suppress=unmatchedSuppression \
17+
--suppress=unusedFunction \
18+
--suppress=invalidPrintfArgType_sint \
19+
--suppress=variableScope \
20+
--suppress=unsignedPositive \
21+
--suppress=badBitmaskCheck \
22+
--suppress=unusedStructMember \
23+
--suppress=constParameterPointer \
24+
--suppress=oppositeInnerCondition \
25+
--suppress=normalCheckLevelMaxBranches \
26+
--quiet \
27+
${ROOT}/app \
28+
${ROOT}/audio.c \
29+
${ROOT}/bitmaps.c \
30+
${ROOT}/board.c \
31+
${ROOT}/dcs.c \
32+
${ROOT}/driver \
33+
${ROOT}/functions.c \
34+
${ROOT}/helper \
35+
${ROOT}/misc.c \
36+
${ROOT}/radio.c \
37+
${ROOT}/scheduler.c \
38+
${ROOT}/settings.c \
39+
${ROOT}/ui \
40+
${ROOT}/version.c
41+
42+
echo "Running unit tests..."
43+
pytest -q
44+
45+
echo "Building firmware..."
46+
make clean
47+
make TARGET=loaner-firmware
48+
49+
cp loaner-firmware*.bin "${ARTIFACT_DIR}/"

compile-with-docker.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@ docker build -t "${IMAGE_TAG}" "${SCRIPT_DIR}"
1212
docker run --rm \
1313
-v "${OUT_DIR}":/app/compiled-firmware \
1414
"${IMAGE_TAG}" \
15-
/bin/bash -lc "cd /app && make clean && make TARGET=loaner-firmware && cp loaner-firmware*.bin compiled-firmware/"
15+
/bin/bash -lc "cd /app && chmod +x ci/run.sh && ./ci/run.sh"

tests/test_fw_pack.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import ast
2+
import subprocess
3+
import sys
4+
from binascii import crc_hqx
5+
from itertools import cycle
6+
from pathlib import Path
7+
8+
9+
def _load_obfuscation():
10+
tree = ast.parse(Path("fw-pack.py").read_text())
11+
for node in tree.body:
12+
if isinstance(node, ast.Assign):
13+
target = node.targets[0]
14+
if isinstance(target, ast.Name) and target.id == "OBFUSCATION":
15+
values = []
16+
for elt in node.value.elts:
17+
if isinstance(elt, ast.Constant):
18+
values.append(elt.value)
19+
else:
20+
raise ValueError("Unexpected AST element in OBFUSCATION table")
21+
return values
22+
raise RuntimeError("OBFUSCATION table not found")
23+
24+
25+
OBFUSCATION = _load_obfuscation()
26+
27+
28+
def deobfuscate(payload: bytes) -> bytes:
29+
return bytes(a ^ b for a, b in zip(payload, cycle(OBFUSCATION)))
30+
31+
32+
def run_fw_pack(input_path, version, output_path):
33+
subprocess.run(
34+
[sys.executable, "fw-pack.py", str(input_path), version, str(output_path)],
35+
check=True,
36+
)
37+
38+
39+
def test_fw_pack_injects_version(tmp_path):
40+
plain = (b"\x01\x02\x03\x04" * 4096) # 16 KiB test payload
41+
input_bin = tmp_path / "input.bin"
42+
packed_bin = tmp_path / "output.bin"
43+
input_bin.write_bytes(plain)
44+
45+
run_fw_pack(input_bin, "LOANER01", packed_bin)
46+
47+
data = packed_bin.read_bytes()
48+
assert len(data) == len(plain) + 16 + 2 # Version block inserted + CRC
49+
50+
deobfuscated = deobfuscate(data[:-2])
51+
version_block = deobfuscated[0x2000:0x2010]
52+
assert version_block.startswith(b"*OEFW-")
53+
assert version_block[6:6 + len("LOANER01")] == b"LOANER01"
54+
assert version_block.endswith(b"\x00" * (16 - len("*OEFW-") - len("LOANER01")))
55+
56+
# Verify CRC matches (xmodem polynomial)
57+
payload, crc_bytes = data[:-2], data[-2:]
58+
crc = crc_hqx(payload, 0x0000)
59+
assert crc_bytes == bytes([crc & 0xFF, (crc >> 8) & 0xFF])
60+
61+
62+
def test_fw_pack_rejects_long_version(tmp_path):
63+
input_bin = tmp_path / "input.bin"
64+
input_bin.write_bytes(b"\x00" * 0x2100)
65+
packed_bin = tmp_path / "output.bin"
66+
67+
result = subprocess.run(
68+
[sys.executable, "fw-pack.py", str(input_bin), "THIS_IS_TOO_LONG", str(packed_bin)],
69+
capture_output=True,
70+
text=True,
71+
)
72+
73+
assert result.returncode != 0
74+
assert "Version suffix is too big" in result.stdout
75+
assert not packed_bin.exists()

0 commit comments

Comments
 (0)