import copy import json import os import struct import time from pathlib import Path from xml.etree.ElementTree import fromstring import pytest from sqlalchemy import update from app.documents import parse_document from app.parser.record_decoder import ParseError from app.parser.pcb_binary import Reader, at, binary_records, wide_strings from app.parser.pcb_container import identify_pcb from app.parser.pcb_geometry import unit,raw_unit,pad_shape from app.models.schematic import Point from app.render import svg_document from app.pcb_diff import compare_pcbs from app.db import Job,claim,sessions from app.storage import storage from app.worker import process from pcb_fixture import pcb from test_platform import client,KEY,upload def parse(raw):return parse_document(raw,'board.PcbDoc','a'*48,document_type='pcb') def create(client,raw,compare=False,**fields): files={'old_file':('old.PcbDoc',raw),'new_file':('new.PcbDoc',fields.pop('new',raw))} if compare else {'file':('board.PcbDoc',raw)} return client.post('/api/v1/pcbs/comparisons' if compare else '/api/v1/pcbs/render',headers={'X-API-Key':KEY},files=files,data=fields) def test_pcb_units_and_checked_framing(): assert raw_unit(100000)==1 and unit('2.54mm')==pytest.approx(10) for bad in ['nan','infinity','999999999mm']: with pytest.raises(ValueError):unit(bad) with pytest.raises(ParseError):Reader(b'\x01').read('I') with pytest.raises(ParseError):at(struct.pack('0 and len(doc.primitives)>0 assert len({p.id for p in doc.primitives})==len(doc.primitives) assert compare_pcbs(doc,doc)['total_changed']==0 assert any(p.role=='pad' for p in doc.primitives) assert any(p.role=='drill' for p in doc.primitives) assert all(set(o.layer_ids)<={layer.id for layer in doc.layers} for o in doc.objects) def test_pad_stack_slot_via_layers_and_native_ids(): from pcb_fixture import rich_pcb doc=parse(rich_pcb());pad=next(o for o in doc.objects if o.kind=='pad');via=next(o for o in doc.objects if o.kind=='via') assert pad.native_uid=='PAD_UID' and pad.component_id==doc.components[0].id assert pad.layer_ids==['1','32'] and via.layer_ids==['1','32'] pads=[p for p in doc.primitives if p.object_id==pad.id and p.role=='pad'] assert {p.layer_id for p in pads}=={'1','32'} assert next(p for p in pads if p.layer_id=='1').bounds.width==1 assert next(p for p in pads if p.layer_id=='32').bounds.width==2 slot=next(p for p in doc.primitives if p.object_id==pad.id and p.role=='drill') assert slot.bounds.width==1 and slot.bounds.height==.5 and slot.layer_ids==['1','32'] bottom_via=next(p for p in doc.primitives if p.object_id==via.id and p.layer_id=='32') assert bottom_via.bounds.width==2 def test_footprint_move_rotation_and_child_edits_are_one_change(): from pcb_fixture import rich_pcb from app.parser.pcb_geometry import pad_shape old=parse(rich_pcb());new=old.model_copy(deep=True) component=new.components[0];component.position.x+=4 for o in new.objects: if o.component_id==component.id:o.position.x+=4;o.bounds.x+=4 # Paths translated independently from their existing SVG string. for p in new.primitives: if p.component_id==component.id: if p.role=='pad': w,h=(1,2) if p.layer_id=='1' else (2,3) p.path,p.bounds=pad_shape(Point(x=5,y=-1),w,h,1 if p.layer_id=='1' else 3,0) elif p.role=='drill':p.path,p.bounds=pad_shape(Point(x=5,y=-1),1,.5,1,0) # Source parameters retain physical placement too. component.parameters['X']='50mil' result=compare_pcbs(old,new) assert result['summary']==dict(added=0,removed=0,modified=0,moved=1),result['changes'] component.value='new value' result=compare_pcbs(old,new) assert result['summary']['modified']==1 and result['changes'][0]['moved'] # Reparse native revision with a rotated footprint/pad: only one footprint modification. rotated=parse(rich_pcb());rotated.components[0].rotation=90 assert compare_pcbs(old,rotated)['summary']['modified']==1 def test_record_reordering_net_renumber_and_duplicate_ambiguity(): import io import olefile from pcb_fixture import cfb,text,record def native(reverse): with olefile.OleFileIO(io.BytesIO(pcb())) as ole:streams={'/'.join(e):ole.openstream(e).read() for e in ole.listdir()} streams['Nets6/Data']=text({'NAME':'VCC'})+text({'NAME':'GND'}) if reverse else text({'NAME':'GND'})+text({'NAME':'VCC'}) records=[] for x,net in [(100000,0),(900000,1)]: p=bytearray(45);p[0]=1 struct.pack_into('