Files
my-skills/knowledge-map/scripts/test_render_map.py
T
2026-11-05 01:04:53 +08:00

131 lines
5.6 KiB
Python

#!/usr/bin/env python3
"""Behavioral checks for ordering, semantics, timing, geometry and CLI diagnostics."""
import copy
import json
import subprocess
import sys
import tempfile
import unittest
import xml.etree.ElementTree as ET
from pathlib import Path
from render_map import Renderer, smooth, overlaps
ROOT=Path(__file__).resolve().parents[1]
def demo(name):
return json.loads((ROOT/'assets'/f'{name}-demo.json').read_text())
class MapTests(unittest.TestCase):
def test_examples_are_valid_deterministic_and_labels_clear(self):
for name in ('metro','timeline'):
with self.subTest(name=name):
d=demo(name)
renderer=Renderer(copy.deepcopy(d))
svg,report=renderer.render()
svg2,report2=Renderer(copy.deepcopy(d)).render()
self.assertEqual(svg,svg2)
self.assertEqual(report,report2)
root=ET.fromstring(svg)
self.assertEqual(len(root.findall('.//{*}g[@data-node]')),len(d['nodes']))
self.assertEqual(len(root.findall('.//{*}g[@data-route]')),len(d['routes']))
self.assertFalse([w for w in report['warnings'] if w['kind']!='overpass'])
labels=[b for b in renderer.boxes if b[-1]=='label']
for i,a in enumerate(labels):
self.assertFalse(any(overlaps(a[:4],b[:4]) for b in labels[i+1:]))
for a in labels:
self.assertFalse(any(overlaps(a[:4],b[:4]) for b in renderer.boxes if b[-1] in ('station','endpoint')))
def test_metro_order_respects_all_route_edges(self):
d=demo('metro')
_,report=Renderer(d).render()
for route in d['routes']:
ys=[report['positions'][n]['y'] for n in route['stops']]
self.assertTrue(all(a<b for a,b in zip(ys,ys[1:])))
def test_shared_station_has_exact_membership(self):
r=Renderer(demo('metro'))
_,report=r.render()
self.assertEqual(set(report['positions']['pilot']['xs']),{'design','qual'})
self.assertIn({'kind':'overpass','node':'pilot','route':'recruit'},report['warnings'])
self.assertNotIn('recruit',r.members['pilot'])
def test_time_scale_and_label_adjustments_preserve_event_time(self):
d=demo('timeline')
r=Renderer(copy.deepcopy(d))
_,report=r.render()
for node in ('s1','p1','d1','p6'):
fraction={'s1':0,'p1':.28,'d1':.59,'p6':1}[node]
self.assertAlmostEqual(report['positions'][node]['y'],r.top+fraction*(r.bottom-r.top))
d['nodes'][0]['label_dy']=52
_,changed=Renderer(d).render()
self.assertEqual(report['positions'],changed['positions'])
def test_easing_geometry_and_station_positions(self):
r=Renderer(demo('metro'))
_,report=r.render()
for pos in report['positions'].values():
for rid,x in pos['xs'].items():
self.assertAlmostEqual(x,r.rx(rid,pos['t']))
value,derivative=smooth([[0,0],[1,90]],.25)
self.assertAlmostEqual(value,14.0625)
self.assertAlmostEqual(derivative,101.25)
def test_cycle_is_rejected(self):
d={'routes':[{'id':'a','stops':['x','y']},{'id':'b','stops':['y','x']}], 'nodes':[{'id':'x'},{'id':'y'}]}
with self.assertRaisesRegex(ValueError,'cycle'):
Renderer(d).render()
def test_invalid_membership_and_ids_are_rejected(self):
changes=[lambda d:d['routes'][0]['stops'].append('missing'),lambda d:d['nodes'].append(copy.deepcopy(d['nodes'][0])),lambda d:d['nodes'].append({'id':'orphan'})]
for change in changes:
d=demo('metro')
change(d)
with self.assertRaises(ValueError):
Renderer(d).render()
def test_invalid_time_scale_and_offsets_are_rejected(self):
for scale in ([[1800,0],[1900,.5],[2000,.9]],[[1800,0],[1900,.8],[1950,.7],[2000,1]]):
d=demo('timeline')
d['layout']['time_scale']=scale
with self.assertRaises(ValueError):
Renderer(d).render()
d=demo('metro')
d['routes'][0]['offsets']=[[-1,0],[1,0]]
with self.assertRaises(ValueError):
Renderer(d).render()
def test_svg_text_is_escaped(self):
d=demo('metro')
d['title']='<script>& "示例"'
svg,_=Renderer(d).render()
root=ET.fromstring(svg)
self.assertEqual(root.find('{*}title').text,d['title'])
self.assertNotIn('<script>',svg)
def test_cli_strict_preserves_reviewable_output_and_returns_failure(self):
d=demo('metro')
d['nodes'][0]['label']='超长标签'*100
with tempfile.TemporaryDirectory() as tmp:
src=Path(tmp)/'map.json'
out=Path(tmp)/'map.svg'
src.write_text(json.dumps(d))
result=subprocess.run([sys.executable,str(ROOT/'scripts'/'render_map.py'),str(src),'-o',str(out),'--strict'],capture_output=True,text=True)
self.assertEqual(result.returncode,2,result.stderr)
self.assertTrue(out.exists())
report=json.loads(out.with_suffix('.report.json').read_text())
self.assertTrue(any(w['kind']=='label_needs_review' for w in report['warnings']))
def test_single_node_route_and_extra_legend_rows(self):
d={'routes':[{'id':str(i),'stops':['x']} for i in range(5)],'nodes':[{'id':'x','label':'共享'}]}
r=Renderer(d)
svg,report=r.render()
ET.fromstring(svg)
self.assertEqual(report['positions']['x']['y'],r.top)
self.assertEqual(len(report['positions']['x']['xs']),5)
if __name__=='__main__':
unittest.main()