Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,4 +56,5 @@ built with it, or their updates, features, or game content, including early acce
Matthew Stanley has authority from the relevant copyright holders to grant
these permissions for the project's PolyForm-covered code. Third-party
components and code explicitly licensed under other terms retain those terms.
Optional startup checks remove previous run reports before starting the kit, so a recent old report cannot substitute for fresh frames.
Boot filenames with an extra letter after a known territory prefix are normalized consistently. Such names require a selected runtime declaring compatible serial normalization; ordinary names retain older-runtime support.
2 changes: 2 additions & 0 deletions src/pin_builder/workbench_package.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ def kit_exe_start(kit, exe_name, disc, bios, env, output, receipt, name='b4-kit-
receipt.setdefault('commands', []).append(command)
own = output / (name + '-host')
start_env = pb.host_start_environment({key: value for key, value in env.items() if not key.startswith('PSX')}, own) | forced
for folder in (kit, product.parent):
(folder / RUN_REPORT).unlink(missing_ok=True)
began = time.time()
code, text = start_setup_program(executable, kit, start_env, log, KIT_START_SECONDS, arguments=argv[1:])
command['exit_code'] = code
Expand Down
60 changes: 60 additions & 0 deletions tests/test_kit_start_reports.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Kit startup must consume this start's report, never a recent previous one."""
import json
from pathlib import Path
import tempfile
import unittest
from unittest import mock
from pin_builder import pin_build as builder, workbench_package as package


class KitStartReportTests(unittest.TestCase):
def run_start(self, root, in_product, fresh):
kit = root / "kit"
product = kit / "build-release" / "game.exe"
product.parent.mkdir(parents=True)
output = root / "evidence"
output.mkdir()
folder = product.parent if in_product else kit
old = folder / package.RUN_REPORT
old.write_text(json.dumps({"frame": 999, "exit_origin": "exit_after_ms"}), encoding="utf-8")
calls = 0

def processes(_executable):
nonlocal calls
calls += 1
if calls == 1:
return []
if calls == 2:
return [{"pid": 42, "window": 0, "command_line": f'"{product}" --bios "bios.bin" --disc "disc.iso"'}]
if fresh:
old.write_text(json.dumps({"frame": 1234, "exit_origin": "exit_after_ms"}), encoding="utf-8")
return []

def start(_executable, _cwd, _env, log, *_args, **_kwargs):
text = package.HANDOVER_LINE + "\n" + str(product) + "\n"
Path(log).write_text(text, encoding="utf-8")
return 0, text

with mock.patch.object(package, "start_setup_program", side_effect=start), \
mock.patch.object(package, "product_processes", side_effect=processes), \
mock.patch.object(package, "stop_process") as stop:
if fresh:
result = package.kit_exe_start(kit, "game", "disc.iso", "bios.bin", {}, output,
{"commands": []}, stage=2, wait=0.1, poll=0, frames_wait=1)
self.assertEqual(result["frames"], 1234)
else:
with self.assertRaisesRegex(builder.BuildStop, "left no fresh"):
package.kit_exe_start(kit, "game", "disc.iso", "bios.bin", {}, output,
{"commands": []}, stage=2, wait=0.1, poll=0, frames_wait=1)
self.assertFalse(old.exists())
stop.assert_not_called()

def test_recent_previous_reports_are_removed_at_both_locations(self):
for in_product in (False, True):
with self.subTest(in_product=in_product), tempfile.TemporaryDirectory() as temp:
self.run_start(Path(temp), in_product, False)

def test_fresh_report_is_accepted_at_both_locations(self):
for in_product in (False, True):
with self.subTest(in_product=in_product), tempfile.TemporaryDirectory() as temp:
self.run_start(Path(temp), in_product, True)
Loading