mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-14 19:02:22 +02:00
171 lines
8.1 KiB
Python
171 lines
8.1 KiB
Python
"""Exercise publication failure modes without changing the real static tree."""
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from tests.helpers.cli_loader import load_script
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
build = load_script("build_frontend.py")
|
|
|
|
|
|
def project(tmp_path, entries=None):
|
|
source = tmp_path / "frontend/src/shared/helper.js"
|
|
source.parent.mkdir(parents=True)
|
|
source.write_bytes(b"export const value = 1;\n")
|
|
manifest = {"version": 1, "modules": entries if entries is not None else [
|
|
{"source": "shared/helper.js", "deployment": "js/helper.js"}
|
|
]}
|
|
(tmp_path / "frontend/modules.json").write_text(json.dumps(manifest), encoding="utf-8")
|
|
(tmp_path / ".gitattributes").write_text("* text=auto\nfrontend/src/** text eol=lf\nstatic/js/helper.js text eol=lf\n", encoding="utf-8")
|
|
return source, tmp_path / "static/js/helper.js"
|
|
|
|
|
|
def test_check_detects_missing_and_stale_output_without_writing(tmp_path):
|
|
source, output = project(tmp_path)
|
|
assert build.synchronize(tmp_path, check=True) == ["static/js/helper.js"]
|
|
assert not output.exists()
|
|
assert build.synchronize(tmp_path) == ["static/js/helper.js"]
|
|
assert output.read_bytes() == source.read_bytes()
|
|
source.write_bytes(b"export const value = 2;\n")
|
|
assert build.synchronize(tmp_path, check=True) == ["static/js/helper.js"]
|
|
assert output.read_bytes() == b"export const value = 1;\n"
|
|
|
|
|
|
def test_publication_is_idempotent_and_preserves_unowned_assets(tmp_path):
|
|
source, output = project(tmp_path)
|
|
unowned = tmp_path / "static/js/notes.js"
|
|
unowned.parent.mkdir(parents=True)
|
|
unowned.write_bytes(b"legacy state owner\n")
|
|
build.synchronize(tmp_path)
|
|
timestamp = output.stat().st_mtime_ns
|
|
assert build.synchronize(tmp_path) == []
|
|
assert output.stat().st_mtime_ns == timestamp
|
|
assert unowned.read_bytes() == b"legacy state owner\n"
|
|
assert output.read_bytes() == source.read_bytes()
|
|
|
|
|
|
@pytest.mark.parametrize("bad", ["../outside.js", "/absolute.js", "shared/../helper.js", "shared\\helper.js", "shared/helper.js?v=1", "shared/helper.js#fragment", "shared/helper.ts"])
|
|
def test_invalid_plan_does_not_partially_publish(tmp_path, bad):
|
|
project(tmp_path, [
|
|
{"source": "shared/helper.js", "deployment": "js/helper.js"},
|
|
{"source": bad, "deployment": "js/other.js"},
|
|
])
|
|
with pytest.raises(ValueError, match="invalid module path"):
|
|
build.synchronize(tmp_path)
|
|
assert not (tmp_path / "static").exists()
|
|
|
|
|
|
def test_duplicate_outputs_and_unmapped_sources_are_rejected(tmp_path):
|
|
source, output = project(tmp_path)
|
|
extra = source.with_name("extra.js")
|
|
extra.write_bytes(b"export const value = 2;\n")
|
|
with pytest.raises(ValueError, match="missing from manifest"):
|
|
build.synchronize(tmp_path)
|
|
manifest_path = tmp_path / "frontend/modules.json"
|
|
manifest = json.loads(manifest_path.read_text())
|
|
manifest["modules"].append({"source": "shared/extra.js", "deployment": "js/helper.js"})
|
|
manifest_path.write_text(json.dumps(manifest))
|
|
with pytest.raises(ValueError, match="must be unique"):
|
|
build.synchronize(tmp_path)
|
|
assert not output.exists()
|
|
|
|
|
|
def test_symlink_output_cannot_overwrite_another_file(tmp_path):
|
|
_, output = project(tmp_path)
|
|
outside = tmp_path / "outside.js"
|
|
outside.write_bytes(b"untouched\n")
|
|
output.parent.mkdir(parents=True)
|
|
output.symlink_to(outside)
|
|
with pytest.raises(ValueError, match="escapes|symlink"):
|
|
build.synchronize(tmp_path)
|
|
assert outside.read_bytes() == b"untouched\n"
|
|
|
|
|
|
def test_real_checkout_check_works_from_another_directory(tmp_path):
|
|
result = subprocess.run([sys.executable, str(ROOT / "scripts/build_frontend.py"), "--check"], cwd=tmp_path, capture_output=True, text=True, timeout=20)
|
|
assert result.returncode == 0, result.stdout + result.stderr
|
|
assert (ROOT / "frontend/src/shared/actionMenuOrder.js").read_bytes() == (ROOT / "static/js/actionMenuOrder.js").read_bytes()
|
|
|
|
|
|
def test_isolated_python_distribution_runs_without_source_or_node():
|
|
result = subprocess.run([sys.executable, str(ROOT / "scripts/verify_frontend_runtime.py")], cwd=ROOT, capture_output=True, text=True, timeout=90)
|
|
assert result.returncode == 0, result.stdout + result.stderr
|
|
assert "PASS: isolated Python lifespan" in result.stdout
|
|
|
|
|
|
@pytest.mark.parametrize("check", [False, True])
|
|
@pytest.mark.parametrize("path,rule", [
|
|
("frontend/src/shared/helper.js", "text"),
|
|
("static/js/helper.js", "text"),
|
|
("frontend/src/shared/helper.js", "text eol=crlf"),
|
|
("static/js/helper.js", "text=auto eol=lf"),
|
|
("static/js/helper.js", "-text eol=lf"),
|
|
])
|
|
def test_each_manifest_path_requires_lf_even_when_bytes_already_match(tmp_path, check, path, rule):
|
|
source, output = project(tmp_path)
|
|
output.parent.mkdir(parents=True)
|
|
output.write_bytes(source.read_bytes())
|
|
other = "static/js/helper.js" if path.startswith("frontend/") else "frontend/src/shared/helper.js"
|
|
(tmp_path / ".gitattributes").write_text(f"* text=auto\n{other} text eol=lf\n{path} {rule}\n", encoding="utf-8")
|
|
with pytest.raises(ValueError, match="require repository-owned.*" + path):
|
|
build.synchronize(tmp_path, check=check)
|
|
assert output.read_bytes() == source.read_bytes()
|
|
|
|
|
|
def test_future_manifest_pair_cannot_publish_without_output_attributes(tmp_path):
|
|
source, output = project(tmp_path)
|
|
extra = source.with_name("future.js")
|
|
extra.write_bytes(b"export const future = true;\n")
|
|
manifest_file = tmp_path / "frontend/modules.json"
|
|
manifest = json.loads(manifest_file.read_text())
|
|
manifest["modules"].append({"source": "shared/future.js", "deployment": "js/future.js"})
|
|
manifest_file.write_text(json.dumps(manifest))
|
|
with pytest.raises(ValueError, match="static/js/future.js"):
|
|
build.synchronize(tmp_path)
|
|
assert not output.exists()
|
|
assert not (tmp_path / "static/js/future.js").exists()
|
|
with (tmp_path / ".gitattributes").open("a", encoding="utf-8") as attributes:
|
|
attributes.write("static/js/future.js text eol=lf\n")
|
|
assert build.synchronize(tmp_path) == ["static/js/helper.js", "static/js/future.js"]
|
|
assert build.synchronize(tmp_path, check=True) == []
|
|
|
|
|
|
def test_nested_git_attribute_rules_and_paths_with_spaces_are_supported(tmp_path):
|
|
source, _ = project(tmp_path, [{"source": "shared/helper.js", "deployment": "js/feature space/helper.js"}])
|
|
attributes = tmp_path / "static/js/feature space/.gitattributes"
|
|
attributes.parent.mkdir(parents=True)
|
|
attributes.write_text("*.js text eol=lf\n", encoding="utf-8")
|
|
assert build.synchronize(tmp_path) == ["static/js/feature space/helper.js"]
|
|
assert (attributes.parent / "helper.js").read_bytes() == source.read_bytes()
|
|
attributes.write_text("*.js text eol=crlf\n", encoding="utf-8")
|
|
with pytest.raises(ValueError, match="eol=crlf"):
|
|
build.synchronize(tmp_path, check=True)
|
|
|
|
|
|
def test_global_and_local_git_settings_cannot_mask_missing_repository_rule(tmp_path, monkeypatch):
|
|
project(tmp_path)
|
|
(tmp_path / ".gitattributes").write_text("frontend/src/** text eol=lf\n", encoding="utf-8")
|
|
global_attributes = tmp_path / "global-attributes"
|
|
global_attributes.write_text("* text eol=lf\n", encoding="utf-8")
|
|
config = tmp_path / "global-config"
|
|
config.write_text(f'[core]\n attributesFile = "{global_attributes.as_posix()}"\n autocrlf = true\n', encoding="utf-8")
|
|
subprocess.run(["git", "init", "--quiet", str(tmp_path)], check=True, capture_output=True)
|
|
(tmp_path / ".git/info/attributes").write_text("* text eol=lf\n", encoding="utf-8")
|
|
monkeypatch.setenv("GIT_CONFIG_GLOBAL", str(config))
|
|
monkeypatch.setenv("GIT_DIR", str(tmp_path / ".git"))
|
|
monkeypatch.setenv("GIT_WORK_TREE", str(tmp_path))
|
|
with pytest.raises(ValueError, match="static/js/helper.js"):
|
|
build.synchronize(tmp_path, check=True)
|
|
|
|
|
|
def test_attribute_enforcement_fails_clearly_when_development_git_is_missing(tmp_path, monkeypatch):
|
|
_, output = project(tmp_path)
|
|
monkeypatch.setattr(build.shutil, "which", lambda name: None)
|
|
with pytest.raises(ValueError, match="Git is required for frontend publication"):
|
|
build.synchronize(tmp_path)
|
|
assert not output.exists()
|