1#!/usr/bin/env python3
2"""Check source coverage across deterministic mixed-Unicode layout cases."""
3import argparse
4import json
5from pathlib import Path
6import random
7import subprocess
8
9
10def cases():
11 rng = random.Random(20260906)
12 tokens = ['a', ' ', 'é', 'é', 'שלום', 'العربية', '👩🏽‍💻', '👩‍👩‍👧‍👦', '🇨🇦', '\u00a0', 'ffi', '\u200d']
13 result = []
14 for index in range(500):
15 text = ''.join(rng.choice(tokens) for _ in range(rng.randrange(35)))
16 result.append({'id': str(index), 'width': rng.uniform(1, 250),
17 'runs': [{'text': text, 'font': 'Arial', 'size': rng.uniform(7, 25),
18 'bold': False, 'italic': False}]})
19 return result
20
21
22def check(source, output):
23 if len(source) != len(output['cases']):
24 raise ValueError('Probe did not return every case')
25 count = 0
26 for case, result in zip(source, output['cases']):
27 if case['id'] != result['id']:
28 raise ValueError('Probe returned different case identities')
29 raw = ''.join(run['text'] for run in case['runs']).encode('utf-16-le')
30 end = 0
31 for line in result['lines']:
32 if line['start_utf16'] != end or not end <= line['end_utf16'] <= len(raw) // 2:
33 raise ValueError(f"{case['id']}: noncontiguous or out-of-bounds source ranges")
34 start, end = line['start_utf16'], line['end_utf16']
35 if raw[2 * start:2 * end].decode('utf-16-le') != line['text']:
36 raise ValueError(f"{case['id']}: line text differs from its source range")
37 count += 1
38 if end * 2 != len(raw):
39 raise ValueError(f"{case['id']}: layout does not cover all source text")
40 return count
41
42
43if __name__ == '__main__':
44 parser = argparse.ArgumentParser(description=__doc__)
45 parser.add_argument('output', type=Path)
46 parser.add_argument('probe', nargs=argparse.REMAINDER)
47 args = parser.parse_args()
48 if not args.probe:
49 parser.error('Provide a probe command after the output directory')
50 args.output.mkdir(parents=True, exist_ok=False)
51 source = cases()
52 path = args.output / 'cases.json'
53 path.write_text(json.dumps(source, ensure_ascii=False, indent=2) + '\n')
54 command = args.probe + [str(path)]
55 (args.output / 'command.json').write_text(json.dumps(command) + '\n')
56 with (args.output / 'output.json').open('w') as stdout, (args.output / 'stderr.txt').open('w') as stderr:
57 result = subprocess.run(command, stdout=stdout, stderr=stderr)
58 summary = {'seed': 20260906, 'cases': len(source), 'exit': result.returncode}
59 if result.returncode == 0:
60 try:
61 summary['lines_checked'] = check(source, json.loads((args.output / 'output.json').read_text()))
62 except (ValueError, KeyError) as error:
63 summary['error'] = str(error)
64 else:
65 summary['error'] = 'Probe rejected a generated case; see stderr.txt'
66 (args.output / 'summary.json').write_text(json.dumps(summary, indent=2) + '\n')
67 print(json.dumps(summary))
68 raise SystemExit(1 if 'error' in summary else 0)