119 lines
5.4 KiB
Python
119 lines
5.4 KiB
Python
"""Summarize retained headless evidence without treating counts as a verdict.
|
|
|
|
Usage: python analyze.py LAB
|
|
Requires Pillow for PNG decoding. Writes analysis.json inside each run directory.
|
|
"""
|
|
import json
|
|
from pathlib import Path
|
|
import re
|
|
import sys
|
|
|
|
from PIL import Image
|
|
|
|
if len(sys.argv) != 2:
|
|
raise SystemExit(__doc__)
|
|
lab = Path(sys.argv[1]).resolve()
|
|
colors = {'A_yellow': (224, 192, 32), 'B_green': (32, 208, 160)}
|
|
|
|
|
|
def open_image(path):
|
|
return Image.open(path).convert('RGB')
|
|
|
|
|
|
def local_points(lines, pattern):
|
|
points = []
|
|
for line in lines:
|
|
match = re.search(pattern, line)
|
|
if match:
|
|
points.append(tuple(map(float, match.groups())))
|
|
return points
|
|
|
|
|
|
for run in sorted(lab.glob('run-*')):
|
|
if not (run / 'exit-status.json').exists():
|
|
continue
|
|
client = (run / 'client.log').read_text()
|
|
ordinary = (run / 'wev.log').read_text()
|
|
compositor = (run / 'compositor.log').read_text()
|
|
result = {
|
|
'run': run.name,
|
|
'exit_status': json.loads((run / 'exit-status.json').read_text()),
|
|
'grab_requests': re.findall(
|
|
r'(POPUP_[AB]) GRAB_REQUEST origin=wl_pointer.button target=(\w+) serial=(\d+) used=(\d+)',
|
|
client),
|
|
'popup_parents': re.findall(
|
|
r'(POPUP_[AB]) CREATE wl=(\d+) xdg=(\d+) role=(\d+) parent=(\w+) parent_xdg=(\d+)',
|
|
client),
|
|
'popup_done_order': re.findall(r'(POPUP_[AB]) POPUP_DONE', client),
|
|
'popup_destroy_order': re.findall(r'(POPUP_[AB]) DESTROY', client),
|
|
'popup_buttons': re.findall(
|
|
r'POINTER_BUTTON target=(POPUP_[AB]) serial=(\d+) button=(\d+) state=(\d+)',
|
|
client),
|
|
'ordinary_buttons': re.findall(
|
|
r'button: serial: (\d+); time: \d+; button: (\d+).*state: ([01])',
|
|
ordinary),
|
|
'scene_clip_logs': re.findall(
|
|
r'PROBE_POPUP_CLIP owner=(\S+) local=(-?\d+),(-?\d+) (\d+)x(\d+)',
|
|
compositor),
|
|
'hidden_owner_logs': re.findall(r'PROBE_POPUP_HIDDEN_OWNER[^\n]*', compositor),
|
|
'captures': {},
|
|
'open_sample_rgb': {},
|
|
}
|
|
reference_path = run / 'pointer-validation.json'
|
|
if reference_path.exists():
|
|
reference = json.loads(reference_path.read_text())
|
|
owner_points = local_points(reference['owner_motion_lines'],
|
|
r'x=(-?[\d.]+) y=(-?[\d.]+)')
|
|
ordinary_points = local_points(reference['ordinary_motion_lines'],
|
|
r'x, y: (-?[\d.]+), (-?[\d.]+)')
|
|
if len(owner_points) >= 2 and len(ordinary_points) >= 2:
|
|
result['pointer_observed'] = [
|
|
{'requested_global': list(reference['global_points'][i]),
|
|
'observed_output': output, 'observed_surface_local': list(point)}
|
|
for i, output, point in zip(range(4),
|
|
('HEADLESS-1', 'HEADLESS-2', 'HEADLESS-1', 'HEADLESS-2'),
|
|
(owner_points[-2], ordinary_points[-2],
|
|
owner_points[-1], ordinary_points[-1]))]
|
|
for path in sorted(run.glob('*.png')):
|
|
if not path.name.startswith(('left-', 'right-')):
|
|
continue
|
|
image = open_image(path)
|
|
pixels = image.get_flattened_data()
|
|
result['captures'][path.name] = {
|
|
'size': list(image.size),
|
|
'exact_color_pixels': {name: pixels.count(rgb) for name, rgb in colors.items()},
|
|
}
|
|
measurements_path = run / 'measurements.json'
|
|
if measurements_path.exists():
|
|
result['measurements'] = json.loads(measurements_path.read_text())
|
|
# CONTRACT: the private log's clip is local to the root popup tree.
|
|
# Subtracting it from the output viewport origin recovers that applied
|
|
# scene-tree origin; XDG role configure offsets locate A and B below it.
|
|
if result['scene_clip_logs']:
|
|
clip = result['scene_clip_logs'][-1]
|
|
left = result['measurements']['verified_outputs']['HEADLESS-1']
|
|
root_x, root_y = left[0] - int(clip[1]), left[1] - int(clip[2])
|
|
a = result['measurements']['popup_a_configure']
|
|
b = result['measurements']['popup_b_configure']
|
|
result['applied_scene_from_clip'] = {
|
|
'root_popup_tree': [root_x, root_y],
|
|
'popup_a': [root_x + a[0], root_y + a[1], a[2], a[3]],
|
|
'popup_b': [root_x + a[0] + b[0], root_y + a[1] + b[1], b[2], b[3]],
|
|
}
|
|
for sample in result['measurements']['sample_points']:
|
|
x, y = sample['global']
|
|
output = 'left' if x < result['measurements']['verified_outputs']['HEADLESS-2'][0] else 'right'
|
|
origin = result['measurements']['verified_outputs'][
|
|
'HEADLESS-1' if output == 'left' else 'HEADLESS-2']
|
|
path = run / f'{output}-open.png'
|
|
if path.exists():
|
|
local = (int(x - origin[0]), int(y - origin[1]))
|
|
image = open_image(path)
|
|
result['open_sample_rgb'][sample['purpose']] = {
|
|
'global': [x, y], 'output': output, 'local': list(local),
|
|
'rgb': list(image.getpixel(local)) if (
|
|
0 <= local[0] < image.width and 0 <= local[1] < image.height) else None,
|
|
}
|
|
(run / 'analysis.json').write_text(json.dumps(result, indent=2) + '\n')
|
|
print(run.name, result['captures'].get('left-open.png', {}).get('exact_color_pixels'),
|
|
result['captures'].get('right-open.png', {}).get('exact_color_pixels'))
|