修:draw-ui / oil-motion 原被当子模块指针收录 ⇒ 改为正常文件入库(两份内容原先对别人是空的)

一、问题(本轮实测)
`draw-ui` 与 `oil-motion` 目录里**各自带一个内嵌 `.git`** ⇒ 上一次提交把它们记成了 **gitlink(子模块指针)**
⇒ 仓库里只存了一个不属于任何远端的 commit id,**别人克隆下来这两份是空的** ✗(`git status` 显示 ` m draw-ui` / ` m oil-motion` = 子模块内容有改动)。

二、处置(可回退)
· 把两处的 `.git` **挪走**(⛔ 不是删除)⇒ `归档/内嵌git-20261008/{draw-ui,oil-motion}.git`;
· `git rm --cached` 掉那两个 gitlink,再 `git add` 两个目录 ⇒ **按正常文件入库**(内容才真的进仓库)。

三、副作用(如实记)
挪走 `.git` 后,这两个技能**不能再原地 `git pull` 取上游更新**(要更新得重新拉一份覆盖);
如需恢复其本地仓库,把 `归档/内嵌git-20261008/` 里的 `.git` 挪回原处即可。
This commit is contained in:
admin committed 2026-10-08 22:29:52 +08:00
1 parent e03465c398
commit 237a09a5b0
161 files changed
+19429 -2

No files matched your search

+119
View File
@@ -0,0 +1,119 @@
"""通过真实 CLI 验证冻结基线与采集契约;捕获对象为明确的合成夹具。"""
from concurrent.futures import ThreadPoolExecutor
import json
from pathlib import Path
import subprocess
import sys
import tempfile
import unittest
from PIL import Image
SCRIPTS=Path(__file__).resolve().parents[1]/'scripts'
class CapturePipelineTests(unittest.TestCase):
def setUp(self):
self.tmp=tempfile.TemporaryDirectory(prefix='capture pipeline ')
self.root=Path(self.tmp.name)
Image.new('RGB',(32,64),'white').save(self.root/'reference.png')
self.expected={'url':'http://127.0.0.1:4187/','title':'合成夹具','viewport':{'width':32,'height':64},'dpr':1,'full_page':True,'screenshot_scale':'css'}
self.manifest={'reference':'reference.png','css_viewport_width':32,'expected':self.expected,'regions':[
{'name':'card','box':[0,2,10,10],'selector':'.card'},
{'name':'ink','box':[0,20,12,12],'box_type':'ink','selector':'h1'},
{'name':'paper','kind':'color','box':[20,0,10,10]}]}
def tearDown(self):self.tmp.cleanup()
def cli(self,name,*args):return subprocess.run([sys.executable,str(SCRIPTS/name),*map(str,args)],capture_output=True,text=True)
def write(self,path,data):path.write_text(json.dumps(data),encoding='utf-8')
def measure(self):
self.write(self.root/'regions.json',self.manifest)
result=self.cli('measure_reference.py','--manifest',self.root/'regions.json','--out-dir',self.root/'measured')
self.assertEqual(result.returncode,0,result.stderr)
def prepare(self,name='round-01'):
result=self.cli('prepare_capture.py','--measurements',self.root/'measured/measurements.json','--out-dir',self.root/name)
self.assertEqual(result.returncode,0,result.stderr)
return self.root/name
def capture(self,round_dir,shift=0):
plan=json.loads((round_dir/'capture-plan.json').read_text())
capture={**self.expected,'measurement_sha256':plan['measurement_sha256'],'document_height':64,'fonts_ready':True,'images_ready':True,'elements':[
{'name':'card','selector':'.card','count':1,'status':'measured','box':[shift,2,10,10]},
{'name':'ink','selector':'h1','count':1,'status':'measured','box':[0,20,32,20]}]}
self.write(round_dir/'capture.json',capture)
Image.new('RGB',(32,64),'white').save(round_dir/'candidate.png')
return capture
def verify(self,round_dir,previous=None,out='report'):
args=['--manifest',round_dir/'run.json','--out-dir',round_dir/out]
if previous:args+=['--previous',previous]
return self.cli('verify_capture.py',*args)
def test_full_pipeline_excludes_ink(self):
self.measure();r=self.prepare();self.capture(r)
result=self.verify(r);self.assertEqual(result.returncode,0,result.stderr)
data=json.loads((r/'report/verification.json').read_text())['geometry']
self.assertTrue(data['complete']);self.assertEqual(data['regions'][0]['delta_css'],[0,0,0,0])
self.assertEqual(data['regions'][1]['status'],'visual_only');self.assertNotIn('delta_css',data['regions'][1])
self.assertEqual(data['regions'][2]['status'],'reference_only')
self.assertTrue((r/'report/geometry.md').exists())
def test_prepare_requires_expected_and_mapping(self):
for i,change in enumerate([{'expected':None},{'css_viewport_width':16}]):
with self.subTest(change=change):
altered={**self.manifest,**change};self.write(self.root/'regions.json',altered)
m=self.root/f'm{i}';self.assertEqual(self.cli('measure_reference.py','--manifest',self.root/'regions.json','--out-dir',m).returncode,0)
out=self.root/f'r{i}';result=self.cli('prepare_capture.py','--measurements',m/'measurements.json','--out-dir',out)
self.assertEqual(result.returncode,2);self.assertFalse(out.exists())
def test_reference_mutation_rejected(self):
self.measure();Image.new('RGB',(32,64),'black').save(self.root/'reference.png')
result=self.cli('prepare_capture.py','--measurements',self.root/'measured/measurements.json','--out-dir',self.root/'r')
self.assertEqual(result.returncode,2);self.assertIn('参考图已改动',result.stderr)
def test_measurement_mutation_rejected(self):
self.measure();r=self.prepare();self.capture(r)
p=self.root/'measured/measurements.json';d=json.loads(p.read_text());d['regions'][0]['box'][0]=3;self.write(p,d)
result=self.verify(r);self.assertEqual(result.returncode,2);self.assertIn('基线已改动',result.stderr);self.assertFalse((r/'report').exists())
def test_old_collector_and_duplicate_rows_rejected(self):
self.measure();r=self.prepare();good=self.capture(r)
for i,c in enumerate([{**good,'measurement_sha256':'bad'},{**good,'elements':good['elements']+[good['elements'][0]]},{**good,'elements':good['elements'][1:]}]):
with self.subTest(index=i):
self.write(r/'capture.json',c);self.assertEqual(self.verify(r,out=f'bad-{i}').returncode,2);self.assertFalse((r/f'bad-{i}').exists())
def test_missing_hidden_and_ambiguous_are_not_success(self):
self.measure();r=self.prepare();good=self.capture(r)
for status in ['missing','hidden','ambiguous','unmeasurable','invalid_selector']:
with self.subTest(status=status):
rows=[{**good['elements'][0],'status':status,'count':2 if status=='ambiguous' else 0},good['elements'][1]]
self.write(r/'capture.json',{**good,'elements':rows})
result=self.verify(r,out=status);self.assertEqual(result.returncode,2)
geometry=json.loads((r/status/'verification.json').read_text())['geometry']
self.assertFalse(geometry['complete']);self.assertEqual(geometry['unavailable_regions'],['card'])
self.assertNotIn('delta_css',geometry['regions'][0])
def test_invalid_geometry_rejected(self):
self.measure();r=self.prepare();good=self.capture(r)
for i,row in enumerate([{**good['elements'][0],'count':2},{**good['elements'][0],'box':[0,0,-1,10]},{**good['elements'][0],'box':[float('nan'),0,10,10]}]):
self.write(r/'capture.json',{**good,'elements':[row,good['elements'][1]]})
self.assertEqual(self.verify(r,out=f'invalid-{i}').returncode,2)
def test_previous_reports_reduction_and_regression(self):
self.measure();r1=self.prepare();self.capture(r1,shift=3);self.assertEqual(self.verify(r1).returncode,0)
previous=r1/'report/verification.json'
for name,shift,change in [('better',1,'reduced'),('worse',6,'increased')]:
r=self.prepare(name);self.capture(r,shift=shift);result=self.verify(r,previous);self.assertEqual(result.returncode,0,result.stderr)
changes=json.loads((r/'report/verification.json').read_text())['geometry']['changes']
self.assertEqual(changes[0]['change'],change);self.assertEqual(changes[1]['change'],'unavailable')
def test_unrelated_previous_rejected(self):
self.measure();r=self.prepare();self.capture(r);self.assertEqual(self.verify(r).returncode,0)
p=r/'report/verification.json';d=json.loads(p.read_text());d['geometry']['measurement_sha256']='other';self.write(self.root/'unrelated.json',d)
result=self.verify(r,self.root/'unrelated.json',out='new');self.assertEqual(result.returncode,2);self.assertFalse((r/'new').exists())
def test_run_conditions_cannot_override_baseline(self):
self.measure();r=self.prepare();c=self.capture(r)
p=r/'run.json';d=json.loads(p.read_text());d['expected']['title']='后改标题';self.write(p,d)
c['title']='后改标题';self.write(r/'capture.json',c)
result=self.verify(r);self.assertEqual(result.returncode,2);self.assertIn('配置与测量基线',result.stderr)
def test_prepare_and_verify_refuse_overwrite(self):
self.measure();r=self.prepare();self.capture(r)
result=self.cli('prepare_capture.py','--measurements',self.root/'measured/measurements.json','--out-dir',r)
self.assertEqual(result.returncode,2)
with ThreadPoolExecutor(max_workers=2) as pool:
codes=sorted(pool.map(lambda _:self.verify(r).returncode,range(2)))
self.assertEqual(codes,[0,2]);self.assertTrue((r/'report/verification.json').exists())
def test_device_pixel_mapping(self):
self.manifest['css_viewport_width']=16;self.manifest['expected']={**self.expected,'viewport':{'width':16,'height':32},'dpr':2,'screenshot_scale':'device'}
self.measure();r=self.prepare();c=self.capture(r)
c.update(self.manifest['expected']);c['document_height']=32;c['elements'][0]['box']=[0,1,5,5]
self.write(r/'capture.json',c)
result=self.verify(r);self.assertEqual(result.returncode,0,result.stderr)
geometry=json.loads((r/'report/verification.json').read_text())['geometry'];self.assertEqual(geometry['regions'][0]['delta_css'],[0,0,0,0])
if __name__=='__main__':unittest.main()