mirror of
https://github.com/penpot/penpot.git
synced 2026-10-30 22:40:33 +00:00
`./scripts/ci --build` compiles each module the way production does, without going through `manage.sh build-bundle`, which wipes node_modules and reinstalls dependencies. It runs the release commands already defined per module and reuses existing caches, so a broken release build (shadow-cljs optimizations, the wasm targets, the backend uberjar, tsc/typedoc) shows up before a bundle is built. The task is opt-in and runs last, after lint, test and fmt, so the default flow keeps its duration. `common` has no build of its own and reports as skipped. `media-processor` joins as the ninth module, with its lint skipped and `tsc --noEmit` folded into its build. All build outputs are gitignored. `scripts/test_ci.py` covers the flag wiring and one command per module through `--dry-run`; it never runs a real build. AI-assisted-by: deepseek-v4.1-flash
160 lines
4.6 KiB
Python
160 lines
4.6 KiB
Python
#!/usr/bin/env python3
|
|
"""Tests for scripts/ci.
|
|
|
|
Run with:
|
|
|
|
python3 scripts/test_ci.py
|
|
|
|
Covers the wiring of the orchestrator around the `build` task and the
|
|
`media-processor` module: the flag set, one build command per module, the
|
|
help text, the dry-run mode (which must execute and write nothing), and the
|
|
default task list staying free of build.
|
|
"""
|
|
|
|
import pathlib
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import unittest
|
|
|
|
sys.dont_write_bytecode = True
|
|
|
|
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
|
|
CI = REPO_ROOT / "scripts" / "ci"
|
|
LOG_DIR = REPO_ROOT / ".ci-logs"
|
|
|
|
ANSI = re.compile(r"\x1b\[[0-9;]*m")
|
|
|
|
# Modules with a production build. `common` has no build of its own: its
|
|
# compilation happens in its test task and in the modules consuming it.
|
|
BUILD_MODULES = (
|
|
"frontend",
|
|
"backend",
|
|
"render-wasm",
|
|
"exporter",
|
|
"mcp",
|
|
"plugins",
|
|
"library",
|
|
"media-processor",
|
|
)
|
|
|
|
ALL_MODULES = BUILD_MODULES + ("common",)
|
|
|
|
|
|
def run_ci(*args, cwd=REPO_ROOT):
|
|
"""Run scripts/ci and return (exit_code, output without ANSI escapes)."""
|
|
proc = subprocess.run(
|
|
[str(CI), *args],
|
|
cwd=cwd,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
return proc.returncode, ANSI.sub("", proc.stdout + proc.stderr)
|
|
|
|
|
|
def log_snapshot():
|
|
"""Names and mtimes of every entry under .ci-logs, or None when absent."""
|
|
if not LOG_DIR.exists():
|
|
return None
|
|
return sorted((p.name, p.stat().st_mtime_ns) for p in LOG_DIR.iterdir())
|
|
|
|
|
|
class BuildTaskTests(unittest.TestCase):
|
|
def test_build_all_has_one_command_per_module_without_common(self):
|
|
code, out = run_ci("--build", "--dry-run", "--all")
|
|
|
|
self.assertEqual(code, 0, out)
|
|
self.assertEqual(out.count("build: would run"), len(BUILD_MODULES), out)
|
|
self.assertIn("build: not defined for common, skipping", out)
|
|
for module in BUILD_MODULES:
|
|
self.assertIn(f" in {module}/", out)
|
|
|
|
def test_build_is_not_part_of_the_default_tasks(self):
|
|
code, out = run_ci("--dry-run", "--all")
|
|
|
|
self.assertEqual(code, 0, out)
|
|
self.assertNotIn("build:", out)
|
|
for task in ("lint", "test", "fmt"):
|
|
self.assertIn(f"{task}: would run", out)
|
|
|
|
def test_build_flag_runs_build_only(self):
|
|
code, out = run_ci("--build", "--dry-run", "frontend")
|
|
|
|
self.assertEqual(code, 0, out)
|
|
self.assertIn("build: would run", out)
|
|
for task in ("lint", "test", "fmt"):
|
|
self.assertNotIn(f"{task}:", out)
|
|
|
|
def test_no_build_flag_is_accepted(self):
|
|
code, out = run_ci("--no-build", "--dry-run", "frontend")
|
|
|
|
self.assertEqual(code, 0, out)
|
|
self.assertIn("lint: would run", out)
|
|
self.assertNotIn("build:", out)
|
|
|
|
def test_dry_run_executes_nothing(self):
|
|
before = log_snapshot()
|
|
|
|
run_ci("--build", "--dry-run", "mcp")
|
|
|
|
self.assertEqual(before, log_snapshot())
|
|
|
|
|
|
class MediaProcessorModuleTests(unittest.TestCase):
|
|
def test_module_is_accepted_and_lint_is_skipped(self):
|
|
code, out = run_ci("--dry-run", "media-processor")
|
|
|
|
self.assertEqual(code, 0, out)
|
|
self.assertIn(
|
|
"lint: not defined for media-processor, skipping",
|
|
out,
|
|
)
|
|
for task in ("test", "fmt"):
|
|
self.assertIn(f"{task}: would run", out)
|
|
|
|
def test_module_is_included_in_all(self):
|
|
code, out = run_ci("--dry-run", "--all")
|
|
|
|
self.assertEqual(code, 0, out)
|
|
self.assertIn("media-processor", out)
|
|
self.assertIn("media-processor:test", out)
|
|
|
|
def test_build_runs_types_check_before_esbuild(self):
|
|
code, out = run_ci("--build", "--dry-run", "media-processor")
|
|
|
|
self.assertEqual(code, 0, out)
|
|
self.assertIn(
|
|
"pnpm run types:check && pnpm run build",
|
|
out,
|
|
)
|
|
|
|
|
|
class HelpTests(unittest.TestCase):
|
|
def help_options(self, out):
|
|
"""Option tokens from the OPTIONS section, e.g. '--build'."""
|
|
options = []
|
|
for line in out.splitlines():
|
|
stripped = line.strip()
|
|
if stripped.startswith("--"):
|
|
options.append(stripped.split()[0])
|
|
return options
|
|
|
|
def test_help_documents_the_build_flags(self):
|
|
code, out = run_ci("--help")
|
|
|
|
self.assertEqual(code, 0, out)
|
|
options = self.help_options(out)
|
|
self.assertIn("--build", options)
|
|
self.assertIn("--no-build", options)
|
|
|
|
def test_help_lists_every_module(self):
|
|
code, out = run_ci("--help")
|
|
|
|
self.assertEqual(code, 0, out)
|
|
for module in ALL_MODULES:
|
|
self.assertIn(module, out)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|