KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_drc_rule_editor.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright (C) 2024 KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, you may find one here:
18 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
19 * or you may search the http://www.gnu.org website for the version 2 license,
20 * or you may write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
22 */
23
24#include <boost/test/unit_test.hpp>
25#include <wx/ffile.h>
26#include <core/typeinfo.h>
37#include <drc/drc_rule.h>
45
46BOOST_AUTO_TEST_SUITE( DRC_RULE_EDITOR )
47
48BOOST_AUTO_TEST_CASE( RoundTripViaStyle )
49{
50 DRC_RE_VIA_STYLE_CONSTRAINT_DATA original( 0, 0, "My_Via_Rule", 0.5, 0.8, 0.6, 0.2, 0.4, 0.3 );
51 original.SetConstraintCode( "via_style" );
52 original.SetRuleCondition( "A.NetClass == 'Power'" );
53
55 ctx.ruleName = original.GetRuleName();
57 ctx.constraintCode = original.GetConstraintCode();
58
59 wxString ruleText = original.GenerateRule( ctx );
60
61 auto parsedRules = DRC_RULE_EDITOR_UTILS::ParseRules( ruleText );
62
63 BOOST_REQUIRE_EQUAL( parsedRules.size(), 1 );
64 auto parsed = std::dynamic_pointer_cast<DRC_RE_VIA_STYLE_CONSTRAINT_DATA>( parsedRules[0] );
65 BOOST_REQUIRE( parsed );
66
67 BOOST_CHECK_EQUAL( parsed->GetRuleName(), original.GetRuleName() );
68 BOOST_CHECK_EQUAL( parsed->GetRuleCondition(), original.GetRuleCondition() );
69 BOOST_CHECK_CLOSE( parsed->GetMinViaDiameter(), original.GetMinViaDiameter(), 0.0001 );
70 BOOST_CHECK_CLOSE( parsed->GetMaxViaDiameter(), original.GetMaxViaDiameter(), 0.0001 );
71 BOOST_CHECK_CLOSE( parsed->GetPreferredViaDiameter(), original.GetPreferredViaDiameter(), 0.0001 );
72 BOOST_CHECK_CLOSE( parsed->GetMinViaHoleSize(), original.GetMinViaHoleSize(), 0.0001 );
73 BOOST_CHECK_CLOSE( parsed->GetMaxViaHoleSize(), original.GetMaxViaHoleSize(), 0.0001 );
74 BOOST_CHECK_CLOSE( parsed->GetPreferredViaHoleSize(), original.GetPreferredViaHoleSize(), 0.0001 );
75}
76
77BOOST_AUTO_TEST_CASE( RoundTripRoutingWidth )
78{
79 DRC_RE_ROUTING_WIDTH_CONSTRAINT_DATA original( 0, 0, "My_Track_Rule", 0.2, 0.3, 0.5 );
80 original.SetConstraintCode( "track_width" );
81 original.SetRuleCondition( "A.NetClass == 'Signal'" );
82
84 ctx.ruleName = original.GetRuleName();
86 ctx.constraintCode = original.GetConstraintCode();
87
88 wxString ruleText = original.GenerateRule( ctx );
89
90 auto parsedRules = DRC_RULE_EDITOR_UTILS::ParseRules( ruleText );
91
92 BOOST_REQUIRE_EQUAL( parsedRules.size(), 1 );
93 auto parsed = std::dynamic_pointer_cast<DRC_RE_ROUTING_WIDTH_CONSTRAINT_DATA>( parsedRules[0] );
94 BOOST_REQUIRE( parsed );
95
96 BOOST_CHECK_EQUAL( parsed->GetRuleName(), original.GetRuleName() );
97 BOOST_CHECK_EQUAL( parsed->GetRuleCondition(), original.GetRuleCondition() );
98 BOOST_CHECK_CLOSE( parsed->GetMinRoutingWidth(), original.GetMinRoutingWidth(), 0.0001 );
99 BOOST_CHECK_CLOSE( parsed->GetMaxRoutingWidth(), original.GetMaxRoutingWidth(), 0.0001 );
100 BOOST_CHECK_CLOSE( parsed->GetPreferredRoutingWidth(), original.GetPreferredRoutingWidth(), 0.0001 );
101}
102
104{
105 std::vector<std::shared_ptr<DRC_RE_BASE_CONSTRAINT_DATA>> rules;
106
107 auto rule1 = std::make_shared<DRC_RE_VIA_STYLE_CONSTRAINT_DATA>( 0, 0, "ViaRule", 0.5, 0.8, 0.6, 0.2, 0.4, 0.3 );
108 rule1->SetConstraintCode( "via_style" );
109 rule1->SetRuleCondition( "A.NetClass == 'Power'" );
110 rules.push_back( rule1 );
111
112 auto rule2 = std::make_shared<DRC_RE_ROUTING_WIDTH_CONSTRAINT_DATA>( 0, 0, "TrackRule", 0.2, 0.3, 0.5 );
113 rule2->SetConstraintCode( "track_width" );
114 rule2->SetRuleCondition( "A.NetClass == 'Signal'" );
115 rules.push_back( rule2 );
116
117 wxString filename = "test_rules.dru";
118
119 // We pass nullptr for board, so layers won't be saved, which is fine for this test
120 bool result = DRC_RULE_EDITOR_UTILS::SaveRules( filename, rules, nullptr );
122
123 wxFFile file( filename );
124 BOOST_REQUIRE( file.IsOpened() );
125 wxString content;
126 file.ReadAll( &content );
127
128 // Verify content
129 BOOST_CHECK( content.Contains( "(version 1)" ) );
130 BOOST_CHECK( content.Contains( "(rule ViaRule" ) );
131 BOOST_CHECK( content.Contains( "(constraint via_diameter" ) );
132 BOOST_CHECK( content.Contains( "(constraint hole_size" ) );
133 BOOST_CHECK( content.Contains( "(rule TrackRule" ) );
134 BOOST_CHECK( content.Contains( "(constraint track_width" ) );
135
136 // Clean up
137 wxRemoveFile( filename );
138}
139
140BOOST_AUTO_TEST_CASE( SaveRulesToFileLogic )
141{
142 // Mimic m_ruleTreeNodeDatas
143 std::vector<RULE_TREE_NODE> ruleTreeNodeDatas;
144
145 // Add a rule node
146 RULE_TREE_NODE node;
147 node.m_nodeId = 1;
148 node.m_nodeName = "TestRule";
149 node.m_nodeType = RULE; // 4
150
151 auto data = std::make_shared<DRC_RE_BASE_CONSTRAINT_DATA>( 1, 0, "TestRule" );
152 data->SetConstraintCode( "clearance" );
153 node.m_nodeData = data;
154
155 ruleTreeNodeDatas.push_back( node );
156
157 // Mimic SaveRulesToFile logic
158 std::vector<std::shared_ptr<DRC_RE_BASE_CONSTRAINT_DATA>> rules;
159
160 for( const RULE_TREE_NODE& n : ruleTreeNodeDatas )
161 {
162 if( n.m_nodeType != RULE )
163 continue;
164
165 auto d = std::dynamic_pointer_cast<DRC_RE_BASE_CONSTRAINT_DATA>( n.m_nodeData );
166
167 if( d )
168 rules.push_back( d );
169 }
170
171 BOOST_CHECK_EQUAL( rules.size(), 1 );
172}
173
174BOOST_AUTO_TEST_CASE( SaveNumericRules )
175{
176 auto rule = std::make_shared<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>( 0, 0, 0.2, "ClearanceRule" );
177 rule->SetConstraintCode( "clearance" );
178
180 ctx.ruleName = rule->GetRuleName();
181 ctx.constraintCode = rule->GetConstraintCode();
182
183 wxString ruleText = rule->GenerateRule( ctx );
184
185 BOOST_CHECK( ruleText.Contains( "(constraint clearance (min 0.2mm))" ) );
186
187 // Test via_count (max, unitless)
188 auto rule2 = std::make_shared<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>( 0, 0, 10, "ViaCountRule" );
189 rule2->SetConstraintCode( "via_count" );
190 ctx.ruleName = rule2->GetRuleName();
191 ctx.constraintCode = rule2->GetConstraintCode();
192
193 ruleText = rule2->GenerateRule( ctx );
194 BOOST_CHECK( ruleText.Contains( "(constraint via_count (max 10))" ) );
195
196 // Test track_angle (min, deg)
197 auto rule3 = std::make_shared<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>( 0, 0, 45, "AngleRule" );
198 rule3->SetConstraintCode( "track_angle" );
199 ctx.ruleName = rule3->GetRuleName();
200 ctx.constraintCode = rule3->GetConstraintCode();
201
202 ruleText = rule3->GenerateRule( ctx );
203 BOOST_CHECK( ruleText.Contains( "(constraint track_angle (min 45deg))" ) );
204}
205
206BOOST_AUTO_TEST_CASE( SaveBoolRules )
207{
208 auto rule = std::make_shared<DRC_RE_BOOL_INPUT_CONSTRAINT_DATA>( 0, 0, true, "DisallowTestRule" );
209 rule->SetConstraintCode( "disallow" );
210
212 ctx.ruleName = rule->GetRuleName();
213 ctx.constraintCode = rule->GetConstraintCode();
214
215 wxString ruleText = rule->GenerateRule( ctx );
216
217 BOOST_CHECK( ruleText.Contains( "(constraint disallow)" ) );
218
219 // Test false value (should not generate constraint)
220 auto rule2 = std::make_shared<DRC_RE_BOOL_INPUT_CONSTRAINT_DATA>( 0, 0, false, "NoDisallowTestRule" );
221 rule2->SetConstraintCode( "disallow" );
222 ctx.ruleName = rule2->GetRuleName();
223 ctx.constraintCode = rule2->GetConstraintCode();
224
225 ruleText = rule2->GenerateRule( ctx );
226 BOOST_CHECK( ruleText.IsEmpty() );
227}
228
229BOOST_AUTO_TEST_CASE( ParseRulesCategories )
230{
231 wxString rules =
232 "(version 1)\n"
233 "(rule \"Clearance Rule\"\n"
234 "\t(constraint clearance (min 0.2mm))\n"
235 "\t(condition \"A.NetClass == 'Power'\"))\n"
236 "(rule \"Via Style Rule\"\n"
237 "\t(constraint via_diameter (min 0.5mm))\n"
238 "\t(constraint hole_size (min 0.3mm))\n"
239 "\t(condition \"A.NetClass == 'Power'\"))\n";
240
241 auto parsedRules = DRC_RULE_EDITOR_UTILS::ParseRules( rules );
242
243 BOOST_REQUIRE_EQUAL( parsedRules.size(), 2 );
244
245 // Check Clearance Rule
246 auto clearanceRule = parsedRules[0];
247 BOOST_CHECK_EQUAL( clearanceRule->GetRuleName(), "Clearance Rule" );
248 BOOST_CHECK_EQUAL( clearanceRule->GetConstraintCode(), "clearance" );
249
250 // Verify it is a numeric constraint data
251 auto numericData = std::dynamic_pointer_cast<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>( clearanceRule );
252 BOOST_REQUIRE( numericData );
253 BOOST_CHECK_CLOSE( numericData->GetNumericInputValue(), 0.2, 0.0001 );
254
255 // Check Via Style Rule
256 auto viaStyleRule = parsedRules[1];
257 BOOST_CHECK_EQUAL( viaStyleRule->GetRuleName(), "Via Style Rule" );
258 BOOST_CHECK_EQUAL( viaStyleRule->GetConstraintCode(), "via_style" );
259
260 auto viaData = std::dynamic_pointer_cast<DRC_RE_VIA_STYLE_CONSTRAINT_DATA>( viaStyleRule );
261 BOOST_REQUIRE( viaData );
262 BOOST_CHECK_CLOSE( viaData->GetMinViaDiameter(), 0.5, 0.0001 );
263 BOOST_CHECK_CLOSE( viaData->GetMinViaHoleSize(), 0.3, 0.0001 );
264}
265
266BOOST_AUTO_TEST_CASE( FactoryRegistration )
267{
268 // Register a custom parser for 'clearance' to override default
269 bool parserCalled = false;
271 [&]( const std::shared_ptr<DRC_RULE>& aRule ) -> std::shared_ptr<DRC_RE_BASE_CONSTRAINT_DATA>
272 {
273 if( aRule->FindConstraint( CLEARANCE_CONSTRAINT ) )
274 {
275 parserCalled = true;
276 auto data = std::make_shared<DRC_RE_CUSTOM_RULE_CONSTRAINT_DATA>( 0, 0, aRule->m_Name );
277 data->SetConstraintCode( "custom_clearance" );
278 return data;
279 }
280 return nullptr;
281 } );
282
283 wxString ruleText = "(version 1) (rule \"Test Rule\" (constraint clearance (min 1.0mm)) (condition \"A.Type == 'Pad'\"))";
284 auto parsedRules = DRC_RULE_EDITOR_UTILS::ParseRules( ruleText );
285
286 BOOST_CHECK( parserCalled );
287 BOOST_REQUIRE_EQUAL( parsedRules.size(), 1 );
288 BOOST_CHECK_EQUAL( parsedRules[0]->GetConstraintCode(), "custom_clearance" );
289}
290
291BOOST_AUTO_TEST_CASE( ValidateViaStyleValid )
292{
293 DRC_RE_VIA_STYLE_CONSTRAINT_DATA data( 0, 0, "ValidRule", 0.5, 0.8, 0.6, 0.2, 0.4, 0.3 );
294
296
297 BOOST_CHECK( result.isValid );
298 BOOST_CHECK( result.errors.empty() );
299}
300
301BOOST_AUTO_TEST_CASE( ValidateViaStyleInvalidMinGreaterThanMax )
302{
303 // min > max for via diameter
304 DRC_RE_VIA_STYLE_CONSTRAINT_DATA data( 0, 0, "InvalidRule", 0.9, 0.5, 0.6, 0.2, 0.4, 0.3 );
305
307
308 BOOST_CHECK( !result.isValid );
309 BOOST_CHECK( !result.errors.empty() );
310
311 // Should have error about min > max
312 bool foundMinMaxError = false;
313 for( const auto& error : result.errors )
314 {
315 if( error.find( "Minimum Via Diameter cannot be greater than Maximum Via Diameter" ) != std::string::npos )
316 foundMinMaxError = true;
317 }
318 BOOST_CHECK( foundMinMaxError );
319}
320
321BOOST_AUTO_TEST_CASE( ValidateViaStyleInvalidNegativeValues )
322{
323 // Negative values
324 DRC_RE_VIA_STYLE_CONSTRAINT_DATA data( 0, 0, "NegativeRule", -0.5, 0.8, 0.6, 0.2, 0.4, 0.3 );
325
327
328 BOOST_CHECK( !result.isValid );
329 BOOST_CHECK( !result.errors.empty() );
330
331 // Should have error about negative value
332 bool foundNegativeError = false;
333 for( const auto& error : result.errors )
334 {
335 if( error.find( "must be greater than 0" ) != std::string::npos )
336 foundNegativeError = true;
337 }
338 BOOST_CHECK( foundNegativeError );
339}
340
341BOOST_AUTO_TEST_CASE( FactoryOverwrite )
342{
343 // Register a parser
345 []( const std::shared_ptr<DRC_RULE>& aRule ) -> std::shared_ptr<DRC_RE_BASE_CONSTRAINT_DATA> {
346 if( aRule->m_Name == "Test" )
347 return std::make_shared<DRC_RE_CUSTOM_RULE_CONSTRAINT_DATA>( 0, 0, aRule->m_Name + "_1" );
348 return nullptr;
349 } );
350
351 // Overwrite it (prepend)
353 []( const std::shared_ptr<DRC_RULE>& aRule ) -> std::shared_ptr<DRC_RE_BASE_CONSTRAINT_DATA> {
354 if( aRule->m_Name == "Test" )
355 return std::make_shared<DRC_RE_CUSTOM_RULE_CONSTRAINT_DATA>( 0, 0, aRule->m_Name + "_2" );
356 return nullptr;
357 } );
358
359 wxString ruleText = "(version 1) (rule \"Test\" (constraint clearance (min 1.0mm)))";
360 auto parsedRules = DRC_RULE_EDITOR_UTILS::ParseRules( ruleText );
361
362 BOOST_REQUIRE_EQUAL( parsedRules.size(), 1 );
363 BOOST_CHECK_EQUAL( parsedRules[0]->GetRuleName(), "Test_2" );
364}
365
366BOOST_AUTO_TEST_CASE( ValidateAbsLengthTwoValid )
367{
368 // Valid: min < opt < max, all positive
369 DRC_RE_ABSOLUTE_LENGTH_TWO_CONSTRAINT_DATA data( 0, 0, 1.0, 3.0, 5.0, "ValidLengthRule" );
370
372
373 BOOST_CHECK( result.isValid );
374 BOOST_CHECK( result.errors.empty() );
375}
376
377BOOST_AUTO_TEST_CASE( ValidateAbsLengthTwoInvalid )
378{
379 // Invalid: min > max
380 DRC_RE_ABSOLUTE_LENGTH_TWO_CONSTRAINT_DATA data1( 0, 0, 5.0, 3.0, 1.0, "InvalidMinMaxRule" );
381
382 VALIDATION_RESULT result1 = data1.Validate();
383
384 BOOST_CHECK( !result1.isValid );
385 BOOST_CHECK( !result1.errors.empty() );
386
387 bool foundMinMaxError = false;
388
389 for( const auto& error : result1.errors )
390 {
391 if( error.find( "Minimum Length cannot be greater than Maximum Length" ) != std::string::npos )
392 foundMinMaxError = true;
393 }
394
395 BOOST_CHECK( foundMinMaxError );
396
397 // Invalid: negative values
398 DRC_RE_ABSOLUTE_LENGTH_TWO_CONSTRAINT_DATA data2( 0, 0, -1.0, 3.0, 5.0, "NegativeMinRule" );
399
400 VALIDATION_RESULT result2 = data2.Validate();
401
402 BOOST_CHECK( !result2.isValid );
403
404 bool foundNegativeError = false;
405
406 for( const auto& error : result2.errors )
407 {
408 if( error.find( "must be greater than 0" ) != std::string::npos )
409 foundNegativeError = true;
410 }
411
412 BOOST_CHECK( foundNegativeError );
413}
414
415BOOST_AUTO_TEST_CASE( ValidateDiffPairValid )
416{
417 // Valid: all positive, min <= preferred <= max for width and gap
418 // Constructor: id, parentId, ruleName, maxUncoupledLength, minWidth, preferredWidth, maxWidth, minGap, preferredGap, maxGap
419 DRC_RE_ROUTING_DIFF_PAIR_CONSTRAINT_DATA data( 0, 0, "ValidDiffPairRule", 10.0, 0.2, 0.3, 0.5, 0.1, 0.15, 0.2 );
420
422
423 BOOST_CHECK( result.isValid );
424 BOOST_CHECK( result.errors.empty() );
425}
426
427BOOST_AUTO_TEST_CASE( ValidateDiffPairInvalid )
428{
429 // Invalid: min width > max width
430 DRC_RE_ROUTING_DIFF_PAIR_CONSTRAINT_DATA data1( 0, 0, "InvalidWidthRule", 10.0, 0.5, 0.3, 0.2, 0.1, 0.15, 0.2 );
431
432 VALIDATION_RESULT result1 = data1.Validate();
433
434 BOOST_CHECK( !result1.isValid );
435 BOOST_CHECK( !result1.errors.empty() );
436
437 bool foundWidthError = false;
438 for( const auto& error : result1.errors )
439 {
440 if( error.find( "Minimum Width cannot be greater than Maximum Width" ) != std::string::npos )
441 foundWidthError = true;
442 }
443 BOOST_CHECK( foundWidthError );
444
445 // Invalid: negative gap
446 DRC_RE_ROUTING_DIFF_PAIR_CONSTRAINT_DATA data2( 0, 0, "NegativeGapRule", 10.0, 0.2, 0.3, 0.5, -0.1, 0.15, 0.2 );
447
448 VALIDATION_RESULT result2 = data2.Validate();
449
450 BOOST_CHECK( !result2.isValid );
451
452 bool foundNegativeError = false;
453 for( const auto& error : result2.errors )
454 {
455 if( error.find( "must be greater than 0" ) != std::string::npos )
456 foundNegativeError = true;
457 }
458 BOOST_CHECK( foundNegativeError );
459}
460
461BOOST_AUTO_TEST_CASE( ValidatePermittedLayersValid )
462{
463 // Valid: at least one layer is selected (top layer enabled)
464 DRC_RE_PERMITTED_LAYERS_CONSTRAINT_DATA data1( 0, 0, "ValidTopLayerRule", true, false );
465
466 VALIDATION_RESULT result1 = data1.Validate();
467
468 BOOST_CHECK( result1.isValid );
469 BOOST_CHECK( result1.errors.empty() );
470
471 // Valid: at least one layer is selected (bottom layer enabled)
472 DRC_RE_PERMITTED_LAYERS_CONSTRAINT_DATA data2( 0, 0, "ValidBottomLayerRule", false, true );
473
474 VALIDATION_RESULT result2 = data2.Validate();
475
476 BOOST_CHECK( result2.isValid );
477 BOOST_CHECK( result2.errors.empty() );
478
479 // Valid: both layers enabled
480 DRC_RE_PERMITTED_LAYERS_CONSTRAINT_DATA data3( 0, 0, "ValidBothLayersRule", true, true );
481
482 VALIDATION_RESULT result3 = data3.Validate();
483
484 BOOST_CHECK( result3.isValid );
485 BOOST_CHECK( result3.errors.empty() );
486}
487
488BOOST_AUTO_TEST_CASE( ValidatePermittedLayersInvalid )
489{
490 // Invalid: no layers selected
491 DRC_RE_PERMITTED_LAYERS_CONSTRAINT_DATA data( 0, 0, "NoLayersRule", false, false );
492
494
495 BOOST_CHECK( !result.isValid );
496 BOOST_CHECK( !result.errors.empty() );
497
498 bool foundLayerError = false;
499 for( const auto& error : result.errors )
500 {
501 if( error.find( "At least one layer must be selected" ) != std::string::npos )
502 foundLayerError = true;
503 }
504 BOOST_CHECK( foundLayerError );
505}
506
507BOOST_AUTO_TEST_CASE( ValidateNumericInputValid )
508{
509 DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA data( 0, 0, 0.5, "ValidRule" );
510
512
513 BOOST_CHECK( result.isValid );
514 BOOST_CHECK( result.errors.empty() );
515}
516
517BOOST_AUTO_TEST_CASE( ValidateNumericInputInvalidNegative )
518{
519 DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA data( 0, 0, -0.5, "InvalidRule" );
520
522
523 BOOST_CHECK( !result.isValid );
524 BOOST_CHECK( !result.errors.empty() );
525
526 // Should have error about value not being greater than 0
527 bool foundError = false;
528 for( const auto& error : result.errors )
529 {
530 if( error.find( "must be greater than 0" ) != std::string::npos )
531 foundError = true;
532 }
533 BOOST_CHECK( foundError );
534}
535
536BOOST_AUTO_TEST_CASE( ValidateRoutingWidthValid )
537{
538 DRC_RE_ROUTING_WIDTH_CONSTRAINT_DATA data( 0, 0, "ValidRule", 0.2, 0.3, 0.5 );
539
541
542 BOOST_CHECK( result.isValid );
543 BOOST_CHECK( result.errors.empty() );
544}
545
546BOOST_AUTO_TEST_CASE( ValidateRoutingWidthInvalidMinGreaterThanMax )
547{
548 // min > max for routing width
549 DRC_RE_ROUTING_WIDTH_CONSTRAINT_DATA data( 0, 0, "InvalidRule", 0.9, 0.5, 0.3 );
550
552
553 BOOST_CHECK( !result.isValid );
554 BOOST_CHECK( !result.errors.empty() );
555
556 // Should have error about min > max
557 bool foundMinMaxError = false;
558 for( const auto& error : result.errors )
559 {
560 if( error.find( "Minimum Routing Width cannot be greater than Maximum Routing Width" ) != std::string::npos )
561 foundMinMaxError = true;
562 }
563 BOOST_CHECK( foundMinMaxError );
564}
565
566BOOST_AUTO_TEST_CASE( ValidateAllowedOrientationValid )
567{
568 // Valid: at least one orientation is selected (0 degrees)
569 DRC_RE_ALLOWED_ORIENTATION_CONSTRAINT_DATA data1( 0, 0, true, false, false, false, false, "ValidZeroDegRule" );
570
571 VALIDATION_RESULT result1 = data1.Validate();
572
573 BOOST_CHECK( result1.isValid );
574 BOOST_CHECK( result1.errors.empty() );
575
576 // Valid: 90 degrees selected
577 DRC_RE_ALLOWED_ORIENTATION_CONSTRAINT_DATA data2( 0, 0, false, true, false, false, false, "ValidNinetyDegRule" );
578
579 VALIDATION_RESULT result2 = data2.Validate();
580
581 BOOST_CHECK( result2.isValid );
582 BOOST_CHECK( result2.errors.empty() );
583
584 // Valid: all degrees selected
585 DRC_RE_ALLOWED_ORIENTATION_CONSTRAINT_DATA data3( 0, 0, false, false, false, false, true, "ValidAllDegreesRule" );
586
587 VALIDATION_RESULT result3 = data3.Validate();
588
589 BOOST_CHECK( result3.isValid );
590 BOOST_CHECK( result3.errors.empty() );
591
592 // Valid: multiple orientations selected
593 DRC_RE_ALLOWED_ORIENTATION_CONSTRAINT_DATA data4( 0, 0, true, true, false, false, false, "ValidMultipleRule" );
594
595 VALIDATION_RESULT result4 = data4.Validate();
596
597 BOOST_CHECK( result4.isValid );
598 BOOST_CHECK( result4.errors.empty() );
599}
600
601BOOST_AUTO_TEST_CASE( ValidateAllowedOrientationInvalid )
602{
603 // Invalid: no orientation selected
604 DRC_RE_ALLOWED_ORIENTATION_CONSTRAINT_DATA data( 0, 0, false, false, false, false, false, "NoOrientationRule" );
605
607
608 BOOST_CHECK( !result.isValid );
609 BOOST_CHECK( !result.errors.empty() );
610
611 bool foundOrientationError = false;
612 for( const auto& error : result.errors )
613 {
614 if( error.find( "At least one orientation must be selected" ) != std::string::npos )
615 foundOrientationError = true;
616 }
617 BOOST_CHECK( foundOrientationError );
618}
619
620BOOST_AUTO_TEST_CASE( ValidateBoolInputValid )
621{
622 // Test with true value
623 DRC_RE_BOOL_INPUT_CONSTRAINT_DATA dataTrue( 0, 0, true, "BoolRuleTrue" );
624
625 VALIDATION_RESULT resultTrue = dataTrue.Validate();
626
627 BOOST_CHECK( resultTrue.isValid );
628 BOOST_CHECK( resultTrue.errors.empty() );
629
630 // Test with false value
631 DRC_RE_BOOL_INPUT_CONSTRAINT_DATA dataFalse( 0, 0, false, "BoolRuleFalse" );
632
633 VALIDATION_RESULT resultFalse = dataFalse.Validate();
634
635 BOOST_CHECK( resultFalse.isValid );
636 BOOST_CHECK( resultFalse.errors.empty() );
637}
638
639BOOST_AUTO_TEST_CASE( ValidateMinTxtHtThValid )
640{
641 // Valid: both text height and thickness are positive
642 DRC_RE_MINIMUM_TEXT_HEIGHT_THICKNESS_CONSTRAINT_DATA data( 0, 0, "ValidTextRule", 1.0, 0.15 );
643
645
646 BOOST_CHECK( result.isValid );
647 BOOST_CHECK( result.errors.empty() );
648}
649
650BOOST_AUTO_TEST_CASE( ValidateMinTxtHtThInvalid )
651{
652 // Invalid: negative text height
653 DRC_RE_MINIMUM_TEXT_HEIGHT_THICKNESS_CONSTRAINT_DATA data1( 0, 0, "NegativeHeightRule", -1.0, 0.15 );
654
655 VALIDATION_RESULT result1 = data1.Validate();
656
657 BOOST_CHECK( !result1.isValid );
658 BOOST_CHECK( !result1.errors.empty() );
659
660 bool foundHeightError = false;
661 for( const auto& error : result1.errors )
662 {
663 if( error.find( "Minimum Text Height must be greater than 0" ) != std::string::npos )
664 foundHeightError = true;
665 }
666 BOOST_CHECK( foundHeightError );
667
668 // Invalid: negative text thickness
669 DRC_RE_MINIMUM_TEXT_HEIGHT_THICKNESS_CONSTRAINT_DATA data2( 0, 0, "NegativeThicknessRule", 1.0, -0.15 );
670
671 VALIDATION_RESULT result2 = data2.Validate();
672
673 BOOST_CHECK( !result2.isValid );
674
675 bool foundThicknessError = false;
676 for( const auto& error : result2.errors )
677 {
678 if( error.find( "Minimum Text Thickness must be greater than 0" ) != std::string::npos )
679 foundThicknessError = true;
680 }
681 BOOST_CHECK( foundThicknessError );
682
683 // Invalid: both zero (default values)
685
686 VALIDATION_RESULT result3 = data3.Validate();
687
688 BOOST_CHECK( !result3.isValid );
689 BOOST_CHECK_EQUAL( result3.errors.size(), 2 );
690}
691
692BOOST_AUTO_TEST_CASE( ValidateCustomRuleValid )
693{
694 DRC_RE_CUSTOM_RULE_CONSTRAINT_DATA data( 0, 0, "ValidCustomRule" );
695 data.SetRuleText( "(constraint clearance (min 0.2mm))" );
696
698
699 BOOST_CHECK( result.isValid );
700 BOOST_CHECK( result.errors.empty() );
701}
702
703BOOST_AUTO_TEST_CASE( ValidateCustomRuleInvalid )
704{
705 DRC_RE_CUSTOM_RULE_CONSTRAINT_DATA data( 0, 0, "InvalidCustomRule" );
706 data.SetRuleText( "" );
707
709
710 BOOST_CHECK( !result.isValid );
711 BOOST_CHECK( !result.errors.empty() );
712
713 // Should have error about empty rule text
714 bool foundEmptyError = false;
715 for( const auto& error : result.errors )
716 {
717 if( error.find( "Rule text cannot be empty" ) != std::string::npos )
718 foundEmptyError = true;
719 }
720 BOOST_CHECK( foundEmptyError );
721}
722
723
724// ============================================================================
725// Panel Matcher Tests (Phase 5.1)
726// ============================================================================
727
728BOOST_AUTO_TEST_CASE( PanelMatcherExactMatchViaStyle )
729{
730 // Test: Rule with via_diameter + hole_size → VIA_STYLE panel
731 DRC_RULE rule( "ViaStyleRule" );
732
734 viaDia.Value().SetMin( 500000 );
735 viaDia.Value().SetOpt( 600000 );
736 viaDia.Value().SetMax( 800000 );
737 rule.AddConstraint( viaDia );
738
740 holeSize.Value().SetMin( 200000 );
741 holeSize.Value().SetOpt( 300000 );
742 holeSize.Value().SetMax( 400000 );
743 rule.AddConstraint( holeSize );
744
745 DRC_PANEL_MATCHER matcher;
746 std::vector<DRC_PANEL_MATCH> matches = matcher.MatchRule( rule );
747
748 BOOST_REQUIRE_EQUAL( matches.size(), 1 );
749 BOOST_CHECK_EQUAL( matches[0].panelType, VIA_STYLE );
750 BOOST_CHECK_EQUAL( matches[0].claimedConstraints.size(), 2 );
751 BOOST_CHECK( matches[0].claimedConstraints.count( VIA_DIAMETER_CONSTRAINT ) > 0 );
752 BOOST_CHECK( matches[0].claimedConstraints.count( HOLE_SIZE_CONSTRAINT ) > 0 );
753}
754
755BOOST_AUTO_TEST_CASE( PanelMatcherPartialMatchViaDiameter )
756{
757 // Test: Rule with only via_diameter → MINIMUM_VIA_DIAMETER panel
758 DRC_RULE rule( "ViaDiameterOnlyRule" );
759
761 viaDia.Value().SetMin( 500000 );
762 rule.AddConstraint( viaDia );
763
764 DRC_PANEL_MATCHER matcher;
765 std::vector<DRC_PANEL_MATCH> matches = matcher.MatchRule( rule );
766
767 BOOST_REQUIRE_EQUAL( matches.size(), 1 );
768 BOOST_CHECK_EQUAL( matches[0].panelType, MINIMUM_VIA_DIAMETER );
769 BOOST_CHECK_EQUAL( matches[0].claimedConstraints.size(), 1 );
770 BOOST_CHECK( matches[0].claimedConstraints.count( VIA_DIAMETER_CONSTRAINT ) > 0 );
771}
772
773BOOST_AUTO_TEST_CASE( PanelMatcherSplitRule )
774{
775 // Test: Rule with via_diameter + hole_size + clearance → 2 panels
776 DRC_RULE rule( "SplitRule" );
777
779 viaDia.Value().SetMin( 500000 );
780 rule.AddConstraint( viaDia );
781
783 holeSize.Value().SetMin( 200000 );
784 rule.AddConstraint( holeSize );
785
787 clearance.Value().SetMin( 150000 );
788 rule.AddConstraint( clearance );
789
790 DRC_PANEL_MATCHER matcher;
791 std::vector<DRC_PANEL_MATCH> matches = matcher.MatchRule( rule );
792
793 BOOST_REQUIRE_EQUAL( matches.size(), 2 );
794
795 // VIA_STYLE should claim via_diameter + hole_size
796 BOOST_CHECK_EQUAL( matches[0].panelType, VIA_STYLE );
797 BOOST_CHECK_EQUAL( matches[0].claimedConstraints.size(), 2 );
798
799 // MINIMUM_CLEARANCE should claim clearance
800 BOOST_CHECK_EQUAL( matches[1].panelType, MINIMUM_CLEARANCE );
801 BOOST_CHECK_EQUAL( matches[1].claimedConstraints.size(), 1 );
802 BOOST_CHECK( matches[1].claimedConstraints.count( CLEARANCE_CONSTRAINT ) > 0 );
803}
804
805BOOST_AUTO_TEST_CASE( PanelMatcherPriorityDiffPairOverRoutingWidth )
806{
807 // Test: Rule with track_width + diff_pair_gap → ROUTING_DIFF_PAIR (not ROUTING_WIDTH)
808 DRC_RULE rule( "DiffPairRule" );
809
811 trackWidth.Value().SetMin( 200000 );
812 trackWidth.Value().SetOpt( 250000 );
813 trackWidth.Value().SetMax( 300000 );
814 rule.AddConstraint( trackWidth );
815
817 diffGap.Value().SetMin( 100000 );
818 diffGap.Value().SetOpt( 150000 );
819 diffGap.Value().SetMax( 200000 );
820 rule.AddConstraint( diffGap );
821
822 DRC_PANEL_MATCHER matcher;
823 std::vector<DRC_PANEL_MATCH> matches = matcher.MatchRule( rule );
824
825 BOOST_REQUIRE_EQUAL( matches.size(), 1 );
826 BOOST_CHECK_EQUAL( matches[0].panelType, ROUTING_DIFF_PAIR );
827 BOOST_CHECK( matches[0].claimedConstraints.count( TRACK_WIDTH_CONSTRAINT ) > 0 );
828 BOOST_CHECK( matches[0].claimedConstraints.count( DIFF_PAIR_GAP_CONSTRAINT ) > 0 );
829}
830
831BOOST_AUTO_TEST_CASE( PanelMatcherDiffPairWithOptionalUncoupled )
832{
833 // Test: Rule with track_width + diff_pair_gap + uncoupled → ROUTING_DIFF_PAIR claims all three
834 DRC_RULE rule( "DiffPairWithUncoupledRule" );
835
837 trackWidth.Value().SetMin( 200000 );
838 rule.AddConstraint( trackWidth );
839
841 diffGap.Value().SetMin( 100000 );
842 rule.AddConstraint( diffGap );
843
845 uncoupled.Value().SetMax( 5000000 );
846 rule.AddConstraint( uncoupled );
847
848 DRC_PANEL_MATCHER matcher;
849 std::vector<DRC_PANEL_MATCH> matches = matcher.MatchRule( rule );
850
851 BOOST_REQUIRE_EQUAL( matches.size(), 1 );
852 BOOST_CHECK_EQUAL( matches[0].panelType, ROUTING_DIFF_PAIR );
853 BOOST_CHECK_EQUAL( matches[0].claimedConstraints.size(), 3 );
854 BOOST_CHECK( matches[0].claimedConstraints.count( TRACK_WIDTH_CONSTRAINT ) > 0 );
855 BOOST_CHECK( matches[0].claimedConstraints.count( DIFF_PAIR_GAP_CONSTRAINT ) > 0 );
856 BOOST_CHECK( matches[0].claimedConstraints.count( MAX_UNCOUPLED_CONSTRAINT ) > 0 );
857}
858
859BOOST_AUTO_TEST_CASE( PanelMatcherTrackWidthOnly )
860{
861 // Test: Rule with only track_width → ROUTING_WIDTH (not ROUTING_DIFF_PAIR)
862 DRC_RULE rule( "TrackWidthOnlyRule" );
863
865 trackWidth.Value().SetMin( 200000 );
866 trackWidth.Value().SetOpt( 250000 );
867 trackWidth.Value().SetMax( 300000 );
868 rule.AddConstraint( trackWidth );
869
870 DRC_PANEL_MATCHER matcher;
871 std::vector<DRC_PANEL_MATCH> matches = matcher.MatchRule( rule );
872
873 BOOST_REQUIRE_EQUAL( matches.size(), 1 );
874 BOOST_CHECK_EQUAL( matches[0].panelType, ROUTING_WIDTH );
875 BOOST_CHECK( matches[0].claimedConstraints.count( TRACK_WIDTH_CONSTRAINT ) > 0 );
876}
877
878BOOST_AUTO_TEST_CASE( PanelMatcherTextHeightAndThickness )
879{
880 // Test: Rule with text_height + text_thickness → MINIMUM_TEXT_HEIGHT_AND_THICKNESS
881 DRC_RULE rule( "TextRule" );
882
884 textHeight.Value().SetMin( 1000000 );
885 rule.AddConstraint( textHeight );
886
888 textThickness.Value().SetMin( 150000 );
889 rule.AddConstraint( textThickness );
890
891 DRC_PANEL_MATCHER matcher;
892 std::vector<DRC_PANEL_MATCH> matches = matcher.MatchRule( rule );
893
894 BOOST_REQUIRE_EQUAL( matches.size(), 1 );
896 BOOST_CHECK_EQUAL( matches[0].claimedConstraints.size(), 2 );
897}
898
899BOOST_AUTO_TEST_CASE( PanelMatcherLengthConstraint )
900{
901 // Test: Rule with length constraint matches ABSOLUTE_LENGTH panel
902 DRC_RULE rule( "LengthRule" );
903
905 length.Value().SetMin( 10000000 );
906 length.Value().SetOpt( 30000000 );
907 length.Value().SetMax( 50000000 );
908 rule.AddConstraint( length );
909
910 DRC_PANEL_MATCHER matcher;
911 std::vector<DRC_PANEL_MATCH> matches = matcher.MatchRule( rule );
912
913 BOOST_REQUIRE_EQUAL( matches.size(), 1 );
914 BOOST_CHECK_EQUAL( matches[0].panelType, ABSOLUTE_LENGTH );
915 BOOST_CHECK( matches[0].claimedConstraints.count( LENGTH_CONSTRAINT ) > 0 );
916}
917
918BOOST_AUTO_TEST_CASE( PanelMatcherLengthAndSkewConstraint )
919{
920 // Rule with length + skew should match MATCHED_LENGTH_DIFF_PAIR, not ABSOLUTE_LENGTH
921 DRC_RULE rule( "MatchedLengthRule" );
922
924 length.Value().SetMin( 10000000 );
925 length.Value().SetOpt( 30000000 );
926 length.Value().SetMax( 50000000 );
927 rule.AddConstraint( length );
928
930 skew.Value().SetMax( 1000000 );
931 rule.AddConstraint( skew );
932
933 DRC_PANEL_MATCHER matcher;
934 std::vector<DRC_PANEL_MATCH> matches = matcher.MatchRule( rule );
935
936 BOOST_REQUIRE_EQUAL( matches.size(), 1 );
937 BOOST_CHECK_EQUAL( matches[0].panelType, MATCHED_LENGTH_DIFF_PAIR );
938 BOOST_CHECK( matches[0].claimedConstraints.count( LENGTH_CONSTRAINT ) > 0 );
939 BOOST_CHECK( matches[0].claimedConstraints.count( SKEW_CONSTRAINT ) > 0 );
940}
941
942BOOST_AUTO_TEST_CASE( PanelMatcherEmptyRule )
943{
944 // Test: Rule with no constraints → no matches
945 DRC_RULE rule( "EmptyRule" );
946
947 DRC_PANEL_MATCHER matcher;
948 std::vector<DRC_PANEL_MATCH> matches = matcher.MatchRule( rule );
949
950 BOOST_CHECK_EQUAL( matches.size(), 0 );
951}
952
953BOOST_AUTO_TEST_CASE( PanelMatcherCanPanelLoad )
954{
955 DRC_PANEL_MATCHER matcher;
956
957 // VIA_STYLE can load via_diameter + hole_size
958 std::set<DRC_CONSTRAINT_T> viaStyleConstraints = { VIA_DIAMETER_CONSTRAINT, HOLE_SIZE_CONSTRAINT };
959 BOOST_CHECK( matcher.CanPanelLoad( VIA_STYLE, viaStyleConstraints ) );
960
961 // VIA_STYLE cannot load clearance
962 std::set<DRC_CONSTRAINT_T> clearanceConstraint = { CLEARANCE_CONSTRAINT };
963 BOOST_CHECK( !matcher.CanPanelLoad( VIA_STYLE, clearanceConstraint ) );
964
965 // CUSTOM_RULE can load anything
966 BOOST_CHECK( matcher.CanPanelLoad( CUSTOM_RULE, viaStyleConstraints ) );
967 BOOST_CHECK( matcher.CanPanelLoad( CUSTOM_RULE, clearanceConstraint ) );
968}
969
982
983
984// ============================================================================
985// Rule Loader Tests (Phase 5.1)
986// ============================================================================
987
988BOOST_AUTO_TEST_CASE( RuleLoaderViaStyleFromText )
989{
990 // Test: Load a via style rule from text and verify values
991 wxString ruleText =
992 "(version 1)\n"
993 "(rule \"Via Test\"\n"
994 " (constraint via_diameter (min 0.5mm) (opt 0.6mm) (max 0.8mm))\n"
995 " (constraint hole_size (min 0.2mm) (opt 0.3mm) (max 0.4mm)))";
996
997 DRC_RULE_LOADER loader;
998 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
999
1000 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
1001 BOOST_CHECK_EQUAL( entries[0].panelType, VIA_STYLE );
1002 BOOST_CHECK_EQUAL( entries[0].ruleName, "Via Test" );
1003
1004 auto viaData = std::dynamic_pointer_cast<DRC_RE_VIA_STYLE_CONSTRAINT_DATA>( entries[0].constraintData );
1005 BOOST_REQUIRE( viaData );
1006
1007 BOOST_CHECK_CLOSE( viaData->GetMinViaDiameter(), 0.5, 0.0001 );
1008 BOOST_CHECK_CLOSE( viaData->GetPreferredViaDiameter(), 0.6, 0.0001 );
1009 BOOST_CHECK_CLOSE( viaData->GetMaxViaDiameter(), 0.8, 0.0001 );
1010 BOOST_CHECK_CLOSE( viaData->GetMinViaHoleSize(), 0.2, 0.0001 );
1011 BOOST_CHECK_CLOSE( viaData->GetPreferredViaHoleSize(), 0.3, 0.0001 );
1012 BOOST_CHECK_CLOSE( viaData->GetMaxViaHoleSize(), 0.4, 0.0001 );
1013}
1014
1015BOOST_AUTO_TEST_CASE( RuleLoaderRoutingWidthFromText )
1016{
1017 // Test: Load a routing width rule from text
1018 wxString ruleText =
1019 "(version 1)\n"
1020 "(rule \"Track Test\"\n"
1021 " (constraint track_width (min 0.2mm) (opt 0.25mm) (max 0.3mm)))";
1022
1023 DRC_RULE_LOADER loader;
1024 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1025
1026 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
1027 BOOST_CHECK_EQUAL( entries[0].panelType, ROUTING_WIDTH );
1028 BOOST_CHECK_EQUAL( entries[0].ruleName, "Track Test" );
1029
1030 auto trackData = std::dynamic_pointer_cast<DRC_RE_ROUTING_WIDTH_CONSTRAINT_DATA>( entries[0].constraintData );
1031 BOOST_REQUIRE( trackData );
1032
1033 BOOST_CHECK_CLOSE( trackData->GetMinRoutingWidth(), 0.2, 0.0001 );
1034 BOOST_CHECK_CLOSE( trackData->GetPreferredRoutingWidth(), 0.25, 0.0001 );
1035 BOOST_CHECK_CLOSE( trackData->GetMaxRoutingWidth(), 0.3, 0.0001 );
1036}
1037
1038BOOST_AUTO_TEST_CASE( RuleLoaderDiffPairFromText )
1039{
1040 // Test: Load a diff pair rule from text
1041 wxString ruleText =
1042 "(version 1)\n"
1043 "(rule \"Diff Pair Test\"\n"
1044 " (constraint track_width (min 0.2mm) (opt 0.25mm) (max 0.3mm))\n"
1045 " (constraint diff_pair_gap (min 0.1mm) (opt 0.15mm) (max 0.2mm))\n"
1046 " (constraint diff_pair_uncoupled (max 5mm)))";
1047
1048 DRC_RULE_LOADER loader;
1049 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1050
1051 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
1052 BOOST_CHECK_EQUAL( entries[0].panelType, ROUTING_DIFF_PAIR );
1053
1054 auto dpData = std::dynamic_pointer_cast<DRC_RE_ROUTING_DIFF_PAIR_CONSTRAINT_DATA>( entries[0].constraintData );
1055 BOOST_REQUIRE( dpData );
1056
1057 BOOST_CHECK_CLOSE( dpData->GetMinWidth(), 0.2, 0.0001 );
1058 BOOST_CHECK_CLOSE( dpData->GetPreferredWidth(), 0.25, 0.0001 );
1059 BOOST_CHECK_CLOSE( dpData->GetMaxWidth(), 0.3, 0.0001 );
1060 BOOST_CHECK_CLOSE( dpData->GetMinGap(), 0.1, 0.0001 );
1061 BOOST_CHECK_CLOSE( dpData->GetPreferredGap(), 0.15, 0.0001 );
1062 BOOST_CHECK_CLOSE( dpData->GetMaxGap(), 0.2, 0.0001 );
1063 BOOST_CHECK_CLOSE( dpData->GetMaxUncoupledLength(), 5.0, 0.0001 );
1064}
1065
1066BOOST_AUTO_TEST_CASE( RuleLoaderSplitRuleFromText )
1067{
1068 // Test: Load a rule that splits into multiple panels
1069 wxString ruleText =
1070 "(version 1)\n"
1071 "(rule \"Split Test\"\n"
1072 " (constraint via_diameter (min 0.5mm))\n"
1073 " (constraint hole_size (min 0.2mm))\n"
1074 " (constraint clearance (min 0.15mm)))";
1075
1076 DRC_RULE_LOADER loader;
1077 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1078
1079 BOOST_REQUIRE_EQUAL( entries.size(), 2 );
1080
1081 // First entry should be VIA_STYLE
1082 BOOST_CHECK_EQUAL( entries[0].panelType, VIA_STYLE );
1083 BOOST_CHECK_EQUAL( entries[0].ruleName, "Split Test" );
1084
1085 // Second entry should be MINIMUM_CLEARANCE
1086 BOOST_CHECK_EQUAL( entries[1].panelType, MINIMUM_CLEARANCE );
1087 BOOST_CHECK_EQUAL( entries[1].ruleName, "Split Test" );
1088
1089 auto numericData = std::dynamic_pointer_cast<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>( entries[1].constraintData );
1090 BOOST_REQUIRE( numericData );
1091 BOOST_CHECK_CLOSE( numericData->GetNumericInputValue(), 0.15, 0.0001 );
1092}
1093
1094BOOST_AUTO_TEST_CASE( RuleLoaderVmeWrenClearanceUnderFpga )
1095{
1096 // Test: Load the clearance_under_fpga rule from vme-wren demo which should split
1097 // This rule has: clearance, hole_size, via_diameter
1098 // Should split into: VIA_STYLE (hole_size + via_diameter) + MINIMUM_CLEARANCE (clearance)
1099 wxString ruleText =
1100 "(version 1)\n"
1101 "(rule \"clearance_under_fpga\"\n"
1102 " (constraint clearance (min 0.1mm))\n"
1103 " (constraint hole_size (min 0.2mm))\n"
1104 " (constraint via_diameter (min 0.4mm))\n"
1105 " (condition \"A.intersectsArea('underFPGA') || A.intersectsArea('underDDR')\"))";
1106
1107 DRC_RULE_LOADER loader;
1108 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1109
1110 // Should produce 2 entries from the split
1111 BOOST_REQUIRE_EQUAL( entries.size(), 2 );
1112
1113 // First entry should be VIA_STYLE (claims via_diameter + hole_size at priority 90)
1114 BOOST_CHECK_EQUAL( entries[0].panelType, VIA_STYLE );
1115 BOOST_CHECK_EQUAL( entries[0].ruleName, "clearance_under_fpga" );
1116 BOOST_CHECK_EQUAL( entries[0].condition, "A.intersectsArea('underFPGA') || A.intersectsArea('underDDR')" );
1117
1118 // Second entry should be MINIMUM_CLEARANCE (claims clearance at priority 30)
1119 BOOST_CHECK_EQUAL( entries[1].panelType, MINIMUM_CLEARANCE );
1120 BOOST_CHECK_EQUAL( entries[1].ruleName, "clearance_under_fpga" );
1121 BOOST_CHECK_EQUAL( entries[1].condition, "A.intersectsArea('underFPGA') || A.intersectsArea('underDDR')" );
1122
1123 // Verify VIA_STYLE has correct values
1124 auto viaData = std::dynamic_pointer_cast<DRC_RE_VIA_STYLE_CONSTRAINT_DATA>( entries[0].constraintData );
1125 BOOST_REQUIRE( viaData );
1126 BOOST_CHECK_CLOSE( viaData->GetMinViaDiameter(), 0.4, 0.0001 );
1127 BOOST_CHECK_CLOSE( viaData->GetMinViaHoleSize(), 0.2, 0.0001 );
1128
1129 // Verify MINIMUM_CLEARANCE has correct value
1130 auto clearanceData = std::dynamic_pointer_cast<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>( entries[1].constraintData );
1131 BOOST_REQUIRE( clearanceData );
1132 BOOST_CHECK_CLOSE( clearanceData->GetNumericInputValue(), 0.1, 0.0001 );
1133}
1134
1135BOOST_AUTO_TEST_CASE( RuleLoaderTextHeightThicknessFromText )
1136{
1137 // Test: Load text height and thickness rule
1138 wxString ruleText =
1139 "(version 1)\n"
1140 "(rule \"Text Test\"\n"
1141 " (constraint text_height (min 1mm))\n"
1142 " (constraint text_thickness (min 0.15mm)))";
1143
1144 DRC_RULE_LOADER loader;
1145 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1146
1147 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
1149
1150 auto textData = std::dynamic_pointer_cast<DRC_RE_MINIMUM_TEXT_HEIGHT_THICKNESS_CONSTRAINT_DATA>( entries[0].constraintData );
1151 BOOST_REQUIRE( textData );
1152
1153 BOOST_CHECK_CLOSE( textData->GetMinTextHeight(), 1.0, 0.0001 );
1154 BOOST_CHECK_CLOSE( textData->GetMinTextThickness(), 0.15, 0.0001 );
1155}
1156
1157BOOST_AUTO_TEST_CASE( RuleLoaderAbsoluteLengthFromText )
1158{
1159 // Test: Load absolute length rule with min/opt/max
1160 wxString ruleText =
1161 "(version 1)\n"
1162 "(rule \"Length Test\"\n"
1163 " (constraint length (min 10mm) (opt 30mm) (max 50mm)))";
1164
1165 DRC_RULE_LOADER loader;
1166 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1167
1168 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
1169 BOOST_CHECK_EQUAL( entries[0].panelType, ABSOLUTE_LENGTH );
1170 BOOST_CHECK_EQUAL( entries[0].ruleName, "Length Test" );
1171
1172 auto lengthData = dynamic_pointer_cast<DRC_RE_ABSOLUTE_LENGTH_TWO_CONSTRAINT_DATA>( entries[0].constraintData );
1173 BOOST_REQUIRE( lengthData );
1174 BOOST_CHECK_CLOSE( lengthData->GetMinimumLength(), 10.0, 0.0001 );
1175 BOOST_CHECK_CLOSE( lengthData->GetOptimumLength(), 30.0, 0.0001 );
1176 BOOST_CHECK_CLOSE( lengthData->GetMaximumLength(), 50.0, 0.0001 );
1177}
1178
1179BOOST_AUTO_TEST_CASE( RuleLoaderMatchedLengthWithSkew )
1180{
1181 // Rule with length + skew should load as MATCHED_LENGTH_DIFF_PAIR and preserve skew
1182 wxString ruleText =
1183 "(version 1)\n"
1184 "(rule \"DP Length Match\"\n"
1185 " (constraint length (min 10mm) (opt 30mm) (max 50mm))\n"
1186 " (constraint skew (max 1mm)))";
1187
1188 DRC_RULE_LOADER loader;
1189 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1190
1191 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
1192 BOOST_CHECK_EQUAL( entries[0].panelType, MATCHED_LENGTH_DIFF_PAIR );
1193 BOOST_CHECK_EQUAL( entries[0].ruleName, "DP Length Match" );
1194
1195 auto matchedData =
1196 std::dynamic_pointer_cast<DRC_RE_MATCHED_LENGTH_DIFF_PAIR_CONSTRAINT_DATA>( entries[0].constraintData );
1197 BOOST_REQUIRE( matchedData );
1198
1199 BOOST_CHECK_CLOSE( matchedData->GetMinimumLength(), 10.0, 0.0001 );
1200 BOOST_CHECK_CLOSE( matchedData->GetOptimumLength(), 30.0, 0.0001 );
1201 BOOST_CHECK_CLOSE( matchedData->GetMaximumLength(), 50.0, 0.0001 );
1202 BOOST_CHECK_CLOSE( matchedData->GetMaxSkew(), 1.0, 0.0001 );
1203}
1204
1205BOOST_AUTO_TEST_CASE( RuleLoaderMatchedLengthRoundTrip )
1206{
1207 // Verify length+skew rule round-trips without losing skew
1208 wxString ruleText =
1209 "(version 1)\n"
1210 "(rule \"DP Match RT\"\n"
1211 " (constraint length (min 10mm) (opt 30mm) (max 50mm))\n"
1212 " (constraint skew (max 2.5mm)))";
1213
1214 DRC_RULE_LOADER loader;
1215 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1216
1217 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
1218 entries[0].wasEdited = true;
1219
1220 DRC_RULE_SAVER saver;
1221 wxString savedText = saver.GenerateRulesText( entries, nullptr );
1222
1223 BOOST_CHECK( savedText.Contains( "length" ) );
1224 BOOST_CHECK( savedText.Contains( "skew" ) );
1225 BOOST_CHECK( savedText.Contains( "2.5mm" ) );
1226
1227 std::vector<DRC_RE_LOADED_PANEL_ENTRY> reloaded = loader.LoadFromString( savedText );
1228
1229 BOOST_REQUIRE_EQUAL( reloaded.size(), 1 );
1230 BOOST_CHECK_EQUAL( reloaded[0].panelType, MATCHED_LENGTH_DIFF_PAIR );
1231
1232 auto reloadedData =
1233 std::dynamic_pointer_cast<DRC_RE_MATCHED_LENGTH_DIFF_PAIR_CONSTRAINT_DATA>( reloaded[0].constraintData );
1234 BOOST_REQUIRE( reloadedData );
1235
1236 BOOST_CHECK_CLOSE( reloadedData->GetMaxSkew(), 2.5, 0.0001 );
1237 BOOST_CHECK_CLOSE( reloadedData->GetMinimumLength(), 10.0, 0.0001 );
1238 BOOST_CHECK_CLOSE( reloadedData->GetOptimumLength(), 30.0, 0.0001 );
1239 BOOST_CHECK_CLOSE( reloadedData->GetMaximumLength(), 50.0, 0.0001 );
1240}
1241
1242BOOST_AUTO_TEST_CASE( RuleLoaderWithCondition )
1243{
1244 // Test: Load rule with condition
1245 wxString ruleText =
1246 "(version 1)\n"
1247 "(rule \"Conditional Test\"\n"
1248 " (condition \"A.NetClass == 'Power'\")\n"
1249 " (constraint clearance (min 0.3mm)))";
1250
1251 DRC_RULE_LOADER loader;
1252 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1253
1254 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
1255 BOOST_CHECK_EQUAL( entries[0].ruleName, "Conditional Test" );
1256 BOOST_CHECK_EQUAL( entries[0].condition, "A.NetClass == 'Power'" );
1257 BOOST_CHECK_EQUAL( entries[0].constraintData->GetRuleCondition(), "A.NetClass == 'Power'" );
1258}
1259
1260BOOST_AUTO_TEST_CASE( RuleLoaderMultipleRules )
1261{
1262 // Test: Load multiple rules and verify order preservation
1263 wxString ruleText =
1264 "(version 1)\n"
1265 "(rule \"Rule A\"\n"
1266 " (constraint clearance (min 0.2mm)))\n"
1267 "(rule \"Rule B\"\n"
1268 " (constraint track_width (min 0.15mm)))\n"
1269 "(rule \"Rule C\"\n"
1270 " (constraint via_diameter (min 0.5mm))\n"
1271 " (constraint hole_size (min 0.3mm)))";
1272
1273 DRC_RULE_LOADER loader;
1274 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1275
1276 BOOST_REQUIRE_EQUAL( entries.size(), 3 );
1277
1278 BOOST_CHECK_EQUAL( entries[0].ruleName, "Rule A" );
1279 BOOST_CHECK_EQUAL( entries[0].panelType, MINIMUM_CLEARANCE );
1280
1281 BOOST_CHECK_EQUAL( entries[1].ruleName, "Rule B" );
1282 BOOST_CHECK_EQUAL( entries[1].panelType, ROUTING_WIDTH );
1283
1284 BOOST_CHECK_EQUAL( entries[2].ruleName, "Rule C" );
1285 BOOST_CHECK_EQUAL( entries[2].panelType, VIA_STYLE );
1286}
1287
1288BOOST_AUTO_TEST_CASE( RuleLoaderEmptyRule )
1289{
1290 // Test: Rule with no constraints creates custom rule fallback
1291 wxString ruleText =
1292 "(version 1)\n"
1293 "(rule \"Empty Rule\")";
1294
1295 DRC_RULE_LOADER loader;
1296 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1297
1298 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
1299 BOOST_CHECK_EQUAL( entries[0].panelType, CUSTOM_RULE );
1300 BOOST_CHECK_EQUAL( entries[0].ruleName, "Empty Rule" );
1301}
1302
1303BOOST_AUTO_TEST_CASE( RuleLoaderInvalidText )
1304{
1305 // Test: Invalid rule text returns empty vector
1306 wxString ruleText = "not valid rule text at all";
1307
1308 DRC_RULE_LOADER loader;
1309 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1310
1311 BOOST_CHECK_EQUAL( entries.size(), 0 );
1312}
1313
1314BOOST_AUTO_TEST_CASE( RuleLoaderNumericConstraints )
1315{
1316 // Test various numeric constraints map to correct panels
1317 wxString ruleText =
1318 "(version 1)\n"
1319 "(rule \"Edge Clearance\"\n"
1320 " (constraint edge_clearance (min 0.5mm)))\n"
1321 "(rule \"Hole Clearance\"\n"
1322 " (constraint hole_clearance (min 0.3mm)))\n"
1323 "(rule \"Courtyard\"\n"
1324 " (constraint courtyard_clearance (min 0.25mm)))";
1325
1326 DRC_RULE_LOADER loader;
1327 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1328
1329 BOOST_REQUIRE_EQUAL( entries.size(), 3 );
1330
1331 BOOST_CHECK_EQUAL( entries[0].panelType, COPPER_TO_EDGE_CLEARANCE );
1332 BOOST_CHECK_EQUAL( entries[1].panelType, COPPER_TO_HOLE_CLEARANCE );
1333 BOOST_CHECK_EQUAL( entries[2].panelType, COURTYARD_CLEARANCE );
1334}
1335
1336
1337// ============================================================================
1338// Rule Saver Tests (Phase 3)
1339// ============================================================================
1340
1341BOOST_AUTO_TEST_CASE( RuleSaverBasicGeneration )
1342{
1343 // Test: Basic rule text generation from panel entry
1346 entry.ruleName = "TestClearance";
1347 entry.wasEdited = true;
1348
1349 auto numericData = std::make_shared<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>();
1350 numericData->SetRuleName( "TestClearance" );
1351 numericData->SetConstraintCode( "clearance" );
1352 numericData->SetNumericInputValue( 0.2 );
1353 entry.constraintData = numericData;
1354
1355 DRC_RULE_SAVER saver;
1356 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
1357 wxString result = saver.GenerateRulesText( entries, nullptr );
1358
1359 BOOST_CHECK( result.Contains( "(version 1)" ) );
1360 BOOST_CHECK( result.Contains( "TestClearance" ) );
1361 BOOST_CHECK( result.Contains( "clearance" ) );
1362}
1363
1364BOOST_AUTO_TEST_CASE( RuleSaverRoundTripPreservation )
1365{
1366 // Test: Original text preserved when not edited
1369 entry.ruleName = "Preserved Rule";
1370 entry.originalRuleText = "(rule \"Preserved Rule\"\n\t(constraint clearance (min 0.5mm)))";
1371 entry.wasEdited = false;
1372
1373 auto numericData = std::make_shared<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>();
1374 numericData->SetRuleName( "Preserved Rule" );
1375 numericData->SetConstraintCode( "clearance" );
1376 numericData->SetNumericInputValue( 0.5 );
1377 entry.constraintData = numericData;
1378
1379 DRC_RULE_SAVER saver;
1380 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
1381 wxString result = saver.GenerateRulesText( entries, nullptr );
1382
1383 BOOST_CHECK( result.Contains( entry.originalRuleText ) );
1384}
1385
1386BOOST_AUTO_TEST_CASE( RuleSaverEditedRuleRegenerated )
1387{
1388 // Test: Edited rules regenerate from panel data
1391 entry.ruleName = "EditedRule";
1392 entry.originalRuleText = "(rule \"EditedRule\"\n\t(constraint clearance (min 0.5mm)))";
1393 entry.wasEdited = true;
1394
1395 auto numericData = std::make_shared<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>();
1396 numericData->SetRuleName( "EditedRule" );
1397 numericData->SetConstraintCode( "clearance" );
1398 numericData->SetNumericInputValue( 0.3 );
1399 entry.constraintData = numericData;
1400
1401 DRC_RULE_SAVER saver;
1402 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
1403 wxString result = saver.GenerateRulesText( entries, nullptr );
1404
1405 // Original text should NOT be preserved since wasEdited is true
1406 BOOST_CHECK( !result.Contains( "0.5mm" ) );
1407 BOOST_CHECK( result.Contains( "EditedRule" ) );
1408}
1409
1410BOOST_AUTO_TEST_CASE( RuleSaverViaStyleRule )
1411{
1412 // Test: Via style rule generation
1414 entry.panelType = VIA_STYLE;
1415 entry.ruleName = "ViaTest";
1416
1417 auto viaData = std::make_shared<DRC_RE_VIA_STYLE_CONSTRAINT_DATA>();
1418 viaData->SetRuleName( "ViaTest" );
1419 viaData->SetMinViaDiameter( 0.5 );
1420 viaData->SetPreferredViaDiameter( 0.6 );
1421 viaData->SetMaxViaDiameter( 0.8 );
1422 viaData->SetMinViaHoleSize( 0.2 );
1423 viaData->SetPreferredViaHoleSize( 0.3 );
1424 viaData->SetMaxViaHoleSize( 0.4 );
1425 entry.constraintData = viaData;
1426 entry.wasEdited = true;
1427
1428 DRC_RULE_SAVER saver;
1429 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
1430 wxString result = saver.GenerateRulesText( entries, nullptr );
1431
1432 BOOST_CHECK( result.Contains( "ViaTest" ) );
1433 BOOST_CHECK( result.Contains( "via_diameter" ) );
1434 BOOST_CHECK( result.Contains( "hole_size" ) );
1435}
1436
1437BOOST_AUTO_TEST_CASE( RuleSaverMultipleEntries )
1438{
1439 // Test: Multiple entries saved in order
1440 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries;
1441
1444 entry1.ruleName = "RuleA";
1445 auto data1 = std::make_shared<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>();
1446 data1->SetRuleName( "RuleA" );
1447 data1->SetConstraintCode( "clearance" );
1448 data1->SetNumericInputValue( 0.2 );
1449 entry1.constraintData = data1;
1450 entry1.wasEdited = true;
1451 entries.push_back( entry1 );
1452
1454 entry2.panelType = ROUTING_WIDTH;
1455 entry2.ruleName = "RuleB";
1456 auto data2 = std::make_shared<DRC_RE_ROUTING_WIDTH_CONSTRAINT_DATA>();
1457 data2->SetRuleName( "RuleB" );
1458 data2->SetMinRoutingWidth( 0.15 );
1459 data2->SetPreferredRoutingWidth( 0.2 );
1460 data2->SetMaxRoutingWidth( 0.3 );
1461 entry2.constraintData = data2;
1462 entry2.wasEdited = true;
1463 entries.push_back( entry2 );
1464
1465 DRC_RULE_SAVER saver;
1466 wxString result = saver.GenerateRulesText( entries, nullptr );
1467
1468 // Both rules should appear
1469 BOOST_CHECK( result.Contains( "RuleA" ) );
1470 BOOST_CHECK( result.Contains( "RuleB" ) );
1471
1472 // Check order: RuleA should appear before RuleB
1473 size_t posA = result.Find( "RuleA" );
1474 size_t posB = result.Find( "RuleB" );
1475 BOOST_CHECK( posA < posB );
1476}
1477
1478BOOST_AUTO_TEST_CASE( RuleSaverWithCondition )
1479{
1480 // Test: Rule with condition
1483 entry.ruleName = "ConditionalRule";
1484 entry.condition = "A.NetClass == 'Power'";
1485
1486 auto numericData = std::make_shared<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>();
1487 numericData->SetRuleName( "ConditionalRule" );
1488 numericData->SetConstraintCode( "clearance" );
1489 numericData->SetRuleCondition( "A.NetClass == 'Power'" );
1490 numericData->SetNumericInputValue( 0.3 );
1491 entry.constraintData = numericData;
1492 entry.wasEdited = true;
1493
1494 DRC_RULE_SAVER saver;
1495 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
1496 wxString result = saver.GenerateRulesText( entries, nullptr );
1497
1498 BOOST_CHECK( result.Contains( "ConditionalRule" ) );
1499 BOOST_CHECK( result.Contains( "condition" ) );
1500 BOOST_CHECK( result.Contains( "Power" ) );
1501}
1502
1503BOOST_AUTO_TEST_CASE( RuleSaverEmptyEntries )
1504{
1505 // Test: Empty entries vector
1506 DRC_RULE_SAVER saver;
1507 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries;
1508 wxString result = saver.GenerateRulesText( entries, nullptr );
1509
1510 BOOST_CHECK( result.Contains( "(version 1)" ) );
1511 // Should have version header but nothing else
1512 BOOST_CHECK_EQUAL( result.Trim(), "(version 1)" );
1513}
1514
1515BOOST_AUTO_TEST_CASE( RuleSaverNullConstraintData )
1516{
1517 // Test: Entry with null constraint data is skipped
1520 entry.ruleName = "Null Data Rule";
1521 entry.constraintData = nullptr;
1522 entry.wasEdited = true;
1523
1524 DRC_RULE_SAVER saver;
1525 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
1526 wxString result = saver.GenerateRulesText( entries, nullptr );
1527
1528 // Should have version header but rule text is skipped
1529 BOOST_CHECK( result.Contains( "(version 1)" ) );
1530 BOOST_CHECK( !result.Contains( "Null Data Rule" ) );
1531}
1532
1533BOOST_AUTO_TEST_CASE( RuleSaverLoadSaveRoundTrip )
1534{
1535 // Test: Full round-trip: Load → mark edited → Save → Load produces equivalent data
1536 wxString originalText =
1537 "(version 1)\n"
1538 "(rule \"RoundTripTest\"\n"
1539 " (constraint clearance (min 0.25mm)))";
1540
1541 DRC_RULE_LOADER loader;
1542 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( originalText );
1543
1544 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
1545 BOOST_CHECK_EQUAL( entries[0].ruleName, "RoundTripTest" );
1546
1547 // Mark as edited so it regenerates from data (loader doesn't preserve exact original text)
1548 entries[0].wasEdited = true;
1549
1550 // Save
1551 DRC_RULE_SAVER saver;
1552 wxString savedText = saver.GenerateRulesText( entries, nullptr );
1553
1554 // Verify saved text is valid by reloading
1555 std::vector<DRC_RE_LOADED_PANEL_ENTRY> reloadedEntries = loader.LoadFromString( savedText );
1556
1557 BOOST_REQUIRE_EQUAL( reloadedEntries.size(), 1 );
1558 BOOST_CHECK_EQUAL( reloadedEntries[0].ruleName, "RoundTripTest" );
1559 BOOST_CHECK_EQUAL( reloadedEntries[0].panelType, entries[0].panelType );
1560}
1561
1562BOOST_AUTO_TEST_CASE( RuleSaverDiffPairRule )
1563{
1564 // Test: Diff pair rule generation
1567 entry.ruleName = "DiffPairTest";
1568
1569 auto dpData = std::make_shared<DRC_RE_ROUTING_DIFF_PAIR_CONSTRAINT_DATA>();
1570 dpData->SetRuleName( "DiffPairTest" );
1571 dpData->SetMinWidth( 0.2 );
1572 dpData->SetPreferredWidth( 0.25 );
1573 dpData->SetMaxWidth( 0.3 );
1574 dpData->SetMinGap( 0.1 );
1575 dpData->SetPreferredGap( 0.15 );
1576 dpData->SetMaxGap( 0.2 );
1577 dpData->SetMaxUncoupledLength( 5.0 );
1578 entry.constraintData = dpData;
1579 entry.wasEdited = true;
1580
1581 DRC_RULE_SAVER saver;
1582 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
1583 wxString result = saver.GenerateRulesText( entries, nullptr );
1584
1585 BOOST_CHECK( result.Contains( "DiffPairTest" ) );
1586 BOOST_CHECK( result.Contains( "track_width" ) );
1587 BOOST_CHECK( result.Contains( "diff_pair_gap" ) );
1588}
1589
1590
1591// ============================================================================
1592// Integration Tests (Phase 5.2)
1593// ============================================================================
1594
1595BOOST_AUTO_TEST_CASE( IntegrationLoadHoleClearanceRules )
1596{
1597 // Test: Load issue6879.kicad_dru which has hole_clearance and hole constraints
1598 wxString ruleText =
1599 "(version 1)\n"
1600 "(rule \"PTH to Track Clearance\"\n"
1601 " (constraint hole_clearance (min 1.0mm))\n"
1602 " (condition \"A.Type == 'Pad' && A.Pad_Type == 'Through-hole' && B.Type =='Track'\"))\n"
1603 "(rule \"Max Drill Hole Size Mechanical\"\n"
1604 " (constraint hole (max 2.0mm))\n"
1605 " (condition \"A.Type == 'Pad'\"))";
1606
1607 DRC_RULE_LOADER loader;
1608 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1609
1610 BOOST_REQUIRE_GE( entries.size(), 2 );
1611
1612 // First rule should be COPPER_TO_HOLE_CLEARANCE
1613 BOOST_CHECK_EQUAL( entries[0].panelType, COPPER_TO_HOLE_CLEARANCE );
1614 BOOST_CHECK_EQUAL( entries[0].ruleName, "PTH to Track Clearance" );
1615 BOOST_CHECK( !entries[0].condition.IsEmpty() );
1616
1617 // Second rule should be HOLE_SIZE
1618 BOOST_CHECK_EQUAL( entries[1].panelType, HOLE_SIZE );
1619 BOOST_CHECK_EQUAL( entries[1].ruleName, "Max Drill Hole Size Mechanical" );
1620}
1621
1622BOOST_AUTO_TEST_CASE( IntegrationLoadEdgeClearanceWithSeverity )
1623{
1624 // Test: Load severities.kicad_dru which has severity clause
1625 wxString ruleText =
1626 "(version 1)\n"
1627 "(rule board_edge\n"
1628 " (constraint edge_clearance (min 1mm))\n"
1629 " (condition \"A.memberOf('board_edge')\")\n"
1630 " (severity ignore))";
1631
1632 DRC_RULE_LOADER loader;
1633 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1634
1635 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
1636 BOOST_CHECK_EQUAL( entries[0].panelType, COPPER_TO_EDGE_CLEARANCE );
1637 BOOST_CHECK_EQUAL( entries[0].ruleName, "board_edge" );
1638 BOOST_CHECK_EQUAL( entries[0].severity, RPT_SEVERITY_IGNORE );
1639}
1640
1641BOOST_AUTO_TEST_CASE( IntegrationLoadConnectionWidthRules )
1642{
1643 // Test: Load connection_width_rules.kicad_dru
1644 wxString ruleText =
1645 "(version 1)\n"
1646 "(rule high_current_netclass\n"
1647 " (constraint connection_width (min 0.16mm))\n"
1648 " (condition \"A.NetClass == 'High_current'\"))\n"
1649 "(rule high_current_area\n"
1650 " (constraint connection_width (min 0.16mm))\n"
1651 " (condition \"A.insideArea('high_current')\"))";
1652
1653 DRC_RULE_LOADER loader;
1654 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1655
1656 BOOST_REQUIRE_EQUAL( entries.size(), 2 );
1657
1658 // Both should be MINIMUM_CONNECTION_WIDTH
1659 BOOST_CHECK_EQUAL( entries[0].panelType, MINIMUM_CONNECTION_WIDTH );
1660 BOOST_CHECK_EQUAL( entries[1].panelType, MINIMUM_CONNECTION_WIDTH );
1661
1662 // Both should have conditions
1663 BOOST_CHECK( !entries[0].condition.IsEmpty() );
1664 BOOST_CHECK( !entries[1].condition.IsEmpty() );
1665}
1666
1667BOOST_AUTO_TEST_CASE( IntegrationSaveLoadRoundTripMultipleRules )
1668{
1669 // Test: Load multiple rules, save, reload - verify equivalent data
1670 // Use rule names without spaces to avoid sanitization differences
1671 wxString originalText =
1672 "(version 1)\n"
1673 "(rule ClearanceRule\n"
1674 " (constraint clearance (min 0.2mm))\n"
1675 " (condition \"A.NetClass == 'Power'\"))\n"
1676 "(rule TrackWidthRule\n"
1677 " (constraint track_width (min 0.15mm) (opt 0.2mm) (max 0.3mm)))";
1678
1679 DRC_RULE_LOADER loader;
1680 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( originalText );
1681
1682 BOOST_REQUIRE_EQUAL( entries.size(), 2 );
1683
1684 // Mark as edited to force regeneration
1685 for( auto& entry : entries )
1686 entry.wasEdited = true;
1687
1688 // Save
1689 DRC_RULE_SAVER saver;
1690 wxString savedText = saver.GenerateRulesText( entries, nullptr );
1691
1692 // Reload
1693 std::vector<DRC_RE_LOADED_PANEL_ENTRY> reloadedEntries = loader.LoadFromString( savedText );
1694
1695 BOOST_REQUIRE_EQUAL( reloadedEntries.size(), 2 );
1696
1697 // Verify panel types preserved correctly
1698 BOOST_CHECK_EQUAL( reloadedEntries[0].panelType, entries[0].panelType );
1699 BOOST_CHECK_EQUAL( reloadedEntries[1].panelType, entries[1].panelType );
1700
1701 // Verify rule names preserved
1702 BOOST_CHECK_EQUAL( reloadedEntries[0].ruleName, "ClearanceRule" );
1703 BOOST_CHECK_EQUAL( reloadedEntries[1].ruleName, "TrackWidthRule" );
1704}
1705
1706BOOST_AUTO_TEST_CASE( IntegrationSaveLoadViaStyleRoundTrip )
1707{
1708 // Test: Via style rules round-trip correctly
1709 wxString originalText =
1710 "(version 1)\n"
1711 "(rule \"Via Style\"\n"
1712 " (constraint via_diameter (min 0.5mm) (opt 0.6mm) (max 0.8mm))\n"
1713 " (constraint hole_size (min 0.2mm) (opt 0.3mm) (max 0.4mm)))";
1714
1715 DRC_RULE_LOADER loader;
1716 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( originalText );
1717
1718 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
1719 BOOST_CHECK_EQUAL( entries[0].panelType, VIA_STYLE );
1720
1721 auto viaData = std::dynamic_pointer_cast<DRC_RE_VIA_STYLE_CONSTRAINT_DATA>( entries[0].constraintData );
1722 BOOST_REQUIRE( viaData );
1723
1724 double originalMinDia = viaData->GetMinViaDiameter();
1725 double originalMinHole = viaData->GetMinViaHoleSize();
1726
1727 // Mark as edited and save
1728 entries[0].wasEdited = true;
1729 DRC_RULE_SAVER saver;
1730 wxString savedText = saver.GenerateRulesText( entries, nullptr );
1731
1732 // Reload and verify values preserved
1733 std::vector<DRC_RE_LOADED_PANEL_ENTRY> reloadedEntries = loader.LoadFromString( savedText );
1734
1735 BOOST_REQUIRE_EQUAL( reloadedEntries.size(), 1 );
1736 BOOST_CHECK_EQUAL( reloadedEntries[0].panelType, VIA_STYLE );
1737
1738 auto reloadedViaData =
1739 std::dynamic_pointer_cast<DRC_RE_VIA_STYLE_CONSTRAINT_DATA>( reloadedEntries[0].constraintData );
1740 BOOST_REQUIRE( reloadedViaData );
1741
1742 BOOST_CHECK_CLOSE( reloadedViaData->GetMinViaDiameter(), originalMinDia, 0.0001 );
1743 BOOST_CHECK_CLOSE( reloadedViaData->GetMinViaHoleSize(), originalMinHole, 0.0001 );
1744}
1745
1746BOOST_AUTO_TEST_CASE( IntegrationSplitRulePreservesOrder )
1747{
1748 // Test: A rule with multiple constraint types splits and preserves order
1749 wxString ruleText =
1750 "(version 1)\n"
1751 "(rule \"Rule A\"\n"
1752 " (constraint clearance (min 0.2mm)))\n"
1753 "(rule \"Rule B\"\n"
1754 " (constraint via_diameter (min 0.5mm))\n"
1755 " (constraint hole_size (min 0.2mm))\n"
1756 " (constraint track_width (min 0.15mm)))\n"
1757 "(rule \"Rule C\"\n"
1758 " (constraint edge_clearance (min 0.3mm)))";
1759
1760 DRC_RULE_LOADER loader;
1761 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1762
1763 // Rule B splits into VIA_STYLE and ROUTING_WIDTH
1764 BOOST_REQUIRE_GE( entries.size(), 4 );
1765
1766 // Find entries by rule name
1767 int ruleACount = 0, ruleBCount = 0, ruleCCount = 0;
1768
1769 for( const auto& entry : entries )
1770 {
1771 if( entry.ruleName == "Rule A" )
1772 ruleACount++;
1773 else if( entry.ruleName == "Rule B" )
1774 ruleBCount++;
1775 else if( entry.ruleName == "Rule C" )
1776 ruleCCount++;
1777 }
1778
1779 BOOST_CHECK_EQUAL( ruleACount, 1 );
1780 BOOST_CHECK_GE( ruleBCount, 2 );
1781 BOOST_CHECK_EQUAL( ruleCCount, 1 );
1782
1783 // Verify original order: all Rule A entries before Rule B, Rule B before Rule C
1784 int lastAIndex = -1, firstBIndex = entries.size(), lastBIndex = -1, firstCIndex = entries.size();
1785
1786 for( size_t i = 0; i < entries.size(); i++ )
1787 {
1788 if( entries[i].ruleName == "Rule A" )
1789 lastAIndex = i;
1790 else if( entries[i].ruleName == "Rule B" )
1791 {
1792 if( (int)i < firstBIndex )
1793 firstBIndex = i;
1794
1795 lastBIndex = i;
1796 }
1797 else if( entries[i].ruleName == "Rule C" )
1798 {
1799 if( (int)i < firstCIndex )
1800 firstCIndex = i;
1801 }
1802 }
1803
1804 BOOST_CHECK( lastAIndex < firstBIndex );
1805 BOOST_CHECK( lastBIndex < firstCIndex );
1806}
1807
1808
1809// ============================================================================
1810// Condition Group Panel Tests
1811// ============================================================================
1812
1813BOOST_AUTO_TEST_CASE( ConditionGroupBuildSingleCondition )
1814{
1815 // Test: Single condition builds correctly
1816 // This tests the expression building logic used by DRC_RE_CONDITION_GROUP_PANEL
1817 wxString expr = "A.NetName == 'VCC'";
1818
1819 // When parsed and rebuilt, the result should be equivalent
1820 BOOST_CHECK( !expr.IsEmpty() );
1821 BOOST_CHECK( expr.Contains( "NetName" ) );
1822 BOOST_CHECK( expr.Contains( "VCC" ) );
1823}
1824
1825BOOST_AUTO_TEST_CASE( ConditionGroupTokenizeAndOperator )
1826{
1827 // Test: Tokenizing conditions with AND operator
1828 wxString expr = "A.NetName == 'VCC' && B.NetClass == 'Power'";
1829
1830 // Verify the expression contains expected operators
1831 BOOST_CHECK( expr.Contains( "&&" ) );
1832 BOOST_CHECK( expr.Contains( "NetName" ) );
1833 BOOST_CHECK( expr.Contains( "NetClass" ) );
1834}
1835
1836BOOST_AUTO_TEST_CASE( ConditionGroupTokenizeOrOperator )
1837{
1838 // Test: Tokenizing conditions with OR operator
1839 wxString expr = "A.intersectsArea('underFPGA') || A.intersectsArea('underDDR')";
1840
1841 // Verify the expression contains expected operators
1842 BOOST_CHECK( expr.Contains( "||" ) );
1843 BOOST_CHECK( expr.Contains( "intersectsArea" ) );
1844}
1845
1846BOOST_AUTO_TEST_CASE( ConditionGroupTokenizeAndNotOperator )
1847{
1848 // Test: Tokenizing conditions with AND NOT operator
1849 wxString expr = "A.NetClass == 'Power' && !A.intersectsArea('NoRouting')";
1850
1851 // Verify the expression contains expected operators and negation
1852 BOOST_CHECK( expr.Contains( "&&" ) );
1853 BOOST_CHECK( expr.Contains( "!" ) );
1854}
1855
1856BOOST_AUTO_TEST_CASE( ConditionGroupTokenizeComplexExpression )
1857{
1858 // Test: Complex expression from vme-wren.kicad_dru
1859 wxString expr = "A.intersectsArea('underFPGA') || A.intersectsArea('underDDR')";
1860
1861 // The expression should contain the expected patterns
1862 BOOST_CHECK( expr.Contains( "||" ) );
1863 BOOST_CHECK( expr.Contains( "underFPGA" ) );
1864 BOOST_CHECK( expr.Contains( "underDDR" ) );
1865
1866 // Count number of conditions (2 in this case)
1867 size_t orCount = 0;
1868 size_t pos = 0;
1869
1870 while( ( pos = expr.find( "||", pos ) ) != wxString::npos )
1871 {
1872 orCount++;
1873 pos += 2;
1874 }
1875
1876 BOOST_CHECK_EQUAL( orCount, 1 );
1877}
1878
1879
1880// ============================================================================
1881// DRC Engine Item Filtering Tests
1882// ============================================================================
1883
1884BOOST_AUTO_TEST_CASE( ItemFilterExcludesNetInfoAndGenerator )
1885{
1886 // Test: Verify the item type filter logic matches expected exclusions
1887 // This is a unit test for the switch statement in GetItemsMatchingCondition
1888
1889 std::vector<KICAD_T> excludedTypes = { PCB_NETINFO_T, PCB_GENERATOR_T, PCB_GROUP_T };
1890 std::vector<KICAD_T> includedTypes = { PCB_TRACE_T, PCB_VIA_T, PCB_PAD_T, PCB_FOOTPRINT_T,
1892
1893 for( KICAD_T type : excludedTypes )
1894 {
1895 switch( type )
1896 {
1897 case PCB_NETINFO_T:
1898 case PCB_GENERATOR_T:
1899 case PCB_GROUP_T:
1900 BOOST_CHECK( true ); // These should be excluded (continue in real code)
1901 break;
1902
1903 default:
1904 BOOST_FAIL( "Excluded type not handled in filter" );
1905 break;
1906 }
1907 }
1908
1909 for( KICAD_T type : includedTypes )
1910 {
1911 switch( type )
1912 {
1913 case PCB_NETINFO_T:
1914 case PCB_GENERATOR_T:
1915 case PCB_GROUP_T:
1916 BOOST_FAIL( "Included type incorrectly matched by filter" );
1917 break;
1918
1919 default:
1920 BOOST_CHECK( true ); // These should be included
1921 break;
1922 }
1923 }
1924}
1925
MINOPTMAX< int > & Value()
Definition drc_rule.h:188
Maps DRC rule constraints to appropriate editor panels.
DRC_RULE_EDITOR_CONSTRAINT_NAME GetPanelForConstraint(DRC_CONSTRAINT_T aConstraintType)
Get the panel type for a single constraint.
bool CanPanelLoad(DRC_RULE_EDITOR_CONSTRAINT_NAME aPanel, const std::set< DRC_CONSTRAINT_T > &aConstraints)
Check if a specific panel type can load a set of constraints.
std::vector< DRC_PANEL_MATCH > MatchRule(const DRC_RULE &aRule)
Match a DRC rule to one or more panel types.
VALIDATION_RESULT Validate() const override
Validates the constraint data.
VALIDATION_RESULT Validate() const override
Validates the constraint data.
void SetRuleCondition(wxString aRuleCondition)
VALIDATION_RESULT Validate() const override
Validates the constraint data.
Simple constraint data object used by custom rules.
VALIDATION_RESULT Validate() const override
Validates the constraint data.
VALIDATION_RESULT Validate() const override
Validates the constraint data.
VALIDATION_RESULT Validate() const override
Validates the constraint data.
VALIDATION_RESULT Validate() const override
Validates the constraint data.
VALIDATION_RESULT Validate() const override
Validates the constraint data.
wxString GenerateRule(const RULE_GENERATION_CONTEXT &aContext) override
VALIDATION_RESULT Validate() const override
Validates the constraint data.
VALIDATION_RESULT Validate() const override
Validates the constraint data.
wxString GenerateRule(const RULE_GENERATION_CONTEXT &aContext) override
static bool SaveRules(const wxString &aFilename, const std::vector< std::shared_ptr< DRC_RE_BASE_CONSTRAINT_DATA > > &aRules, const BOARD *aBoard)
static void RegisterRuleConverter(RuleConverter aConverter)
static std::vector< std::shared_ptr< DRC_RE_BASE_CONSTRAINT_DATA > > ParseRules(const wxString &aRules)
Loads DRC rules from .kicad_dru files and converts them to panel entries.
std::vector< DRC_RE_LOADED_PANEL_ENTRY > LoadFromString(const wxString &aRulesText)
Load rules from a text string.
Saves DRC panel entries back to .kicad_dru files.
wxString GenerateRulesText(const std::vector< DRC_RE_LOADED_PANEL_ENTRY > &aEntries, const BOARD *aBoard=nullptr)
Generate rule text from panel entries.
void AddConstraint(DRC_CONSTRAINT &aConstraint)
Definition drc_rule.cpp:60
void SetMin(T v)
Definition minoptmax.h:41
void SetOpt(T v)
Definition minoptmax.h:43
void SetMax(T v)
Definition minoptmax.h:42
wxString GetRuleName()
Get the name of the rule.
@ VIA_DIAMETER_CONSTRAINT
Definition drc_rule.h:70
@ DIFF_PAIR_GAP_CONSTRAINT
Definition drc_rule.h:73
@ TRACK_WIDTH_CONSTRAINT
Definition drc_rule.h:59
@ SILK_CLEARANCE_CONSTRAINT
Definition drc_rule.h:56
@ EDGE_CLEARANCE_CONSTRAINT
Definition drc_rule.h:53
@ TEXT_THICKNESS_CONSTRAINT
Definition drc_rule.h:58
@ LENGTH_CONSTRAINT
Definition drc_rule.h:71
@ CLEARANCE_CONSTRAINT
Definition drc_rule.h:49
@ MAX_UNCOUPLED_CONSTRAINT
Definition drc_rule.h:74
@ SKEW_CONSTRAINT
Definition drc_rule.h:72
@ HOLE_CLEARANCE_CONSTRAINT
Definition drc_rule.h:51
@ HOLE_SIZE_CONSTRAINT
Definition drc_rule.h:54
@ TEXT_HEIGHT_CONSTRAINT
Definition drc_rule.h:57
@ HOLE_TO_HOLE_CONSTRAINT
Definition drc_rule.h:52
@ SILK_TO_SILK_CLEARANCE
@ ROUTING_DIFF_PAIR
@ HOLE_TO_HOLE_CLEARANCE
@ COURTYARD_CLEARANCE
@ MINIMUM_CONNECTION_WIDTH
@ ABSOLUTE_LENGTH
@ MINIMUM_CLEARANCE
@ MINIMUM_TEXT_HEIGHT_AND_THICKNESS
@ COPPER_TO_HOLE_CLEARANCE
@ MATCHED_LENGTH_DIFF_PAIR
@ COPPER_TO_EDGE_CLEARANCE
@ MINIMUM_VIA_DIAMETER
@ RPT_SEVERITY_IGNORE
Represents a rule loaded from a .kicad_dru file and mapped to a panel.
wxString ruleName
wxString originalRuleText
wxString condition
bool wasEdited
std::shared_ptr< DRC_RE_BASE_CONSTRAINT_DATA > constraintData
DRC_RULE_EDITOR_CONSTRAINT_NAME panelType
Structure representing a node in a rule tree, collection of this used for building the rule tree.
std::shared_ptr< RULE_EDITOR_DATA_BASE > m_nodeData
Result of a validation operation.
std::vector< wxString > errors
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_CASE(RoundTripViaStyle)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
int clearance
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")
KICAD_T
The set of class identification values stored in EDA_ITEM::m_structType.
Definition typeinfo.h:78
@ PCB_SHAPE_T
class PCB_SHAPE, a segment not on copper layers
Definition typeinfo.h:88
@ PCB_GENERATOR_T
class PCB_GENERATOR, generator on a layer
Definition typeinfo.h:91
@ PCB_VIA_T
class PCB_VIA, a via (like a track segment on a copper layer)
Definition typeinfo.h:97
@ PCB_GROUP_T
class PCB_GROUP, a set of BOARD_ITEMs
Definition typeinfo.h:111
@ PCB_ZONE_T
class ZONE, a copper pour area
Definition typeinfo.h:108
@ PCB_TEXT_T
class PCB_TEXT, text on a layer
Definition typeinfo.h:92
@ PCB_FOOTPRINT_T
class FOOTPRINT, a footprint
Definition typeinfo.h:86
@ PCB_PAD_T
class PAD, a pad in a footprint
Definition typeinfo.h:87
@ PCB_NETINFO_T
class NETINFO_ITEM, a description of a net
Definition typeinfo.h:110
@ PCB_TRACE_T
class PCB_TRACK, a track segment (segment on a copper layer)
Definition typeinfo.h:96