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/test_side_shutter.py

"""Manufacturer land/pin regression; does not treat generic4pin buttons alike."""
import importlib.util
import json
import tempfile
import unittest
from pathlib import Path

ROOT = Path(__file__).resolve().parents[1]
SPEC = importlib.util.spec_from_file_location('side_switch',ROOT/'scripts/audit-side-shutter.py')
AUDIT = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(AUDIT)


class SideShutterQualification(unittest.TestCase):
    def test_rejected_alps_exact_manufacturer_lands(self):
        report=AUDIT.audit_alps()
        self.assertLess(report['supplier_import_conversion_max_difference_mm'],1e-7)
        self.assertEqual(report['internal_connections'],[['pin1','pin3'],['pin4','pin5']])
        self.assertEqual(report['pads']['4']['width_mm'],1.2999974)

    def test_rejected_alps_five_contacts_have_paste_and_correct_internals(self):
        fixture=json.loads((ROOT/'evidence/R8-side-shutter-2026-10-05/qualification/alps/fixture/circuit.json').read_text())
        ports={p['source_port_id']:p['pin_number'] for p in fixture if p['type']=='source_port'}
        connections=[e for e in fixture if e['type']=='source_component_internal_connection']
        self.assertEqual([{ports[p] for p in e['source_port_ids']} for e in connections],[{1,3},{4,5}])
        self.assertEqual(len([e for e in fixture if e['type']=='pcb_solder_paste']),5)
        self.assertFalse([e for e in fixture if e['type'].endswith('_error')])

    def test_alps_rejection_preserves_all_npth_violations(self):
        rejection=json.loads((ROOT/'evidence/R8-side-shutter-2026-10-05/qualification/alps/manufacturing-rejection.json').read_text())
        self.assertEqual(len(rejection['failures']),4)
        self.assertEqual({e['rule'] for e in rejection['failures']},{'npth_all_copper'})
        self.assertLess(min(e['gap_mm'] for e in rejection['failures']),.08)

    def test_manufacturer_lands_and_frame_contract(self):
        report = AUDIT.audit_switch()
        self.assertLess(report['supplier_import_conversion_max_difference_mm'],1e-7)
        self.assertEqual(report['internal_support_connection'],['pin3','pin4'])
        self.assertTrue(report['normally_open'])
        self.assertEqual(report['contact_land_recommendation_mm']['pitch'],3.4)

    def test_modified_generated_contact_rejected(self):
        with tempfile.TemporaryDirectory() as directory:
            original = ROOT/'imports/TS24CA/TS24CA.tsx'
            altered = Path(directory)/original.name
            altered.write_text(original.read_text().replace('width="0.5999988mm"','width="0.9mm"',1))
            with self.assertRaisesRegex(ValueError,'conversion differs'):
                AUDIT.audit_switch(import_path=altered)

    def test_exact_supplier_identity_required(self):
        with tempfile.TemporaryDirectory() as directory:
            original = ROOT/'imports/TS24CA/TS24CA.tsx'
            altered = Path(directory)/original.name
            altered.write_text(original.read_text().replace('"C393942"','"C000000"'))
            with self.assertRaisesRegex(ValueError,'identity required'):
                AUDIT.audit_switch(import_path=altered)

    def test_requalified_switch_has_paste_for_every_unchanged_land(self):
        fixture=json.loads((ROOT/'evidence/R8-side-shutter-2026-10-05/qualification/fixture-paste-qualified/circuit.json').read_text())
        pads={p['pcb_smtpad_id']:p for p in fixture if p['type']=='pcb_smtpad'}
        pastes={p['pcb_smtpad_id']:p for p in fixture if p['type']=='pcb_solder_paste'}
        self.assertEqual(set(pads),set(pastes))
        self.assertEqual(len(pastes),4)
        for pad_id,pad in pads.items():
            if pad['shape']=='polygon':
                self.assertEqual(pad['points'],pastes[pad_id]['points'])

    def test_frame_is_not_a_switch_contact(self):
        fixture = json.loads((ROOT/'evidence/R8-side-shutter-2026-10-05/qualification/fixture/circuit.json').read_text())
        ports = {p['source_port_id']:p['pin_number'] for p in fixture if p['type']=='source_port'}
        connections = [e for e in fixture if e['type']=='source_component_internal_connection']
        self.assertEqual([{ports[p] for p in e['source_port_ids']} for e in connections],[{3,4}])
        self.assertFalse([e for e in fixture if e['type'].endswith('_error')])

    def test_side_actuator_overhang_keeps_every_copper_land_on_board(self):
        fixture = json.loads((ROOT/'evidence/R8-side-shutter-2026-10-05/qualification/fixture/circuit.json').read_text())
        component, = [e for e in fixture if e['type']=='pcb_component']
        self.assertEqual(component['rotation'],270)
        self.assertEqual(component['display_offset_x'],'22mm')
        self.assertTrue(component['is_allowed_to_be_off_board'])
        self.assertAlmostEqual(component['center']['x'],22,places=6)
        for pad in [e for e in fixture if e['type']=='pcb_smtpad']:
            right_edge = max(p['x'] for p in pad['points']) if pad['shape']=='polygon' else pad['x']+pad['width']/2
            self.assertLessEqual(right_edge,23.7)  #24mm edge,0.30mm copper clearance.


if __name__ == '__main__':
    unittest.main()