AnasSarkiz/magnetic-shutter-remote-r8

A USB‑C rechargeable battery-powered ESP32‑C3 wireless controller with Li‑ion charging, protected 3.3‑V buck‑boost regulation, thermal/safety monitoring, status LEDs, buttons, programming UART, crystal clock, and 2.4‑GHz antenna matching network.

Version
0.4.0-prototype
License
unset
Stars
0

tests/cam/test_r9_filled_via_process.py

"""Keep intentional filled/capped ground vias distinct from unsafe pad overlaps."""
import copy,importlib.util,json,sys,unittest
from pathlib import Path
ROOT=Path(__file__).resolve().parents[2]
SPEC=importlib.util.spec_from_file_location('r9_process_test',ROOT/'scripts/r9-filled-via-process.py')
PROCESS=importlib.util.module_from_spec(SPEC);sys.modules[SPEC.name]=PROCESS;SPEC.loader.exec_module(PROCESS)
G=PROCESS.GEOMETRY

class FilledViaProcessTest(unittest.TestCase):
    def setUp(self):
        self.circuit=json.loads((ROOT/'dist/index/circuit.json').read_text())
        self.process=json.loads((ROOT/'fabrication/r9-process.json').read_text())
    def test_exact_ground_vias_are_conditionally_classified(self):
        copper,drills,_,ids=PROCESS.classify_process(self.circuit,self.process)
        self.assertEqual(len(ids),4)
        for drill in (d for d in drills if d.id in ids):
            pad=next(c for c in copper if c.id==drill.filled_capped_pad_id)
            self.assertEqual(G.drill_copper_rule(drill,pad),(None,'required_filled_capped_barrel_in_pad'))
            unfilled=copy.copy(drill);unfilled.filled_capped_pad_id=None
            self.assertEqual(G.drill_copper_rule(unfilled,pad),(.2,'project_unrelated_drill_copper'))
        self.assertFalse(self.process['intentional_via_in_pad']['supplier_processed_preview_confirmed'])
    def test_soldermask_plugging_is_not_filled_and_capped(self):
        self.process['intentional_via_in_pad']['required_process']='soldermask_plugged'
        with self.assertRaisesRegex(ValueError,'epoxy filling'):PROCESS.classify_process(self.circuit,self.process)
    def test_unlisted_pad_via_is_rejected(self):
        via=copy.deepcopy(next(e for e in self.circuit if e['type']=='pcb_via'))
        via.update({'pcb_via_id':'unlisted_ground_via','x':0,'y':-10.5})
        self.circuit.append(via)
        with self.assertRaisesRegex(ValueError,'Unqualified via-in-pad'):PROCESS.classify_process(self.circuit,self.process)
    def test_wrong_drill_size_is_rejected(self):
        via=next(e for e in self.circuit if e['type']=='pcb_via' and e['x']==-.5 and e['y']==-11)
        via['hole_diameter']=.2
        with self.assertRaisesRegex(ValueError,'0.3/0.45'):PROCESS.classify_process(self.circuit,self.process)
    def test_ordinary_via_must_have_requested_geometry(self):
        via=copy.deepcopy(next(e for e in self.circuit if e['type']=='pcb_via'))
        via.update({'pcb_via_id':'ordinary_test_via','x':-20,'y':0})
        self.circuit.append(via)
        via['outer_diameter']=.6
        with self.assertRaisesRegex(ValueError,'Nonconforming through-via'):PROCESS.classify_process(self.circuit,self.process)
    def test_blind_via_is_rejected(self):
        via=copy.deepcopy(next(e for e in self.circuit if e['type']=='pcb_via'))
        via.update({'pcb_via_id':'ordinary_test_via','x':-20,'y':0})
        self.circuit.append(via)
        via['layers']=['top','inner1']
        with self.assertRaisesRegex(ValueError,'Nonconforming through-via'):PROCESS.classify_process(self.circuit,self.process)
    def test_missing_declared_via_is_rejected(self):
        self.circuit=[e for e in self.circuit if not(e['type']=='pcb_via' and e['x']==-.5 and e['y']==-11)]
        with self.assertRaisesRegex(ValueError,'missing or duplicated'):PROCESS.classify_process(self.circuit,self.process)
    def test_different_net_cannot_be_classified_as_ground(self):
        copper,drills,_,ids=PROCESS.classify_process(self.circuit,self.process)
        drill=next(d for d in drills if d.id in ids)
        pad=next(c for c in copper if c.id==drill.filled_capped_pad_id)
        altered=copy.copy(drill);altered.net='unrelated_signal'
        with self.assertRaisesRegex(ValueError,'Invalid filled/capped'):G.drill_copper_rule(altered,pad)

class SourceTraceAttributionTest(unittest.TestCase):
    def test_unkeyed_single_port_via_uses_canonical_ground_membership(self):
        circuit=[{'type':'source_port','source_port_id':'ground_port','subcircuit_connectivity_map_key':'ground'},
                 {'type':'source_trace','source_trace_id':'via_connection','connected_source_port_ids':['ground_port']},
                 {'type':'pcb_via','pcb_via_id':'via','source_trace_id':'via_connection','x':0,'y':0,'hole_diameter':.3,'outer_diameter':.45,'layers':['top','bottom']}]
        copper,drills,_=G.make_features(circuit)
        self.assertEqual(drills[0].net,'ground')
        self.assertTrue(all(c.net=='ground' for c in copper))
    def test_unkeyed_trace_cannot_merge_different_resolved_nets(self):
        circuit=[{'type':'source_port','source_port_id':'ground_port','subcircuit_connectivity_map_key':'ground'},
                 {'type':'source_port','source_port_id':'signal_port','subcircuit_connectivity_map_key':'signal'},
                 {'type':'source_trace','source_trace_id':'bad_connection','connected_source_port_ids':['ground_port','signal_port']}]
        with self.assertRaisesRegex(ValueError,'Conflicting source net'):G.make_features(circuit)
    def test_unknown_port_does_not_acquire_a_guessed_net(self):
        circuit=[{'type':'source_trace','source_trace_id':'unresolved','connected_source_port_ids':['unknown']},
                 {'type':'pcb_via','pcb_via_id':'via','source_trace_id':'unresolved','x':0,'y':0,'hole_diameter':.3,'outer_diameter':.45,'layers':['top','bottom']}]
        _,drills,_=G.make_features(circuit)
        self.assertIsNone(drills[0].net)
if __name__=='__main__':unittest.main()