1111
1212import tidy3d as td
1313from tidy3d .exceptions import FileError
14- from tidy3d .plugins .klayout .drc .drc import DRCRunner
14+ from tidy3d .plugins .klayout .drc .drc import DRCConfig , DRCRunner , run_drc_on_gds
1515from tidy3d .plugins .klayout .drc .results import DRCResults , parse_violation_value
1616from tidy3d .plugins .klayout .util import check_installation
1717
1818filepath = Path (os .path .dirname (os .path .abspath (__file__ )))
19+ KLAYOUT_PLUGIN_PATH = "tidy3d.plugins.klayout"
20+
21+
22+ def _basic_drc_config_kwargs (tmp_path : Path ) -> dict [str , Path | bool ]:
23+ """Return minimal kwargs needed to instantiate DRCConfig in tests."""
24+
25+ drc_runset = tmp_path / "test.drc"
26+ drc_runset .write_text ('source($gdsfile)\n report("DRC", $resultsfile)\n ' )
27+ gdsfile = tmp_path / "test.gds"
28+ gdsfile .write_text ("" )
29+ resultsfile = tmp_path / "results.lyrdb"
30+ return {
31+ "gdsfile" : gdsfile ,
32+ "drc_runset" : drc_runset ,
33+ "resultsfile" : resultsfile ,
34+ "verbose" : False ,
35+ }
1936
2037
2138def test_check_klayout_not_installed (monkeypatch ):
2239 """check_installation raises when KLayout is not on PATH.
2340
2441 Use monkeypatch to simulate absence, avoiding reliance on CI environment.
2542 """
26- monkeypatch .setattr ("tidy3d.plugins.klayout .util.which" , lambda _cmd : None )
43+ monkeypatch .setattr (f" { KLAYOUT_PLUGIN_PATH } .util.which" , lambda _cmd : None )
2744 with pytest .raises (RuntimeError ):
2845 check_installation (raise_error = True )
2946
3047
3148def test_check_klayout_installed (monkeypatch ):
3249 """check_installation returns a path and does not raise when present."""
3350 fake_path = "/usr/local/bin/klayout"
34- monkeypatch .setattr ("tidy3d.plugins.klayout .util.which" , lambda _cmd : fake_path )
51+ monkeypatch .setattr (f" { KLAYOUT_PLUGIN_PATH } .util.which" , lambda _cmd : fake_path )
3552 assert check_installation (raise_error = True ) == fake_path
3653
3754
55+ def test_runner_passes_drc_args_to_config (monkeypatch , tmp_path ):
56+ """Ensure DRCRunner forwards drc_args into the generated DRCConfig."""
57+
58+ drc_runset = tmp_path / "test.drc"
59+ drc_runset .write_text ('source($gdsfile)\n report("DRC", $resultsfile)\n ' )
60+ gdsfile = tmp_path / "test.gds"
61+ gdsfile .write_text ("" )
62+ resultsfile = tmp_path / "results.lyrdb"
63+ captured_config = {}
64+
65+ def mock_run_drc_on_gds (config ):
66+ captured_config ["config" ] = config
67+ return DRCResults .load (filepath / "drc_results.lyrdb" )
68+
69+ monkeypatch .setattr (f"{ KLAYOUT_PLUGIN_PATH } .drc.drc.run_drc_on_gds" , mock_run_drc_on_gds )
70+
71+ runner = DRCRunner (drc_runset = drc_runset , verbose = False )
72+ user_args = {"foo" : "bar" , "baz" : "1" }
73+ runner .run (source = gdsfile , resultsfile = resultsfile , drc_args = user_args )
74+
75+ assert captured_config ["config" ].drc_args == user_args
76+
77+
78+ def test_run_drc_on_gds_appends_custom_args (monkeypatch , tmp_path ):
79+ """run_drc_on_gds adds extra -rd pairs for drc_args."""
80+
81+ drc_runset = tmp_path / "test.drc"
82+ drc_runset .write_text ('source($gdsfile)\n report("DRC", $resultsfile)\n ' )
83+ gdsfile = tmp_path / "test.gds"
84+ gdsfile .write_text ("" )
85+ resultsfile = tmp_path / "results.lyrdb"
86+
87+ monkeypatch .setattr (f"{ KLAYOUT_PLUGIN_PATH } .drc.drc.check_installation" , lambda ** _ : None )
88+
89+ captured_cmd = {}
90+
91+ class DummyCompleted :
92+ def __init__ (self ):
93+ self .returncode = 0
94+ self .stdout = b""
95+ self .stderr = b""
96+
97+ def fake_run (cmd , capture_output ):
98+ captured_cmd ["cmd" ] = cmd
99+ return DummyCompleted ()
100+
101+ monkeypatch .setattr (f"{ KLAYOUT_PLUGIN_PATH } .drc.drc.run" , fake_run )
102+ monkeypatch .setattr (
103+ f"{ KLAYOUT_PLUGIN_PATH } .drc.drc.DRCResults.load" ,
104+ lambda resultsfile : DRCResults (violations_by_category = {}),
105+ )
106+
107+ config = DRCConfig (
108+ gdsfile = gdsfile ,
109+ drc_runset = drc_runset ,
110+ resultsfile = resultsfile ,
111+ verbose = False ,
112+ drc_args = {"string_arg" : "text" , "numeric_value" : 1 },
113+ )
114+
115+ run_drc_on_gds (config )
116+
117+ expected_tail = ["-rd" , "string_arg=text" , "-rd" , "numeric_value=1" ]
118+ assert captured_cmd ["cmd" ][- len (expected_tail ) :] == expected_tail
119+
120+
121+ def test_drc_config_args_require_mapping (tmp_path ):
122+ """drc_args must be a mapping and refuses other iterables."""
123+
124+ kwargs = _basic_drc_config_kwargs (tmp_path )
125+ with pytest .raises (pd .ValidationError ):
126+ DRCConfig (** kwargs , drc_args = ["not" , "a" , "mapping" ])
127+
128+
129+ def test_drc_config_args_reject_reserved_keys (tmp_path ):
130+ """Reserved keys such as gdsfile cannot be overridden via drc_args."""
131+
132+ kwargs = _basic_drc_config_kwargs (tmp_path )
133+ with pytest .raises (pd .ValidationError ):
134+ DRCConfig (** kwargs , drc_args = {"gdsfile" : "custom.gds" })
135+
136+
137+ def test_drc_config_args_stringify_values (tmp_path ):
138+ """Non-string keys and values are coerced to strings by the validator."""
139+
140+ kwargs = _basic_drc_config_kwargs (tmp_path )
141+ config = DRCConfig (** kwargs , drc_args = {1 : Path ("foo" ), "flag" : True })
142+
143+ assert config .drc_args == {"1" : "foo" , "flag" : "True" }
144+
145+
146+ def test_drc_config_args_unstringifiable_value (tmp_path ):
147+ """Non-stringifiable drc_args values should raise a ValidationError."""
148+
149+ class Unstringifiable :
150+ def __str__ (self ):
151+ raise RuntimeError ("cannot stringify" )
152+
153+ kwargs = _basic_drc_config_kwargs (tmp_path )
154+
155+ with pytest .raises (
156+ pd .ValidationError , match = "Could not coerce keys and values of drc_args to strings."
157+ ):
158+ DRCConfig (** kwargs , drc_args = {"bad" : Unstringifiable ()})
159+
160+
38161class TestDRCRunner :
39162 """Test DRCRunner"""
40163
@@ -147,6 +270,7 @@ def run(
147270 source ,
148271 td_object_gds_savefile ,
149272 resultsfile ,
273+ drc_args = None ,
150274 ** to_gds_file_kwargs ,
151275 ):
152276 """Calls DRCRunner.run with dummy run_drc_on_gds()"""
@@ -155,13 +279,14 @@ def run(
155279 def mock_run_drc_on_gds (config ):
156280 return DRCResults .load (filepath / "drc_results.lyrdb" )
157281
158- monkeypatch .setattr ("tidy3d.plugins.klayout .drc.drc.run_drc_on_gds" , mock_run_drc_on_gds )
282+ monkeypatch .setattr (f" { KLAYOUT_PLUGIN_PATH } .drc.drc.run_drc_on_gds" , mock_run_drc_on_gds )
159283
160284 runner = DRCRunner (drc_runset = drc_runsetfile , verbose = verbose )
161285 return runner .run (
162286 source = source ,
163287 td_object_gds_savefile = td_object_gds_savefile ,
164288 resultsfile = resultsfile ,
289+ drc_args = drc_args ,
165290 ** to_gds_file_kwargs ,
166291 )
167292
0 commit comments