Record original-alignment implementation and traceability checkpoint
Capture current source, content data, tests, asset licenses, and original-specification traceability. Original implementation remains incomplete. The accepted matching UI generation passed 154 suites and 12,253 checks, with eight rendered staff-contract screens reviewed. Latest native Windows and full-original acceptance remain outstanding.
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Evidence inventory for P22; file presence never certifies production readiness."""
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
import struct
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
OUT = ROOT / 'docs/plans/original-completion-2026-09-29/execution/assets'
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
def digest(p):
|
||||
return hashlib.sha256(p.read_bytes()).hexdigest()
|
||||
def save(name, data):
|
||||
(OUT/name).write_text(json.dumps(data, ensure_ascii=False, indent=2)+'\n')
|
||||
|
||||
references = {}
|
||||
for folder in ('scripts', 'scenes', 'autoload', 'data', 'assets'):
|
||||
for source in (ROOT/folder).rglob('*'):
|
||||
if source.suffix not in ('.gd', '.tres', '.tscn') or not source.is_file():
|
||||
continue
|
||||
for path in set(re.findall(r'res://([^"\s]+\.(?:png|svg|jpg|jpeg|webp|ogg|wav|mp3|ttf|otf))', source.read_text())):
|
||||
references.setdefault(path, []).append(str(source.relative_to(ROOT)))
|
||||
media = []
|
||||
for p in sorted((ROOT/'assets').rglob('*')):
|
||||
if not p.is_file() or p.suffix.lower() not in ('.png','.svg','.jpg','.jpeg','.webp','.ogg','.wav','.mp3','.ttf','.otf'):
|
||||
continue
|
||||
path = str(p.relative_to(ROOT))
|
||||
row = {'path':path, 'bytes':p.stat().st_size, 'sha256':digest(p), 'literal_references':references.get(path, []), 'acceptance':'not_reviewed', 'provenance':'unverified'}
|
||||
if p.suffix.lower() == '.png':
|
||||
header = p.read_bytes()[:24]
|
||||
if header[:8] == b'\x89PNG\r\n\x1a\n':
|
||||
row['width'],row['height'] = struct.unpack('>II',header[16:24])
|
||||
if path.startswith('assets/mockups/'):
|
||||
row['purpose']='reference_mockup_not_shipping_acceptance'
|
||||
elif path.startswith('assets/kenney/'):
|
||||
pack = p.relative_to(ROOT/'assets/kenney').parts[0]
|
||||
license_path = ROOT/'assets/kenney'/pack/'License.txt'
|
||||
row.update(provenance='Kenney', creator='Kenney', license=str(license_path.relative_to(ROOT)), license_present=license_path.exists())
|
||||
if license_path.exists():
|
||||
row['license_sha256']=digest(license_path)
|
||||
row['license_declares_cc0']='CC0' in license_path.read_text()
|
||||
else:
|
||||
row['purpose']='existing_theme_or_icon_requires_source_and_quality_review'
|
||||
media.append(row)
|
||||
save('media_inventory.json', media)
|
||||
|
||||
facility_mapping = ROOT/'docs/plans/original-completion-2026-09-29/execution/original_facility_mapping.json'
|
||||
facilities = json.loads(facility_mapping.read_text())
|
||||
renderer = ROOT/'scripts/ui/pixel_tower_art.gd'
|
||||
text = renderer.read_text()
|
||||
slots = []
|
||||
for index,f in enumerate(facilities,1):
|
||||
key = f['definition_id'] or f'original_facility_{index:02}'
|
||||
for level in range(1,4):
|
||||
for layer in ('front','back'):
|
||||
for surface in ('exterior','interior'):
|
||||
slots.append({'slot_id':f'{key}.lv{level}.{layer}.{surface}', 'requirement':['B-13.3',f"B-{f['section']}"], 'facility_name':f['name'], 'definition_id':f['definition_id'], 'level':level, 'layer':layer, 'surface':surface, 'original_placement':f['original_layer_zone'], 'creator':'unassigned', 'status':'unverified_required_slot', 'approved_asset':None, 'existing_candidate':str(renderer.relative_to(ROOT)) if f['definition_id'] and ('&"'+f['definition_id']+'"') in text else None, 'note':'Code token is only a search lead; it does not prove a dedicated level/layer/surface variant. These are usage slots, not a committed image count.'})
|
||||
save('facility_usage_slots.json',slots)
|
||||
|
||||
people = [('anri','アンリ'),('fine','フィーネ'),('tomari','トマリ'),('amelia','アメリア'),('liz','リズ'),('mimi','ミミ'),('satsuki','サツキ')]
|
||||
character_slots = []
|
||||
for key,name in people:
|
||||
for kind in ('portrait_base','portrait_expression','portrait_costume','portrait_damage','portrait_dirt','sd_idle','sd_walk','sd_work','sd_collapsed','sd_carry','sd_rescue','sd_rest'):
|
||||
character_slots.append({'slot_id':f'heroine.{key}.{kind}', 'character':name, 'requirement':['B-6.2','B-13.1' if kind.startswith('portrait') else 'B-13.2'], 'kind':kind, 'required_variant_count':1 if kind=='portrait_base' else None, 'creator':'unassigned', 'status':'unverified_required_slot', 'approved_asset':None, 'candidate':'scripts/ui/pixel_people.gd' if kind.startswith('sd_') else None, 'note':'Generic procedural poses do not prove character-specific appearance or role-specific animation.'})
|
||||
save('character_usage_slots.json',character_slots)
|
||||
|
||||
other = [
|
||||
('background.front','B-13.3','scenes/main/world_preview.gd'),('background.back','B-13.3','scenes/main/world_preview.gd'),
|
||||
('ui.front_skin','B-13.4','assets/themes/japanese_fantasy/visual-theme-v1.tres'),('ui.back_skin','B-13.4','assets/themes/japanese_fantasy/visual-theme-v1.tres'),
|
||||
('ui.emotion_graph','B-13.4',None),('ui.camera_overlay','B-13.4',None),
|
||||
('effect.shield','B-13.2',None),('effect.smoke','B-13.2',None),('effect.mana_light','B-13.2',None),
|
||||
('audio.front_bgm','B-13.5',None),('audio.back_arrangement','B-13.5',None),('audio.front_ambience','B-13.5',None),('audio.back_ambience','B-13.5',None),('audio.ui','B-13.5',None),('audio.character_voice','B-13.5',None),
|
||||
('other_characters','B-13.1',None),('restricted_content_unproduced','B-13.1',None)]
|
||||
save('other_usage_slots.json',[{'slot_id':k,'requirement':(['B-12.3','B-13.1','B-13.2','B-13.5'] if k=='restricted_content_unproduced' else [req]),'candidate':candidate,'status':'not_produced_outside_current_content_scope' if k=='restricted_content_unproduced' else 'unverified_required_slot','creator':'unassigned','required_variant_count':None,'approved_asset':None} for k,req,candidate in other])
|
||||
summary = {'facility_mapping':str(facility_mapping.relative_to(ROOT)), 'facility_mapping_sha256':digest(facility_mapping), 'baseline_sha256':digest(ROOT/'docs/traceability/original/BASELINE.txt'), 'media_files':len(media), 'literal_referenced_media_files':sum(bool(x['literal_references']) for x in media), 'audio_files':sum(Path(x['path']).suffix in ('.wav','.ogg','.mp3') for x in media), 'facility_count':len(facilities), 'facility_usage_slots':len(slots), 'named_heroines':len(people), 'character_usage_slot_groups':len(character_slots), 'other_slot_groups':len(other), 'approved_assets':0, 'missing_literal_references':[p for p in references if not (ROOT/p).exists()], 'scope':'Inventory and candidate mappings only. No asset is promoted to complete by file existence, procedural fallback, or a passing gameplay test. Variant counts and production ownership remain unresolved.'}
|
||||
save('inventory_summary.json',summary)
|
||||
print(json.dumps(summary,ensure_ascii=False))
|
||||
@@ -0,0 +1,23 @@
|
||||
extends SceneTree
|
||||
func _initialize() -> void: call_deferred("run")
|
||||
func run() -> void:
|
||||
root.get_node("GameClock").set_paused(true)
|
||||
var runtime: Variant = root.get_node("SimulationRuntime")
|
||||
var original: Array = JSON.parse_string(FileAccess.get_file_as_string("res://docs/plans/original-completion-2026-09-29/execution/original_facility_mapping.json"))
|
||||
var palette: Array = load("res://scenes/main/main.gd").MVP_FACILITY_IDS
|
||||
var rows: Array = []
|
||||
for raw: Dictionary in original:
|
||||
var id := StringName(raw["definition_id"] if raw["definition_id"] != null else "")
|
||||
var definition: FacilityDefinition = FacilityCatalog.by_id(id) if not id.is_empty() else null
|
||||
var row := {"section":raw["section"], "name":raw["name"], "definition_id":String(id), "original_size":raw["original_size"], "original_layer_zone":raw["original_layer_zone"], "runtime_registered":runtime.definition_registry.has_facility(id), "palette_listed":id in palette, "definition":null}
|
||||
if definition != null:
|
||||
row["definition"] = {"path":definition.resource_path, "display_name":definition.display_name, "footprint":[definition.footprint.x,definition.footprint.y], "layer":FacilityDefinition.ViewLayer.keys()[definition.layer], "zone":FacilityDefinition.MapZone.keys()[definition.zone], "customer_capacity":definition.customer_capacity, "capacity":definition.capacity, "staff_min":definition.staff_min,"staff_max":definition.staff_max, "required_roles":definition.required_roles, "service_attribute":String(definition.service_attribute), "maintenance_gold":definition.maintenance_gold, "faith_per_service":definition.faith_per_service}
|
||||
rows.append(row)
|
||||
var output := OS.get_environment("THE_TREE_FACILITY_AUDIT_PATH")
|
||||
if output.is_empty():
|
||||
push_error("THE_TREE_FACILITY_AUDIT_PATH is required"); quit(2); return
|
||||
var file := FileAccess.open(output,FileAccess.WRITE)
|
||||
if file == null:
|
||||
push_error("Cannot write audit output"); quit(2); return
|
||||
file.store_string(JSON.stringify({"normal_registry_ids":runtime.definition_registry.get_facility_ids(),"palette_ids":palette,"original_facilities":rows},"\t")+"\n");file.close()
|
||||
print("Exported facility audit rows (not acceptance PASS): ", rows.size());quit(0)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b5uppfknx478a
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Run every source test, retain logs and continue past failures.
|
||||
|
||||
The exported-package test needs a built PCK and runs via make test-exported-opening.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--output', type=Path, default=Path('artifacts/all-tests'))
|
||||
parser.add_argument('--godot', default='godot')
|
||||
parser.add_argument('--timeout', type=int, default=1200)
|
||||
args = parser.parse_args()
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
output = args.output.resolve()
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
paths = [root / 'tests/run_tests.gd'] + sorted((root / 'tests').rglob('test*.gd'))
|
||||
results = []
|
||||
for path in paths:
|
||||
relative = path.relative_to(root)
|
||||
if 'export' in relative.parts or relative.as_posix() == 'tests/s0/test_kernel_and_save.gd':
|
||||
# The RefCounted kernel module is already invoked by run_tests.gd.
|
||||
continue
|
||||
start = time.monotonic()
|
||||
name = str(relative).replace('/', '__') + '.log'
|
||||
# Stream to disk so long-running scenarios expose progress immediately.
|
||||
with (output / name).open('w') as log:
|
||||
try:
|
||||
run = subprocess.run(
|
||||
[args.godot, '--headless', '--path', str(root), '--script', 'res://' + str(relative)],
|
||||
cwd=root, stdout=log, stderr=subprocess.STDOUT, timeout=args.timeout,
|
||||
)
|
||||
code = run.returncode
|
||||
except subprocess.TimeoutExpired:
|
||||
code = 124
|
||||
content = (output / name).read_text()
|
||||
failed = code != 0 or 'SCRIPT ERROR:' in content or '\nERROR:' in content
|
||||
results.append(dict(test=str(relative), exit_code=code, failed=failed,
|
||||
seconds=round(time.monotonic() - start, 2), log=name))
|
||||
(output / 'all-tests.json').write_text(json.dumps(results, indent=2) + '\n')
|
||||
print(f'{"FAIL" if failed else "PASS"} {relative}', flush=True)
|
||||
failed = sum(result['failed'] for result in results)
|
||||
print(f'{len(results)} tests, {failed} failures; logs: {output}')
|
||||
return 1 if failed else 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Capture current registration evidence; successful export is not acceptance."""
|
||||
import argparse
|
||||
from collections import Counter
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
MAPPING = 'docs/plans/original-completion-2026-09-29/execution/original_facility_mapping.json'
|
||||
BASELINE = 'docs/plans/original-completion-2026-09-29/execution/baseline-fetched-2026-09-29.txt'
|
||||
|
||||
def manifest():
|
||||
paths = [ROOT / MAPPING, ROOT / BASELINE, ROOT / 'tools/capture_facility_audit.gd', Path(__file__)]
|
||||
for directory in ('autoload', 'scripts', 'scenes', 'data'):
|
||||
paths.extend(p for p in (ROOT / directory).rglob('*') if p.suffix in ('.gd', '.tres', '.tscn', '.json'))
|
||||
return {str(p.relative_to(ROOT)): hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted(set(paths))}
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--godot', default='godot')
|
||||
parser.add_argument('--output', required=True)
|
||||
args = parser.parse_args()
|
||||
out = Path(args.output).resolve()
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
def save(name, value):
|
||||
(out / name).write_text(json.dumps(value, ensure_ascii=False, indent=2) + '\n')
|
||||
before = manifest()
|
||||
save('source-start.json', before)
|
||||
env = dict(os.environ, THE_TREE_FACILITY_AUDIT_PATH=str(out / 'runtime-facilities.json'))
|
||||
with (out / 'export.log').open('w') as log:
|
||||
result = subprocess.run([args.godot, '--headless', '--path', str(ROOT), '--script', 'res://tools/capture_facility_audit.gd'], env=env, stdout=log, stderr=subprocess.STDOUT, timeout=120)
|
||||
after = manifest()
|
||||
changes = [p for p in sorted(before.keys() | after.keys()) if before.get(p) != after.get(p)]
|
||||
save('source-end.json', after)
|
||||
save('source-change.json', changes)
|
||||
if result.returncode or changes:
|
||||
raise SystemExit('Audit capture failed or source changed; inspect logs.')
|
||||
data = json.loads((out / 'runtime-facilities.json').read_text())
|
||||
mapping = json.loads((ROOT / MAPPING).read_text())
|
||||
rows = data['original_facilities']
|
||||
if len(rows) != 30 or len({r['name'] for r in rows}) != 30 or [r['name'] for r in rows] != [r['name'] for r in mapping]:
|
||||
raise SystemExit('Original mapping coverage is invalid.')
|
||||
counts = Counter()
|
||||
mismatches = []
|
||||
for row in rows:
|
||||
definition = row['definition']
|
||||
row['registration_status'] = ('normal' if row['runtime_registered'] and row['palette_listed'] else 'definition_only' if definition else 'missing')
|
||||
counts[row['registration_status']] += 1
|
||||
row['full_acceptance'] = 'unproven'
|
||||
if definition:
|
||||
expected = [int(x) for x in row['original_size'].split('x')]
|
||||
layer = re.match(r'FRONT|BACK', row['original_layer_zone']).group()
|
||||
if definition['footprint'] != expected or definition['layer'] != layer:
|
||||
mismatches.append(row['name'])
|
||||
extra = sorted(set(data['normal_registry_ids']) - {r['definition_id'] for r in rows})
|
||||
unspecified = [r['name'] for r in rows if r['definition'] and r['definition']['customer_capacity'] < 0]
|
||||
summary = dict(original_count=30, registration_counts=dict(counts), normal_catalog_count=len(data['normal_registry_ids']), non_original_registry_ids=extra,
|
||||
dimension_or_layer_mismatches=mismatches, customer_limit_unset=unspecified,
|
||||
all_original_acceptance=False, scope='Registration and loaded definition values only. Not operation, save migration, assets, Windows or whole-spec acceptance.')
|
||||
save('summary.json', summary)
|
||||
save('classified-facilities.json', rows)
|
||||
labels = {'normal': '通常登録・パレット掲載', 'definition_only': '定義のみ', 'missing': '対応定義なし'}
|
||||
lines = ['# 原本30施設・現行登録監査', '',
|
||||
f"通常登録 {counts['normal']}、定義のみ {counts['definition_only']}、対応定義なし {counts['missing']}。通常登録全{len(data['normal_registry_ids'])}件のうち原本30施設以外は{len(extra)}件。",
|
||||
'', '登録状態と読込済み定義の監査。実営業・保存移行・完成資産・Windows・全要件の受入完了は証明しない。', '',
|
||||
f'対応定義の寸法・層の不一致: {len(mismatches)}件。人数専用上限が未設定: {len(unspecified)}件。capacityから人数値を転記していない。高層・背面・騒音・サービス条件は別途受入が必要。', '',
|
||||
'| 原本 | 施設 | ID | 登録 | 寸法 / 層 / ゾーン | 顧客人数上限 |', '|---|---|---|---|---|---|']
|
||||
for r in rows:
|
||||
d = r['definition']
|
||||
geometry = f"{d['footprint'][0]}×{d['footprint'][1]} / {d['layer']} / {d['zone']}" if d else '未定義'
|
||||
limit = str(d['customer_capacity']) if d and d['customer_capacity'] >= 0 else '未設定'
|
||||
lines.append(f"| {r['section']} | {r['name']} | {r['definition_id'] or '—'} | {labels[r['registration_status']]} | {geometry} | {limit} |")
|
||||
lines += ['', '## 未接続施設', '', '対応定義なし: ' + '、'.join(r['name'] for r in rows if r['registration_status']=='missing') + '。', '',
|
||||
'定義のみ: ' + '、'.join(r['name'] for r in rows if r['registration_status']=='definition_only') + '。', '',
|
||||
'## 次の受入', '', '未定義施設は寸法・層だけでなく経済値、人数、業務、通常操作、保存を接続する。登録済み施設は未設定人数とサービス属性、営業時間・停止・経路・客/職員行動を個別検証する。施設追加のたびにこの監査を再実行する。', '',
|
||||
'再現: `python3 tools/run_facility_audit.py --godot /opt/homebrew/bin/godot --output artifacts/facility-audit-current-2026-09-29`。原本SHA・入力ソースSHA・取得中の変更なしは同ディレクトリのsource-start/end/change.jsonを参照。', '']
|
||||
(out / 'REPORT.md').write_text('\n'.join(lines))
|
||||
print(json.dumps(summary, ensure_ascii=False))
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,50 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Debug a naturally opened inn through three closes and an actual process restart."""
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import time
|
||||
import uuid
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--godot', default='godot')
|
||||
parser.add_argument('--rendered', action='store_true')
|
||||
parser.add_argument('--output', type=Path, default=Path('artifacts/opening-restart-flow'))
|
||||
args = parser.parse_args()
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
output = args.output.resolve()
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
env = dict(os.environ, THE_TREE_OPENING_RESTART_SLOT=f'user://tests/opening_restart_{uuid.uuid4().hex}/manual', THE_TREE_OPENING_RESTART_OUTPUT=str(output))
|
||||
env.pop('THE_TREE_OPENING_CAPTURE', None)
|
||||
command = [args.godot] + ([] if args.rendered else ['--headless']) + ['--path', str(root), '--script', 'res://tests/ui/fixtures/opening_restart_flow.gd']
|
||||
source_paths = [p for directory in ('autoload', 'scripts', 'scenes', 'data', 'tests') for p in (root/directory).rglob('*') if p.suffix in ('.gd', '.tres', '.tscn', '.json')]
|
||||
hashes = {str(p.relative_to(root)): hashlib.sha256(p.read_bytes()).hexdigest() for p in sorted(source_paths)}
|
||||
(output/'source-start.json').write_text(json.dumps(hashes, indent=2)+'\n')
|
||||
results = []
|
||||
for phase in ('write', 'read'):
|
||||
started = time.monotonic()
|
||||
log_path = output/f'{phase}.log'
|
||||
with log_path.open('w') as log:
|
||||
try:
|
||||
code = subprocess.run(command + (['--', '--read'] if phase == 'read' else []), cwd=root, env=env, stdout=log, stderr=subprocess.STDOUT, timeout=1200).returncode
|
||||
except subprocess.TimeoutExpired:
|
||||
code = 124
|
||||
content = log_path.read_text()
|
||||
passed = code == 0 and '0 failures' in content and 'ERROR:' not in content
|
||||
results.append(dict(phase=phase, exit_code=code, passed=passed, seconds=round(time.monotonic()-started, 2), log=log_path.name))
|
||||
(output/'result.json').write_text(json.dumps(results, indent=2)+'\n')
|
||||
print(json.dumps(results[-1]), flush=True)
|
||||
if not passed:
|
||||
return 1
|
||||
changed = [p for p,h in hashes.items() if not (root/p).exists() or hashlib.sha256((root/p).read_bytes()).hexdigest()!=h]
|
||||
(output/'source-change.json').write_text(json.dumps(changed, indent=2)+'\n')
|
||||
return 1 if changed else 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,37 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Verify closed-facility upkeep across two independent Godot processes."""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import time
|
||||
import uuid
|
||||
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument('--godot', default='godot')
|
||||
parser.add_argument('--rendered', action='store_true')
|
||||
parser.add_argument('--output', type=Path, default=Path('artifacts/upkeep-ui-flow'))
|
||||
args = parser.parse_args()
|
||||
root = Path(__file__).resolve().parents[1]
|
||||
output = args.output.resolve()
|
||||
output.mkdir(parents=True, exist_ok=True)
|
||||
env = os.environ.copy()
|
||||
env['THE_TREE_UPKEEP_UI_SAVE'] = f'user://tests/upkeep_ui_{uuid.uuid4().hex}/manual'
|
||||
base = [args.godot] + ([] if args.rendered else ['--headless']) + ['--path', str(root), '--script', 'res://tests/ui/test_upkeep_closed_ui.gd']
|
||||
results = []
|
||||
for phase in ('write', 'read'):
|
||||
start = time.monotonic()
|
||||
log_path = output / f'{phase}.log'
|
||||
with log_path.open('w') as log:
|
||||
try:
|
||||
code = subprocess.run(base + (['--', '--read'] if phase == 'read' else []), cwd=root, env=env, stdout=log, stderr=subprocess.STDOUT, timeout=600).returncode
|
||||
except subprocess.TimeoutExpired:
|
||||
code = 124
|
||||
content = log_path.read_text()
|
||||
passed = code == 0 and 'PASS:' in content and 'ERROR:' not in content
|
||||
results.append({'phase':phase, 'exit_code':code, 'passed':passed, 'seconds':round(time.monotonic()-start,2), 'log':str(log_path)})
|
||||
(output/'result.json').write_text(json.dumps(results, indent=2)+'\n')
|
||||
print(json.dumps(results[-1]), flush=True)
|
||||
if not passed:
|
||||
raise SystemExit(1)
|
||||
Reference in New Issue
Block a user