P23-03: 生活の観察
This commit is contained in:
@@ -15,6 +15,7 @@ from pathlib import Path
|
||||
import re
|
||||
import sys
|
||||
import struct
|
||||
import subprocess
|
||||
import zlib
|
||||
|
||||
sys.dont_write_bytecode = True
|
||||
@@ -315,7 +316,7 @@ def drawing_rectangle_rows(report):
|
||||
expected.append((category or ('staff' if a['staff'] else 'guest'), a['key'],
|
||||
a['at'] + [16*a['scale'], 18*a['scale']], people, None))
|
||||
def badge(category, identifier, b):
|
||||
expected.append((category, identifier, b['at'] + [None, 14], world, b['text']))
|
||||
expected.append((category, identifier, badge_rectangle(b['at'], b['text']), world, b['text']))
|
||||
for fid, room in report['rooms'].items():
|
||||
if 'rect' in room:
|
||||
expected.append(('room', fid, room['rect'], tower, None))
|
||||
@@ -419,7 +420,7 @@ def validate_drawing_rows(report, audit, root=ROOT, state=None, rest_audit=None)
|
||||
if text is not None:
|
||||
require(row['text'] == text and type(row['font_width']) in (int, float)
|
||||
and math.isfinite(row['font_width']) and row['font_width'] > 0, 'offline font/text')
|
||||
rect[2] = row['font_width'] + 6
|
||||
require(row['font_width'] <= rect[2] - 6, 'badge text exceeds reproducible rectangle')
|
||||
close(row['rect'], rect, 'offline report drawing rectangle')
|
||||
colors = list(dict.fromkeys(re.findall(r'Color\("([0-9a-fA-F]{6})"', (root/source).read_text())))
|
||||
require(row['style_colors'] == colors and colors, 'offline source palette provenance')
|
||||
@@ -796,6 +797,56 @@ class Transform:
|
||||
return [x, y, max(r[0] + r[2] for r in rects) - x, max(r[1] + r[3] for r in rects) - y]
|
||||
|
||||
|
||||
def badge_rectangle(at, text):
|
||||
return list(at) + [len(text) * 10 + 6, 14]
|
||||
|
||||
|
||||
def intersects(a, b):
|
||||
cut = rect_intersection(a, b)
|
||||
return cut[2] > 0 and cut[3] > 0
|
||||
|
||||
|
||||
def validate_cabin_intrusion(report):
|
||||
for cabin in report['cabins']:
|
||||
require(len(cabin['passengers']) <= cabin['passenger_count'], 'passenger drawn in empty cabin')
|
||||
for actor in report.get('actor_drawings', []):
|
||||
require(not intersects(actor_identity_core(actor), cabin['rect']),
|
||||
'non-passenger inside cabin: ' + actor['key'])
|
||||
|
||||
|
||||
def traffic_obstacles(report):
|
||||
obstacles = []
|
||||
for cabin in report['cabins']:
|
||||
rect = badge_rectangle(cabin['badge_at'], cabin['badge'])
|
||||
close(cabin['badge_rect'], rect, 'cabin badge rectangle')
|
||||
obstacles.extend([cabin['rect'], rect])
|
||||
for collection in ('badges', 'landing_badges', 'actor_aggregations'):
|
||||
obstacles.extend(badge_rectangle(b['at'], b['text']) for b in report.get(collection, []))
|
||||
return obstacles
|
||||
|
||||
|
||||
def traffic_segments(band, cell, obstacles):
|
||||
# Independent rectangle-set subtraction, including the deterministic fallback
|
||||
# for a band entirely behind a cabin. Never accept self-asserted no-overlap.
|
||||
for y in (band[1], cell[1], cell[1] + cell[3] - band[3]):
|
||||
pieces = [[band[0], y, band[2], band[3]]]
|
||||
for obstacle in obstacles:
|
||||
remaining = []
|
||||
for x, top, w, h in pieces:
|
||||
cut = rect_intersection([x, top, w, h], obstacle)
|
||||
if not cut[2] or not cut[3]:
|
||||
remaining.append([x, top, w, h])
|
||||
continue
|
||||
left, upper, cw, ch = cut
|
||||
remaining.extend(r for r in ([x, top, w, upper-top],
|
||||
[x, upper+ch, w, top+h-upper-ch], [x, upper, left-x, ch],
|
||||
[left+cw, upper, x+w-left-cw, ch]) if r[2] > 0 and r[3] > 0)
|
||||
pieces = remaining
|
||||
if pieces:
|
||||
return pieces
|
||||
return []
|
||||
|
||||
|
||||
def compare_traffic(snapshot, report):
|
||||
runtime = snapshot['systems']['runtime']
|
||||
transform = Transform(runtime, report)
|
||||
@@ -810,7 +861,8 @@ def compare_traffic(snapshot, report):
|
||||
band = [rect[0] + rect[2] * .12, y + h - 2 * (w / 16) - thickness / 2,
|
||||
rect[2] * .76, thickness]
|
||||
rows.append(dict(cell=cell, traffic={k: v for k, v in saved.items() if k != 'cell'},
|
||||
rect=rect, band_rect=band))
|
||||
rect=rect, band_rect=band,
|
||||
segment_rects=traffic_segments(band, rect, traffic_obstacles(report))))
|
||||
close(report['traffic'], rows, 'traffic and cell rect')
|
||||
validate_life_overlay(report, snapshot=snapshot)
|
||||
|
||||
@@ -821,7 +873,7 @@ def validate_life_overlay(report, root=ROOT, snapshot=None):
|
||||
require(type(serial) is int and serial > 0
|
||||
and type(report.get('overlay_consumed_serial')) is int
|
||||
and report['overlay_consumed_serial'] == serial, 'stale LifeOverlay serial')
|
||||
require(report.get('overlay_passes') == ['traffic', 'actors'], 'LifeOverlay pass order')
|
||||
require(report.get('overlay_passes') == ['traffic', 'cabins', 'actors'], 'LifeOverlay pass order')
|
||||
indices = report.get('overlay_child_indices', [])
|
||||
require(len(indices) == 3 and all(type(i) is int and i >= 0 for i in indices)
|
||||
and indices[0] < indices[1] < indices[2], 'LifeOverlay child order')
|
||||
@@ -839,8 +891,18 @@ def validate_life_overlay(report, root=ROOT, snapshot=None):
|
||||
require('_draw_congestion_overlay(' not in draw and '_draw_actors(' not in draw
|
||||
and '_draw_observed_actor(' not in body('_draw_elevators'), 'actor or band drawn in parent')
|
||||
require(0 <= overlay.find('_draw_congestion_overlay(_life_overlay)')
|
||||
< overlay.find('_draw_elevators(_life_overlay)')
|
||||
< overlay.find('_draw_life_badges(_life_overlay)')
|
||||
< overlay.find('_draw_observed_actor(actor, _life_overlay)')
|
||||
< overlay.find('_draw_actors(_life_overlay)'), 'LifeOverlay source pass order')
|
||||
require('canvas.draw_rect(rect, Color("34413b"))' in body('_draw_readable_badge')
|
||||
and 'canvas.draw_rect(row[&"band_rect"], Color(color, 0.90))' in body('_draw_congestion_overlay'),
|
||||
'occluded traffic requires opaque later badges and an actual band draw')
|
||||
require('text.length() * 10 + 6' in body('_readable_badge_rect')
|
||||
and '_readable_badge_rect(at, text)' in body('_draw_readable_badge')
|
||||
and 'PixelArt.elevator_cabin(canvas,' in body('_draw_elevators')
|
||||
and 'PixelArt.elevator_gate(canvas,' in body('_draw_elevators')
|
||||
and '_draw_elevators(' not in draw, 'cabin/badge draw geometry or active-layer canvas differs')
|
||||
actors = list(report['actor_drawings']) + [a for c in report['cabins'] for a in c['passengers']]
|
||||
# Projection authority comes from raw state, never riding/region flags.
|
||||
cabin_actors, carried_keys = set(), set()
|
||||
@@ -861,23 +923,34 @@ def validate_life_overlay(report, root=ROOT, snapshot=None):
|
||||
if carried_keys:
|
||||
# Reuse the original raw binding, track and raised-region fit checks.
|
||||
compare_actor_geometry(snapshot, report)
|
||||
obstacles = traffic_obstacles(report)
|
||||
close(report['traffic_obstacles'], obstacles, 'traffic obstacle geometry')
|
||||
validate_cabin_intrusion(report)
|
||||
for row in report.get('traffic', []):
|
||||
x, y, w, h = row['rect']
|
||||
thickness = max(2, min(5, h * .16))
|
||||
expected = [x + w * .12, y + h + 2 - 2 * ((w + 4) / 16) - thickness / 2,
|
||||
w * .76, thickness]
|
||||
close(row['band_rect'], expected, 'traffic band rectangle')
|
||||
segments = traffic_segments(expected, row['rect'], obstacles)
|
||||
close(row['segment_rects'], segments, 'traffic segment geometry')
|
||||
require(all(not intersects(part, obstacle) for part in segments for obstacle in obstacles),
|
||||
'traffic intersects cabin or badge')
|
||||
if row['traffic']['current_density'] <= 0 and not row['traffic']['temporary_block']:
|
||||
continue
|
||||
intervals = []
|
||||
for actor in actors:
|
||||
core = actor_identity_core(actor)
|
||||
intersects = (core[0] < expected[0] + expected[2] and expected[0] < core[0] + core[2]
|
||||
overlaps = (core[0] < expected[0] + expected[2] and expected[0] < core[0] + core[2]
|
||||
and core[1] < expected[1] + expected[3] and expected[1] < core[1] + core[3])
|
||||
require(not intersects or id(actor) in cabin_actors
|
||||
require(not overlaps or id(actor) in cabin_actors
|
||||
or (actor in report['actor_drawings'] and actor['key'] in carried_keys),
|
||||
'floor actor core over traffic band: ' + actor['key'])
|
||||
if intersects:
|
||||
# The relocated fragments also cannot cross an ordinary floor core.
|
||||
require(not any(intersects(core, part) for part in segments)
|
||||
or id(actor) in cabin_actors or actor['key'] in carried_keys,
|
||||
'floor actor core over traffic segment: ' + actor['key'])
|
||||
if overlaps:
|
||||
intervals.append((max(core[0], expected[0]),
|
||||
min(core[0] + core[2], expected[0] + expected[2])))
|
||||
end = expected[0]
|
||||
@@ -915,7 +988,11 @@ def compare_queues(snapshot, report):
|
||||
for trip in elevators['trips']:
|
||||
if trip['state'] == 'QUEUED':
|
||||
add(trip['actor_id'].replace('guest:', 'guest/', 1).replace('staff:', 'staff/', 1), trip['from'])
|
||||
lift_cells = {tuple(cell) for shaft in elevators['shafts'] for cell in shaft['stops']}
|
||||
footprint = list(dict.fromkeys((cell[0] + dx, cell[1], cell[2])
|
||||
for shaft in elevators['shafts'] for cell in shaft['stops']
|
||||
for dx in range(max(int(shaft.get('width', 2)), 1))))
|
||||
close(report['shaft_footprint'], [list(c) for c in footprint], 'shaft footprint')
|
||||
lift_cells = set(footprint)
|
||||
walkable = {tuple(cell) for cell in runtime['world']['walkable_cells']}
|
||||
owners = {tuple(cell): row['entity_id'] for row in runtime['world']['occupancies'] for cell in row['cells']}
|
||||
reserved, positions, hidden, badges = set(), {}, {}, []
|
||||
@@ -926,7 +1003,7 @@ def compare_queues(snapshot, report):
|
||||
for distance in range(1, 9):
|
||||
target = (cell[0] + direction * distance, cell[1], cell[2])
|
||||
facility = facilities.get(owners.get(target))
|
||||
if target not in walkable or (facility and facility['definition_id'] not in ('guest_corridor', 'service_corridor')):
|
||||
if target in lift_cells or target not in walkable or (facility and facility['definition_id'] not in ('guest_corridor', 'service_corridor')):
|
||||
break
|
||||
lane.append(target)
|
||||
index = 0
|
||||
@@ -1207,13 +1284,19 @@ def compare_cabin_geometry(snapshot, report):
|
||||
badge = '満員 ' + badge
|
||||
if not shaft['operational']:
|
||||
badge = '停止 ' + badge
|
||||
else:
|
||||
badge = '運転 ' + badge
|
||||
doors = shaft['operational'] and shaft['phase'] in ('IDLE', 'BOARDING', 'ALIGHTING')
|
||||
badge += (' 開 ' if doors else ' 閉 ') + shaft['phase']
|
||||
expected.append(dict(shaft_id=shaft['shaft_id'], rect=rect, pixel=p, operational=shaft['operational'],
|
||||
doors_open=shaft['operational'] and shaft['phase'] in ('IDLE', 'BOARDING', 'ALIGHTING'),
|
||||
phase=shaft['phase'], passenger_count=count, doors_open=doors,
|
||||
badge_rect=badge_rectangle([rect[0], rect[1]-14], badge),
|
||||
badge_at=[rect[0], rect[1]-14], badge=badge))
|
||||
require(len(report['cabins']) == len(expected), 'cabin count')
|
||||
for row, wanted in zip(report['cabins'], expected):
|
||||
for key, value in wanted.items():
|
||||
close(row[key], value, 'cabin/' + key)
|
||||
validate_cabin_intrusion(report)
|
||||
|
||||
|
||||
def compare_motion(state, report, audit):
|
||||
@@ -1693,6 +1776,126 @@ def BLOCKERS(executed=None):
|
||||
|
||||
CONTRACT = 'docs/plans/original-completion-2026-10-02/execution/completion_marker_contracts.json'
|
||||
PLAN = 'docs/plans/original-completion-2026-10-02/IMPLEMENTATION_PLAN.md'
|
||||
BASE_COMMIT = '860136f7099f5e3aee99da5798a627a77f2d8eea'
|
||||
ALLOWED_PATHS = ('scenes/main/world_preview.gd', 'scripts/ui/room_presentation.gd',
|
||||
'tests/ui/test_p23_life_observation.gd', 'tests/ui/test_p23_life_observation.gd.uid',
|
||||
'tests/fixtures/p23-03/**', 'tools/run_p23_life_observation.py',
|
||||
'tools/check_p23_life_observation_evidence.py', 'tools/test_p23_life_observation_evidence.py',
|
||||
'tools/regression/suite-policy.json',
|
||||
'docs/plans/original-completion-2026-10-02/execution/p23-03/**', CONTRACT)
|
||||
|
||||
|
||||
def strict_json(data):
|
||||
def pairs(items):
|
||||
result = {}
|
||||
for key, value in items:
|
||||
require(key not in result, 'duplicate JSON key: ' + key)
|
||||
result[key] = value
|
||||
return result
|
||||
return json.loads(data, object_pairs_hook=pairs,
|
||||
parse_constant=lambda value: require(False, 'nonfinite JSON: ' + value))
|
||||
|
||||
|
||||
def marker_join(base, submitted, main):
|
||||
"""Preserve M verbatim structurally, adding exactly the S P23 row."""
|
||||
test = 'tests/ui/test_p23_life_observation.gd'
|
||||
for document in (base, submitted, main):
|
||||
rows = document['rows']
|
||||
require(len({row['test'] for row in rows}) == len(rows), 'duplicate marker test')
|
||||
additions = [row for row in submitted['rows'] if row['test'] == test]
|
||||
require(len(additions) == 1 and not any(row['test'] == test for row in base['rows'] + main['rows']),
|
||||
'marker must add exactly one new P23 row')
|
||||
stripped = copy.deepcopy(submitted)
|
||||
stripped['rows'] = [row for row in stripped['rows'] if row['test'] != test]
|
||||
require(structural_equal(stripped, base), 'submitted marker changes non-P23 structure')
|
||||
joined = copy.deepcopy(main)
|
||||
joined['rows'].append(copy.deepcopy(additions[0]))
|
||||
return joined
|
||||
|
||||
|
||||
def structural_equal(a, b):
|
||||
return json.dumps(a, sort_keys=True, ensure_ascii=False, allow_nan=False) == json.dumps(
|
||||
b, sort_keys=True, ensure_ascii=False, allow_nan=False)
|
||||
|
||||
|
||||
def verify_marker_join(base, submitted, main, tested):
|
||||
expected = marker_join(base, submitted, main)
|
||||
p23 = expected['rows'][-1]
|
||||
require(structural_equal([row for row in tested['rows'] if row['test'] == p23['test']], [p23]),
|
||||
'joined marker P23 row changed or duplicated')
|
||||
# merge-tree determines the insertion position; deleting the one addition
|
||||
# must leave M's entire ordered structure, including all P18 rows, intact.
|
||||
stripped = copy.deepcopy(tested)
|
||||
stripped['rows'] = [row for row in stripped['rows'] if row['test'] != p23['test']]
|
||||
require(structural_equal(stripped, main), 'completion marker is not the strict structural join')
|
||||
|
||||
|
||||
def git_bytes(root, *arguments):
|
||||
"""Root-only adapter. Worker tests inject an in-memory object database."""
|
||||
process = subprocess.run(['git', '-C', str(root), *arguments], capture_output=True, check=False)
|
||||
require(process.returncode == 0, 'Git composition failed/conflicted: ' + ' '.join(arguments))
|
||||
return process.stdout
|
||||
|
||||
|
||||
def verify_composition(root, roles, source_map, inputs, read_git=None):
|
||||
require(isinstance(roles, dict), 'missing externally bound S/M/T/Q')
|
||||
names = ('submitted_sha', 'main_context_sha', 'tested_tree', 'tested_commit')
|
||||
require(all(isinstance(roles.get(k), str) and re.fullmatch('[0-9a-f]{40}', roles[k]) for k in names),
|
||||
'invalid externally bound S/M/T/Q')
|
||||
s, m, t, q = (roles[k] for k in names)
|
||||
git = read_git or (lambda *args: git_bytes(root, *args))
|
||||
for commit in (BASE_COMMIT, s, m, q):
|
||||
require(git('cat-file', '-t', commit).strip() == b'commit', 'role is not a commit')
|
||||
require(git('cat-file', '-t', t).strip() == b'tree', 'T is not a tree')
|
||||
require(git('rev-parse', q+'^{tree}').decode().strip() == t, 'Q does not contain T')
|
||||
require(git('rev-parse', 'HEAD').decode().strip() == q, 'checkout is not Q')
|
||||
require(not git('status', '--porcelain'), 'tested checkout is dirty')
|
||||
merged = git('merge-tree', '--write-tree', s, m).decode().splitlines()
|
||||
require(merged and merged[0] == t, 'T differs from deterministic merge-tree')
|
||||
def tree(revision):
|
||||
result = {}
|
||||
for record in git('ls-tree', '-r', '-z', revision).split(b'\0'):
|
||||
if not record:
|
||||
continue
|
||||
metadata, path = record.split(b'\t', 1)
|
||||
mode, kind, oid = metadata.decode().split()
|
||||
result[path.decode()] = (mode, kind, oid)
|
||||
return result
|
||||
base, submitted, main, tested = map(tree, (BASE_COMMIT, s, m, t))
|
||||
changed_s = {p for p in base.keys() | submitted.keys() if base.get(p) != submitted.get(p)}
|
||||
changed_m = {p for p in base.keys() | main.keys() if base.get(p) != main.get(p)}
|
||||
def allowed(path):
|
||||
return any(path.startswith(rule[:-2]) if rule.endswith('/**') else path == rule
|
||||
for rule in ALLOWED_PATHS)
|
||||
require(all(allowed(p) for p in changed_s), 'S changed a path outside allowed_paths')
|
||||
for paths, authority, label in ((changed_s-changed_m, submitted, 'S-only'),
|
||||
(changed_m-changed_s, main, 'M-only')):
|
||||
for path in paths:
|
||||
require(tested.get(path) == authority.get(path), label + ' blob/mode provenance: ' + path)
|
||||
def document(revision):
|
||||
return strict_json(git('show', revision + ':' + CONTRACT))
|
||||
verify_marker_join(document(BASE_COMMIT), document(s), document(m), document(t))
|
||||
source_names = {p for p in tested if p.split('/', 1)[0] in
|
||||
('autoload', 'scripts', 'scenes', 'data', 'tests', 'assets', 'tools')
|
||||
and Path(p).name != '.DS_Store' and '__pycache__' not in Path(p).parts}
|
||||
source_names.update(('project.godot', 'export_presets.cfg', 'Makefile', 'AGENTS.md'))
|
||||
require(set(source_map) == source_names, 'incomplete tested-tree source/settings/QA map')
|
||||
# Source refs include all QA suites and settings, not just S-changed files.
|
||||
required = set(source_map) | {p.split('#', 1)[0] for p in inputs} | {PLAN, CONTRACT,
|
||||
'project.godot', 'tests/business/test_required_kitchen.gd', 'tools/regression/suite-policy.json'}
|
||||
bindings = roles.get('blob_bindings')
|
||||
require(isinstance(bindings, dict) and set(bindings) == required, 'incomplete/extraneous T blob bindings')
|
||||
for path in sorted(required):
|
||||
entry = tested.get(path)
|
||||
require(entry is not None and entry[1] == 'blob' and bindings[path] == entry[2], 'T blob binding: ' + path)
|
||||
content = git('show', t + ':' + path)
|
||||
require((root/path).read_bytes() == content, 'checkout bytes differ from T: ' + path)
|
||||
digest = hashlib.sha256(content).hexdigest()
|
||||
if path in source_map:
|
||||
require(source_map[path] == digest, 'evidence source is not T: ' + path)
|
||||
if path in inputs:
|
||||
require(inputs[path] == digest, 'evidence input is not T: ' + path)
|
||||
return {k: roles[k] for k in names}
|
||||
|
||||
|
||||
def normative_plan_text(root):
|
||||
@@ -1780,6 +1983,11 @@ def inspect_resume(writer, reader, saved, requests, formal_members):
|
||||
return ['distinct_pid', 'formal_members', 'load_ack']
|
||||
|
||||
|
||||
def require_screenshot_coverage(action, screenshots):
|
||||
require(isinstance(screenshots, list) and len(screenshots) >= (1 if action == 'screenshot' else 2),
|
||||
'screenshot-only needs one image; real input needs before/after images')
|
||||
|
||||
|
||||
def inspect_os_requests(directory, layout, role, pid, root=ROOT):
|
||||
pair = directory/'os-pair'/layout
|
||||
def path(name):
|
||||
@@ -1801,6 +2009,7 @@ def inspect_os_requests(directory, layout, role, pid, root=ROOT):
|
||||
observed = json.loads(ack_path.with_name('observed.json').read_bytes())
|
||||
request, ack = json.loads(request_path.read_bytes()), json.loads(ack_path.read_bytes())
|
||||
cua = document(ack['input_receipt'])
|
||||
require_screenshot_coverage(request['action'], cua.get('screenshots'))
|
||||
require(request['sequence'] == ack['sequence'] == observed['sequence'] == sequence, 'OS request sequence')
|
||||
require(request['target_pid'] == ack['target_pid'] == observed['pid'] == cua['target_pid'] == pid, 'OS request PID')
|
||||
digest = sha(request_path)
|
||||
@@ -1887,7 +2096,7 @@ def inspect_native_phases(directory, index, root=ROOT):
|
||||
and receipt['completion']['passed'] is True and receipt['reported_checks'] > 0 and receipt['reported_failures'] == 0, 'native strict marker')
|
||||
|
||||
|
||||
def check(root, directory):
|
||||
def check(root, directory, root_roles=None):
|
||||
"""Never infer completion of unimplemented gates from an index assertion."""
|
||||
deficits = []
|
||||
executed = {c: {category: set() for category in CATEGORIES} for c in CONDITIONS}
|
||||
@@ -1897,8 +2106,13 @@ def check(root, directory):
|
||||
return ['missing evidence index: ' + str(index_path)]
|
||||
try:
|
||||
index = json.loads(index_path.read_bytes())
|
||||
require(index.get('schema') == 'p23-life-observation-v2',
|
||||
'historical v1 evidence has diagnostic applicability only')
|
||||
runner = module('p23_source_applicability', root/'tools/run_all_tests.py')
|
||||
require(index.get('source_map') == {n: v['sha256'] for n, v in runner.source_refs(root).items()}, 'exact frozen source map')
|
||||
roles = verify_composition(root, root_roles, index['source_map'], input_map(root))
|
||||
require(index.get('composition') == roles and index.get('commit') == roles['tested_commit'],
|
||||
'evidence role binding differs from Root S/M/T/Q; historical v1 is not T evidence')
|
||||
if index.get('input_map') != input_map(root):
|
||||
deficits.append('normative input_map differs from current immutable inputs')
|
||||
if index.get('spec_sha256') != SPEC_SHA:
|
||||
@@ -1985,6 +2199,8 @@ def main(argv=None):
|
||||
parser.add_argument('--offline-mirror', type=Path)
|
||||
parser.add_argument('--layout-a', type=Path)
|
||||
parser.add_argument('--layout-b', type=Path)
|
||||
parser.add_argument('--root-composition', type=Path,
|
||||
default=os.environ.get('P23_ROOT_COMPOSITION'))
|
||||
args = parser.parse_args(argv)
|
||||
check_setup_budget_source((args.project_root / 'tests/fixtures/p23-03/observation.gd').read_text())
|
||||
if args.offline_mirror:
|
||||
@@ -1997,7 +2213,8 @@ def main(argv=None):
|
||||
if not build:
|
||||
print('BUILD_OUTPUT_DIR is required', file=sys.stderr)
|
||||
return 2
|
||||
deficits = check(args.project_root, Path(build) / 'p23-life-observation-evidence')
|
||||
root_roles = strict_json(args.root_composition.read_bytes()) if args.root_composition else None
|
||||
deficits = check(args.project_root, Path(build) / 'p23-life-observation-evidence', root_roles)
|
||||
print(json.dumps(dict(task_id='P23-03', integrity='fail' if deficits else 'pass',
|
||||
deficits=deficits, acceptance='pending'), ensure_ascii=False))
|
||||
return 1 if deficits else 0
|
||||
|
||||
@@ -235,12 +235,15 @@ def collect(args):
|
||||
raise ValueError('evidence output must be outside the frozen source checkout')
|
||||
r = module('p23_standard_runner', root/'tools/run_all_tests.py')
|
||||
commit = subprocess.check_output(['git', '-C', str(root), 'rev-parse', 'HEAD'], text=True).strip()
|
||||
r.require(commit == args.commit, 'actual final submitted commit required')
|
||||
r.require(commit == args.commit, 'actual tested commit Q required')
|
||||
r.require(not subprocess.check_output(['git','-C',str(root),'status','--porcelain'], text=True), 'frozen clean worktree required')
|
||||
r.require(not out.exists(), 'fresh output; never overwrite earlier proof')
|
||||
refs = r.source_refs(root)
|
||||
checker = module('p23_initial_inputs', root/'tools/check_p23_life_observation_evidence.py')
|
||||
inputs_before = checker.input_map(root)
|
||||
root_roles = checker.strict_json(args.root_composition.read_bytes())
|
||||
composition = checker.verify_composition(root, root_roles,
|
||||
{name: pin['sha256'] for name, pin in refs.items()}, inputs_before)
|
||||
outer = r.module('p23_owned_outer', r.SUPPORT/'owned_outer.py')
|
||||
r.require(not outer.native.helper['live_engines'](), 'one native engine')
|
||||
engine = shutil.which(args.godot)
|
||||
@@ -328,7 +331,8 @@ def collect(args):
|
||||
r.write(out/'runner-ended.json', {'error':error, 'cleanup':cleanup, 'cleanup_errors':cleanup_errors,
|
||||
'phases':phases, 'formal_acceptance':False, 'native_Windows':'pending'})
|
||||
checker = module('p23_observation_checker', root/'tools/check_p23_life_observation_evidence.py')
|
||||
r.write(out/'index.json', {'schema':'p23-life-observation-v1', 'commit':commit,
|
||||
r.write(out/'index.json', {'schema':'p23-life-observation-v2', 'commit':commit,
|
||||
'composition':composition,
|
||||
'source_map':selected['map'], 'input_map':inputs_before,
|
||||
'provenance':checker.input_provenance(root),
|
||||
'marker_contract_sha256':r.sha(root/CONTRACT),
|
||||
@@ -345,6 +349,8 @@ def main(argv=None):
|
||||
parser.add_argument('--project-root', type=absolute, required=True)
|
||||
parser.add_argument('--output', type=absolute, required=True)
|
||||
parser.add_argument('--commit', required=True)
|
||||
parser.add_argument('--root-composition', type=absolute, required=True,
|
||||
help='Root external S/M/T/Q and complete tested-tree blob bindings')
|
||||
parser.add_argument('--godot', default='godot')
|
||||
parser.add_argument('--input-mode', choices=('handler', 'native_os'), default='handler')
|
||||
parser.add_argument('--collect', action='store_true', required=True)
|
||||
|
||||
@@ -18,6 +18,233 @@ sys.dont_write_bytecode = True
|
||||
import check_p23_life_observation_evidence as check
|
||||
|
||||
|
||||
def synthetic_repair28_fields(report, snapshot=None):
|
||||
"""Counterfactual unit-test projection only; never rewrite raw history."""
|
||||
report['overlay_passes'] = ['traffic', 'cabins', 'actors']
|
||||
report['shaft_footprint'] = [list(c) for c in dict.fromkeys(
|
||||
(stop[0]+dx, stop[1], stop[2]) for shaft in check.elevator_report(snapshot)['shafts']
|
||||
for stop in shaft['stops'] for dx in range(max(shaft.get('width', 2), 1)))] if snapshot else []
|
||||
for cabin in report.get('cabins', []):
|
||||
if snapshot is not None:
|
||||
shaft = next(s for s in check.elevator_report(snapshot)['shafts'] if s['shaft_id'] == cabin['shaft_id'])
|
||||
cabin.update(phase=shaft['phase'], passenger_count=len(shaft['boarded']))
|
||||
count, capacity = len(shaft['boarded']), shaft['capacity']
|
||||
cabin['badge'] = ('運転 ' if shaft['operational'] else '停止 ') + ('満員 ' if capacity > 0 and count >= capacity else '') + f'{count}/{capacity}人' + (' 開 ' if cabin['doors_open'] else ' 閉 ') + shaft['phase']
|
||||
cabin['badge_rect'] = check.badge_rectangle(cabin['badge_at'], cabin['badge'])
|
||||
report['traffic_obstacles'] = check.traffic_obstacles(report)
|
||||
for row in report.get('traffic', []):
|
||||
row['segment_rects'] = check.traffic_segments(row['band_rect'], row['rect'], report['traffic_obstacles'])
|
||||
return report
|
||||
|
||||
|
||||
class Repair28GeometryTests(unittest.TestCase):
|
||||
def test_one_screenshot_allowed_but_real_input_pair_kept(self):
|
||||
check.require_screenshot_coverage('screenshot', [{}])
|
||||
check.require_screenshot_coverage('click', [{}, {}])
|
||||
for action, shots in [('screenshot', []), ('click', [{}]), ('wheel', [{}]), ('key', None)]:
|
||||
with self.subTest(action=action), self.assertRaises(ValueError):
|
||||
check.require_screenshot_coverage(action, shots)
|
||||
|
||||
def test_overview_occlusion_keeps_density_row_and_safe_order(self):
|
||||
report = LifeOverlayTests().report()
|
||||
report['actor_drawings'] = []
|
||||
report['badges'] = [dict(at=[0,y], text='cover') for y in (0,14,28,42,56,70,84)]
|
||||
synthetic_repair28_fields(report)
|
||||
self.assertEqual(report['traffic'][0]['segment_rects'], [])
|
||||
self.assertEqual(report['traffic'][0]['traffic']['current_density'], 1)
|
||||
check.validate_life_overlay(report)
|
||||
source = (check.ROOT/'scenes/main/world_preview.gd').read_text()
|
||||
broken = source.replace('canvas.draw_rect(rect, Color("34413b"))',
|
||||
'canvas.draw_rect(rect, Color("34413b", 0.5))')
|
||||
with mock.patch.object(Path, 'read_text', return_value=broken):
|
||||
with self.assertRaisesRegex(ValueError, 'opaque later badges'):
|
||||
check.validate_life_overlay(report)
|
||||
|
||||
def test_r15_empty_cabin_rejects_queue_guest_even_if_marked_riding(self):
|
||||
fixture = json.loads((check.ROOT/'tests/fixtures/p23-03/repair28_r15_negative.json').read_text())
|
||||
cabin = dict(rect=fixture['cabin_rect'], passengers=[], passenger_count=fixture['passenger_count'])
|
||||
actor = dict(fixture['actor'], riding=True, mechanical_pass=True)
|
||||
with self.assertRaisesRegex(ValueError, 'non-passenger inside cabin'):
|
||||
check.validate_cabin_intrusion(dict(cabins=[cabin], actor_drawings=[actor]))
|
||||
cabin['passengers'] = [actor]
|
||||
with self.assertRaisesRegex(ValueError, 'passenger drawn in empty cabin'):
|
||||
check.validate_cabin_intrusion(dict(cabins=[cabin], actor_drawings=[]))
|
||||
|
||||
def test_r15_traffic_is_split_against_cabin_and_badge(self):
|
||||
fixture = json.loads((check.ROOT/'tests/fixtures/p23-03/repair28_r15_negative.json').read_text())
|
||||
row = fixture['traffic']
|
||||
obstacles = [fixture['cabin_rect'], check.badge_rectangle(**fixture['queue_badge'])]
|
||||
for obstacle in obstacles:
|
||||
self.assertTrue(check.intersects(row['band_rect'], obstacle))
|
||||
pieces = check.traffic_segments(row['band_rect'], row['rect'], obstacles)
|
||||
self.assertTrue(pieces)
|
||||
for piece in pieces:
|
||||
for obstacle in obstacles:
|
||||
self.assertFalse(check.intersects(piece, obstacle))
|
||||
|
||||
def test_exact_subtraction_and_full_cover_fallback(self):
|
||||
self.assertEqual(check.traffic_segments([10,85,60,5], [0,0,80,96],
|
||||
[[20,0,20,90], [45,80,20,14]]), [[10,85,10,5], [40,85,5,5], [65,85,5,5]])
|
||||
self.assertEqual(check.traffic_segments([10,85,60,5], [0,0,80,96],
|
||||
[[0,80,80,16]]), [[10,0,60,5]])
|
||||
|
||||
def test_claimed_safe_segments_and_obstacles_are_recomputed(self):
|
||||
report = LifeOverlayTests().report()
|
||||
report['landing_badges'] = [dict(at=[20,80], text='queue')]
|
||||
synthetic_repair28_fields(report)
|
||||
check.validate_life_overlay(report)
|
||||
for field in ('segment_rects', 'traffic_obstacles'):
|
||||
bad = copy.deepcopy(report)
|
||||
if field == 'segment_rects':
|
||||
bad['traffic'][0][field] = [bad['traffic'][0]['band_rect']]
|
||||
else:
|
||||
bad[field] = []
|
||||
with self.subTest(field=field), self.assertRaisesRegex(ValueError, 'traffic (segment|obstacle) geometry'):
|
||||
check.validate_life_overlay(bad)
|
||||
|
||||
def test_width_two_walkable_interior_is_hidden_not_a_passenger(self):
|
||||
fixture = SpatialTests()
|
||||
fixture.setUp()
|
||||
fixture.test_missing_queue_overflow_rejected()
|
||||
# The old implementation assigned x+1 to g0. Preserve the typed demand
|
||||
# and assert that fabricating that old visible actor is now rejected.
|
||||
fixture.report['landing_badges'][0]['overflow'] = 3
|
||||
fixture.report['queue_positions'] = {'guest/g0': [100,82]}
|
||||
with self.assertRaisesRegex(ValueError, 'queue_positions'):
|
||||
check.compare_queues(fixture.snapshot, fixture.report)
|
||||
fixture.report['queue_positions'] = {}
|
||||
fixture.report['shaft_footprint'].pop(1)
|
||||
with self.assertRaisesRegex(ValueError, 'shaft footprint'):
|
||||
check.compare_queues(fixture.snapshot, fixture.report)
|
||||
|
||||
def test_cabin_state_false_fields_rejected_on_both_layers(self):
|
||||
fixture = SpatialTests()
|
||||
fixture.setUp()
|
||||
fixture.test_cabin_geometry_and_phase()
|
||||
for layer in (0,1):
|
||||
fixture.report['layer'] = layer
|
||||
fixture.runtime['elevators']['shafts'][0]['config']['stops'][0][2] = layer
|
||||
for field, value in [('passenger_count',1), ('operational',False), ('phase','PICKUP'),
|
||||
('doors_open',False), ('badge_rect',[80,58,1,14])]:
|
||||
bad = copy.deepcopy(fixture.report)
|
||||
bad['cabins'][0][field] = value
|
||||
with self.subTest(layer=layer,field=field), self.assertRaises(ValueError):
|
||||
check.compare_cabin_geometry(fixture.snapshot, bad)
|
||||
|
||||
|
||||
class Repair28CompositionTests(unittest.TestCase):
|
||||
"""No Git subprocess, repository writes, engine or proof generation."""
|
||||
def setUp(self):
|
||||
self.s, self.m, self.t, self.q = ('1'*40, '2'*40, '3'*40, '4'*40)
|
||||
base_marker = dict(schema_version=1, rows=[dict(test='old', required=True)])
|
||||
s_marker = copy.deepcopy(base_marker)
|
||||
s_marker['rows'].append(dict(test='tests/ui/test_p23_life_observation.gd', required=True))
|
||||
m_marker = copy.deepcopy(base_marker)
|
||||
m_marker['rows'].append(dict(test='P18', required=True))
|
||||
self.documents = [base_marker, s_marker, m_marker, check.marker_join(base_marker,s_marker,m_marker)]
|
||||
names = ['project.godot','export_presets.cfg','Makefile','AGENTS.md', check.PLAN, check.CONTRACT,
|
||||
'tests/business/test_required_kitchen.gd','tools/regression/suite-policy.json',
|
||||
'scenes/main/world_preview.gd']
|
||||
base = {name: (name+' baseline').encode() for name in names}
|
||||
self.files = {r: copy.deepcopy(base) for r in (check.BASE_COMMIT,self.s,self.m,self.t)}
|
||||
for revision, document in zip(self.files, self.documents):
|
||||
self.files[revision][check.CONTRACT] = json.dumps(document).encode()
|
||||
self.files[self.s]['scenes/main/world_preview.gd'] = b'S drawing'
|
||||
self.files[self.m]['tools/regression/suite-policy.json'] = b'M only policy'
|
||||
self.files[self.m]['tests/business/test_required_kitchen.gd'] = b'M approved QA source'
|
||||
self.files[self.t].update({p:self.files[self.s][p] for p in ['scenes/main/world_preview.gd']})
|
||||
self.files[self.t].update({p:self.files[self.m][p] for p in
|
||||
['tools/regression/suite-policy.json','tests/business/test_required_kitchen.gd']})
|
||||
self.source = {p:hashlib.sha256(v).hexdigest() for p,v in self.files[self.t].items()
|
||||
if p not in (check.PLAN, check.CONTRACT)}
|
||||
self.inputs = {check.CONTRACT:hashlib.sha256(self.files[self.t][check.CONTRACT]).hexdigest()}
|
||||
self.roles = dict(submitted_sha=self.s, main_context_sha=self.m, tested_tree=self.t, tested_commit=self.q,
|
||||
blob_bindings={p:self.blob(v) for p,v in self.files[self.t].items()})
|
||||
self.merge_tree = self.t
|
||||
self.calls = []
|
||||
|
||||
@staticmethod
|
||||
def blob(value):
|
||||
return hashlib.sha1(b'blob '+str(len(value)).encode()+b'\0'+value).hexdigest()
|
||||
|
||||
def git(self, *args):
|
||||
self.calls.append(args)
|
||||
if args[0] == 'cat-file': return b'tree\n' if args[2] == self.t else b'commit\n'
|
||||
if args[0] == 'rev-parse': return (self.q if args[1] == 'HEAD' else self.t).encode()+b'\n'
|
||||
if args[0] == 'status': return b''
|
||||
if args[0] == 'merge-tree': return self.merge_tree.encode()+b'\n'
|
||||
if args[0] == 'ls-tree':
|
||||
return b''.join(('100644 blob '+self.blob(value)+'\t'+path+'\0').encode()
|
||||
for path,value in self.files[args[-1]].items())
|
||||
if args[0] == 'show':
|
||||
revision,path = args[1].split(':',1)
|
||||
return self.files[revision][path]
|
||||
raise AssertionError(args)
|
||||
|
||||
def verify(self):
|
||||
with mock.patch.object(Path, 'read_bytes', lambda p:self.files[self.t][str(p.relative_to(check.ROOT))]):
|
||||
return check.verify_composition(check.ROOT,self.roles,self.source,self.inputs,self.git)
|
||||
|
||||
def test_valid_route_b_keeps_unchanged_allowed_path_from_main(self):
|
||||
self.assertEqual(self.verify()['tested_tree'], self.t)
|
||||
self.assertIn(('merge-tree','--write-tree',self.s,self.m),self.calls)
|
||||
|
||||
def test_out_of_scope_submitted_change_rejected(self):
|
||||
self.files[self.s]['autoload/simulation_runtime.gd'] = b'unauthorized'
|
||||
with self.assertRaisesRegex(ValueError,'outside allowed_paths'): self.verify()
|
||||
|
||||
def test_wrong_merge_tree_rejected(self):
|
||||
self.merge_tree = '5'*40
|
||||
with self.assertRaisesRegex(ValueError,'deterministic merge-tree'): self.verify()
|
||||
|
||||
def test_conflicted_merge_fails_closed(self):
|
||||
original = self.git
|
||||
def conflict(*args):
|
||||
if args[0] == 'merge-tree': raise ValueError('Git composition failed/conflicted')
|
||||
return original(*args)
|
||||
with self.assertRaisesRegex(ValueError,'conflicted'):
|
||||
check.verify_composition(check.ROOT,self.roles,self.source,self.inputs,conflict)
|
||||
|
||||
def test_s_only_and_main_only_blob_provenance(self):
|
||||
for path in ('scenes/main/world_preview.gd','tools/regression/suite-policy.json',
|
||||
'tests/business/test_required_kitchen.gd'):
|
||||
with self.subTest(path=path):
|
||||
original = self.files[self.t][path]
|
||||
self.files[self.t][path] = self.files[check.BASE_COMMIT][path]
|
||||
with self.assertRaisesRegex(ValueError,'blob/mode provenance'): self.verify()
|
||||
self.files[self.t][path] = original
|
||||
|
||||
def test_structural_join_preserves_all_main_rows_and_metadata(self):
|
||||
base,s,m,t = self.documents
|
||||
for mutation in ('delete_P18','overwrite_P18','extra','type','metadata','duplicate_P23'):
|
||||
bad = copy.deepcopy(t)
|
||||
if mutation == 'delete_P18': bad['rows'].pop(1)
|
||||
if mutation == 'overwrite_P18': bad['rows'][1]['required'] = False
|
||||
if mutation == 'extra': bad['rows'].append(dict(test='unapproved'))
|
||||
if mutation == 'type': bad['schema_version'] = True
|
||||
if mutation == 'metadata': bad['extra'] = 'changed'
|
||||
if mutation == 'duplicate_P23': bad['rows'].append(copy.deepcopy(bad['rows'][-1]))
|
||||
with self.subTest(mutation=mutation), self.assertRaises(ValueError):
|
||||
check.verify_marker_join(base,s,m,bad)
|
||||
positioned = copy.deepcopy(t)
|
||||
positioned['rows'].insert(1, positioned['rows'].pop())
|
||||
check.verify_marker_join(base,s,m,positioned)
|
||||
with self.assertRaisesRegex(ValueError,'duplicate JSON key'):
|
||||
check.strict_json('{"rows":[],"rows":[]}')
|
||||
|
||||
def test_binding_completeness_and_old_source_relabel_rejected(self):
|
||||
for mutation in ('missing_blob','wrong_blob','extra_blob','old_source','omitted_QA'):
|
||||
original_roles, original_source = copy.deepcopy(self.roles),dict(self.source)
|
||||
path = 'tests/business/test_required_kitchen.gd'
|
||||
if mutation == 'missing_blob': self.roles['blob_bindings'].pop(path)
|
||||
if mutation == 'wrong_blob': self.roles['blob_bindings'][path] = '0'*40
|
||||
if mutation == 'extra_blob': self.roles['blob_bindings']['unbound'] = '0'*40
|
||||
if mutation == 'old_source': self.source[path] = hashlib.sha256(self.files[self.s][path]).hexdigest()
|
||||
if mutation == 'omitted_QA': self.source.pop(path)
|
||||
with self.subTest(mutation=mutation), self.assertRaises(ValueError): self.verify()
|
||||
self.roles, self.source = original_roles, original_source
|
||||
|
||||
|
||||
class ActorNonoverlapTests(unittest.TestCase):
|
||||
def report(self, second_at):
|
||||
return dict(actor_drawings=[
|
||||
@@ -123,12 +350,12 @@ class ActorNonoverlapTests(unittest.TestCase):
|
||||
|
||||
class LifeOverlayTests(unittest.TestCase):
|
||||
def report(self):
|
||||
return dict(observation_serial=7, overlay_consumed_serial=7,
|
||||
return synthetic_repair28_fields(dict(observation_serial=7, overlay_consumed_serial=7,
|
||||
overlay_passes=['traffic', 'actors'], overlay_child_indices=[0, 1, 2],
|
||||
actor_drawings=[dict(key='guest/a', at=[20, 30], scale=3,
|
||||
mode='idle', frame=0, flip=False)], cabins=[],
|
||||
traffic=[dict(rect=[2, 2, 60, 92], band_rect=[9.2, 85.5, 45.6, 5],
|
||||
traffic=dict(current_density=1, temporary_block=False))])
|
||||
traffic=dict(current_density=1, temporary_block=False))]))
|
||||
|
||||
def test_front_and_back_valid_consumed_band(self):
|
||||
for layer in (0, 1):
|
||||
@@ -181,6 +408,7 @@ class ProjectionBandTests(unittest.TestCase):
|
||||
def setUp(self):
|
||||
fixture = json.loads((check.ROOT / 'tests/fixtures/p23-03/repair28_frame180_projection.json').read_text())
|
||||
self.report, self.snapshot = fixture['report'], fixture['snapshot']
|
||||
synthetic_repair28_fields(self.report, self.snapshot)
|
||||
|
||||
def occupied(self):
|
||||
for row in self.report['traffic']:
|
||||
@@ -228,7 +456,7 @@ class ProjectionBandTests(unittest.TestCase):
|
||||
def test_floor_copy_of_raw_cabin_member_keeps_zero_intersection(self):
|
||||
self.occupied()
|
||||
self.report['actor_drawings'].append(copy.deepcopy(self.report['cabins'][0]['passengers'][0]))
|
||||
with self.assertRaisesRegex(ValueError, 'floor actor core over traffic band'):
|
||||
with self.assertRaisesRegex(ValueError, 'non-passenger inside cabin'):
|
||||
check.validate_life_overlay(self.report, snapshot=self.snapshot)
|
||||
|
||||
def test_blocked_band_uses_same_projection_and_legibility_rules(self):
|
||||
@@ -247,7 +475,7 @@ class ProjectionBandTests(unittest.TestCase):
|
||||
actor.update(key='staff/spoof', staff=True, riding=False, region='carried')
|
||||
self.report['actor_drawings'].append(actor)
|
||||
self.report['carried_patients'] = {'spoof': 'carrier'}
|
||||
with self.assertRaisesRegex(ValueError, 'floor actor core over traffic band: staff/spoof'):
|
||||
with self.assertRaisesRegex(ValueError, 'non-passenger inside cabin: staff/spoof'):
|
||||
check.validate_life_overlay(self.report, snapshot=self.snapshot)
|
||||
|
||||
def test_carried_projection_requires_original_binding_tracks_and_fit(self):
|
||||
@@ -266,6 +494,7 @@ class ProjectionBandTests(unittest.TestCase):
|
||||
# Synthetic band crosses the raised patient, with uncovered span.
|
||||
fixture.report['traffic'] = [dict(cell=[0, 1, 0], rect=[78, 100.5, 60, 92],
|
||||
band_rect=[85.2, 184, 45.6, 5], traffic=dict(current_density=1, temporary_block=False))]
|
||||
synthetic_repair28_fields(fixture.report)
|
||||
check.validate_life_overlay(fixture.report, snapshot=fixture.snapshot)
|
||||
for mutation in ('binding', 'track', 'position'):
|
||||
report, snapshot = copy.deepcopy(fixture.report), copy.deepcopy(fixture.snapshot)
|
||||
@@ -807,6 +1036,7 @@ class SpatialTests(unittest.TestCase):
|
||||
goods=dict(requests=[], deliveries=[]), actor_drawings=[], cabins=[],
|
||||
observation_serial=1, overlay_consumed_serial=1,
|
||||
overlay_passes=['traffic', 'actors'], overlay_child_indices=[0, 1, 2])
|
||||
synthetic_repair28_fields(self.report)
|
||||
|
||||
def four_movers(self):
|
||||
self.runtime['world'].update(bounds=dict(min_x=1, max_x=1, min_y=1, max_y=1), walkable_cells=[[1, 1, 0]])
|
||||
@@ -988,10 +1218,10 @@ class SpatialTests(unittest.TestCase):
|
||||
self.runtime['elevators'] = saved
|
||||
saved['trips'] = [dict(copy.deepcopy(saved['trips'][0]), actor_id='guest:g'+str(i),
|
||||
request_id='trip'+str(i), enqueued_at=i) for i in range(3)]
|
||||
self.report.update(queue_positions={'guest/g0': [100, 82]},
|
||||
queue_hidden={'guest/g1': True, 'guest/g2': True}, landing_badges=[dict(at=[-4, 54],
|
||||
text='待ち3人(他2人)', cell=[0, 0, 0],
|
||||
members=['guest/g0', 'guest/g1', 'guest/g2'], visible_count=1, overflow=2)])
|
||||
self.report.update(shaft_footprint=[[0,0,0],[1,0,0],[0,1,0],[1,1,0]], queue_positions={},
|
||||
queue_hidden={'guest/g0': True, 'guest/g1': True, 'guest/g2': True}, landing_badges=[dict(at=[-4, 54],
|
||||
text='待ち3人(他3人)', cell=[0, 0, 0],
|
||||
members=['guest/g0', 'guest/g1', 'guest/g2'], visible_count=0, overflow=3)])
|
||||
check.compare_queues(self.snapshot, self.report)
|
||||
self.report['landing_badges'][0]['overflow'] = 0
|
||||
with self.assertRaises(ValueError):
|
||||
@@ -1031,6 +1261,7 @@ class SpatialTests(unittest.TestCase):
|
||||
self.report.update(paused=True, animation_time=0, elevator_tracks={'lift': dict(floor=0, at=0)},
|
||||
cabins=[dict(shaft_id='lift', rect=[80, 72, 24, 19], pixel=1, operational=True,
|
||||
doors_open=True, badge_at=[80, 58], badge='0/4人', passengers=[])])
|
||||
synthetic_repair28_fields(self.report, self.snapshot)
|
||||
check.compare_cabin_geometry(self.snapshot, self.report)
|
||||
for field, value in [('rect', [80, 70, 24, 19]), ('doors_open', False), ('badge', '4/4人')]:
|
||||
changed = copy.deepcopy(self.report)
|
||||
@@ -1169,6 +1400,7 @@ class AnchoredGeometryTests(unittest.TestCase):
|
||||
cabin = dict(shaft_id=sid, rect=[92,174,96,76], pixel=4, operational=True, doors_open=True,
|
||||
badge_at=[92,160], badge=('満員 ' if count >= 4 else '')+f'{count}/{max(4,count)}人', passengers=[])
|
||||
self.report['cabins'] = [cabin]
|
||||
synthetic_repair28_fields(self.report, self.snapshot)
|
||||
for i in range(count):
|
||||
key = 'guest/p%02d'%i
|
||||
self.person(key, [0,0,0])
|
||||
@@ -1233,6 +1465,7 @@ class AnchoredGeometryTests(unittest.TestCase):
|
||||
queued['comparison_only'].update(resting=False, label='休憩先へ移動中')
|
||||
audit['staff'].append(queued)
|
||||
self.drawing('staff/q', [44,214], 'wait')
|
||||
synthetic_repair28_fields(self.report, self.snapshot)
|
||||
result = check.compare_rest(self.snapshot, self.report, audit)
|
||||
self.assertTrue(result['s']['resting'])
|
||||
self.assertFalse(result['q']['resting'])
|
||||
@@ -1241,9 +1474,8 @@ class AnchoredGeometryTests(unittest.TestCase):
|
||||
|
||||
def test_passenger_and_floor_actor_front_and_back(self):
|
||||
cabin = self.cabin(1)
|
||||
self.person('guest/floor', [0,0,0])
|
||||
self.drawing('guest/floor', [108,214])
|
||||
cabin['passengers'][0]['at'][0] = 108
|
||||
self.person('guest/floor', [2,0,0])
|
||||
self.drawing('guest/floor', [236,214])
|
||||
for layer in (0,1):
|
||||
self.report['layer'] = layer
|
||||
for guest in self.runtime['guests']['guests']: guest['current_cell'][2] = layer
|
||||
@@ -1251,6 +1483,7 @@ class AnchoredGeometryTests(unittest.TestCase):
|
||||
for row in self.report['actor_drawings']+cabin['passengers']: row['cell'][2] = layer
|
||||
for track in self.report['actor_tracks'].values():
|
||||
track['cell'][2] = track['from'][2] = layer
|
||||
synthetic_repair28_fields(self.report, self.snapshot)
|
||||
with self.subTest(layer=layer):
|
||||
check.compare_cabin_geometry(self.snapshot, self.report)
|
||||
check.compare_actor_geometry(self.snapshot, self.report)
|
||||
|
||||
Reference in New Issue
Block a user