修: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:
1 parent
e03465c398
commit
237a09a5b0
161 files changed
+19429
-2
No files matched your search
@@ -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()
|
||||
Reference in new issue
Block a user