implement an integration test suite

This commit is contained in:
2026-08-08 04:29:38 +03:00
parent 9b01bcce2d
commit ecce4c8170
2 changed files with 180 additions and 0 deletions
+11
View File
@@ -5,3 +5,14 @@ set(CMAKE_CXX_STANDARD 17)
add_executable(ccolors main.cpp)
# Phase 1: black-box integration test suite.
enable_testing()
find_package(Python3 COMPONENTS Interpreter)
if(Python3_Interpreter_FOUND)
add_test(
NAME ccolors_integration
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test_runner.py
$<TARGET_FILE:ccolors>
)
endif()
Executable
+169
View File
@@ -0,0 +1,169 @@
#!/usr/bin/env python3
"""End-to-end integration test suite for ccolors.
Phase 1: black-box tests against the built `ccolors` binary.
Generates minimal valid (PNG/JPEG) and invalid test images, runs the binary,
and asserts against terminal stdout and the generated palette.json.
Run directly (requires a built binary) or via ctest:
python3 test_runner.py path/to/ccolors
# or from a CMake build dir
ctest --output-on-failure
"""
import json
import os
import re
import shutil
import struct
import subprocess
import sys
import tempfile
import zlib
# A swatch line ends with an ANSI reset escape followed by " #RRGGBB".
HEX_LINE_RE = re.compile(rb"\x1b\[0m #([0-9A-F]{6})\n")
def make_png(path, width, height, rgb):
"""Write a minimal solid-color PNG using only the stdlib (zlib)."""
def chunk(tag, data):
c = tag + data
return struct.pack(">I", len(data)) + c + struct.pack(
">I", zlib.crc32(c) & 0xFFFFFFFF)
raw = b""
row = b"\x00" + bytes(rgb) * width
for _ in range(height):
raw += row
png = b"\x89PNG\r\n\x1a\n"
png += chunk(b"IHDR", struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0))
png += chunk(b"IDAT", zlib.compress(raw))
png += chunk(b"IEND", b"")
with open(path, "wb") as f:
f.write(png)
def run(binary, *args, cwd=None):
"""Run ccolors and return (returncode, stdout_bytes)."""
proc = subprocess.run([binary] + list(args), capture_output=True, cwd=cwd)
return proc.returncode, proc.stdout
def assert_(cond, msg):
if not cond:
raise AssertionError(msg)
def test_happy_path_png(binary, workdir):
img = os.path.join(workdir, "solid.png")
make_png(img, 40, 40, (200, 100, 50))
rc, out = run(binary, img, cwd=workdir)
assert_(rc == 0, "happy path PNG: expected exit 0, got %d" % rc)
assert_(b"PNG file detected!" in out, "missing PNG detection banner")
assert_(b"Width: 40" in out, "missing width in output")
assert_(b"Height: 40" in out, "missing height in output")
assert_(b"Sampled pixels:" in out, "missing sampled-pixels line")
matches = HEX_LINE_RE.findall(out)
assert_(len(matches) >= 1, "expected at least one hex swatch line")
for hexstr in matches:
assert_(re.fullmatch(r"[0-9A-F]{6}", hexstr.decode()), "malformed hex output")
jpath = os.path.join(workdir, "palette.json")
assert_(os.path.exists(jpath), "palette.json was not written")
with open(jpath) as f:
data = json.load(f)
assert_(data["image"] == img, "palette.json image field mismatch")
palette = data["palette"]
assert_(isinstance(palette, list) and len(palette) <= 5,
"palette must be a list of at most 5 entries")
assert_(len(palette) >= 1, "palette should contain extracted colors")
for entry in palette:
for k in ("r", "g", "b", "hex"):
assert_(k in entry, "palette entry missing key %r" % k)
assert_(re.fullmatch(r"#[0-9A-F]{6}", entry["hex"]),
"palette hex malformed: %r" % entry["hex"])
return len(matches)
def test_happy_path_jpeg(binary, workdir):
convert = shutil.which("convert")
if convert is None:
print(" [skip] JPEG happy-path: ImageMagick 'convert' not available",
file=sys.stderr)
return None
img = os.path.join(workdir, "solid.jpg")
subprocess.run([convert, "-size", "40x40", "xc:#C86432", img],
check=True)
rc, out = run(binary, img, cwd=workdir)
assert_(rc == 0, "happy path JPEG: expected exit 0, got %d" % rc)
assert_(b"JPEG file detected!" in out, "missing JPEG detection banner")
jpath = os.path.join(workdir, "palette.json")
with open(jpath) as f:
data = json.load(f)
assert_(isinstance(data["palette"], list), "JPEG palette not a list")
return len(HEX_LINE_RE.findall(out))
def test_file_not_found(binary, workdir):
missing = os.path.join(workdir, "does_not_exist.jpg")
# Ensure it truly does not exist (isolated temp dir already guarantees it).
if os.path.exists(missing):
os.remove(missing)
rc, out = run(binary, missing, cwd=workdir)
assert_(rc != 0, "file-not-found: expected non-zero exit")
assert_(b"File not found" in out, "missing 'File not found' message")
def test_invalid_format(binary, workdir):
bogus = os.path.join(workdir, "bogus.png")
with open(bogus, "wb") as f:
f.write(b"this is definitely not an image file \x00\x01\x02")
rc, out = run(binary, bogus, cwd=workdir)
assert_(rc != 0, "invalid-format: expected non-zero exit")
assert_(b"Unknown image file format" in out or b"Unkown image file format" in out,
"missing unknown-image-format message")
def main():
if len(sys.argv) < 2:
print("usage: %s <path-to-ccolors-binary>" % sys.argv[0], file=sys.stderr)
return 1
binary = os.path.abspath(sys.argv[1])
if not os.path.exists(binary):
print("binary not found: %s" % binary, file=sys.stderr)
return 1
# Each test runs in its own temp dir so palette.json never collides.
tests = [
("happy_path_png", lambda d: test_happy_path_png(binary, d)),
("happy_path_jpeg", lambda d: test_happy_path_jpeg(binary, d)),
("file_not_found", lambda d: test_file_not_found(binary, d)),
("invalid_format", lambda d: test_invalid_format(binary, d)),
]
failures = []
for name, fn in tests:
workdir = tempfile.mkdtemp(prefix="ccolors_test_")
try:
info = fn(workdir)
tail = " (%d color line(s))" % info if isinstance(info, int) else ""
print("PASS %s%s" % (name, tail))
except AssertionError as exc:
print("FAIL %s: %s" % (name, exc))
failures.append(name)
except Exception as exc: # noqa: BLE001 - report any unexpected failure
print("ERROR %s: %r" % (name, exc))
failures.append(name)
finally:
shutil.rmtree(workdir, ignore_errors=True)
if failures:
print("FAILED: %s" % ", ".join(failures))
return 1
print("All tests passed.")
return 0
if __name__ == "__main__":
sys.exit(main())