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, see <https://www.gnu.org/licenses/>.
18 */
19
20#include <boost/test/unit_test.hpp>
21#include <wx/ffile.h>
22#include <board.h>
23#include <core/typeinfo.h>
35#include <drc/drc_rule.h>
43#include <drc/drc_engine.h>
46#include <footprint.h>
47
48BOOST_AUTO_TEST_SUITE( DRC_RULE_EDITOR )
49
50BOOST_AUTO_TEST_CASE( RoundTripViaStyle )
51{
52 DRC_RE_VIA_STYLE_CONSTRAINT_DATA original( 0, 0, "My_Via_Rule", 0.5, 0.8, 0.2, 0.4 );
53 original.SetConstraintCode( "via_style" );
54 original.SetRuleCondition( "A.NetClass == 'Power'" );
55
57 ctx.ruleName = original.GetRuleName();
59 ctx.constraintCode = original.GetConstraintCode();
60
61 wxString ruleText = original.GenerateRule( ctx );
62
63 auto parsedRules = DRC_RULE_EDITOR_UTILS::ParseRules( ruleText );
64
65 BOOST_REQUIRE_EQUAL( parsedRules.size(), 1 );
66 auto parsed = std::dynamic_pointer_cast<DRC_RE_VIA_STYLE_CONSTRAINT_DATA>( parsedRules[0] );
67 BOOST_REQUIRE( parsed );
68
69 BOOST_CHECK_EQUAL( parsed->GetRuleName(), original.GetRuleName() );
70 BOOST_CHECK_EQUAL( parsed->GetRuleCondition(), original.GetRuleCondition() );
71 BOOST_CHECK_CLOSE( parsed->GetMinViaDiameter(), original.GetMinViaDiameter(), 0.0001 );
72 BOOST_CHECK_CLOSE( parsed->GetMaxViaDiameter(), original.GetMaxViaDiameter(), 0.0001 );
73 BOOST_CHECK_CLOSE( parsed->GetMinViaHoleSize(), original.GetMinViaHoleSize(), 0.0001 );
74 BOOST_CHECK_CLOSE( parsed->GetMaxViaHoleSize(), original.GetMaxViaHoleSize(), 0.0001 );
75}
76
77
78BOOST_AUTO_TEST_CASE( ShowMatchesIgnoresPairTokensInStringLiterals )
79{
80 BOARD board;
81
82 FOOTPRINT* footprint = new FOOTPRINT( &board );
83 footprint->SetReference( wxT( "B.Width" ) );
84 board.Add( footprint );
85
86 auto rule = std::make_shared<DRC_RULE>( wxT( "String literal" ) );
87 rule->m_Condition = new DRC_RULE_CONDITION( wxT( "A.Reference == 'B.Width'" ) );
88 BOOST_REQUIRE( rule->m_Condition->Compile( nullptr ) );
89
91 rule->AddConstraint( constraint );
92
93 DRC_ENGINE engine( &board, &board.GetDesignSettings() );
94 std::vector<BOARD_ITEM*> matches = engine.GetItemsMatchingRule( rule, nullptr );
95
96 BOOST_REQUIRE_EQUAL( matches.size(), 1u );
97 BOOST_CHECK_EQUAL( matches.front(), footprint );
98}
99
100
101BOOST_AUTO_TEST_CASE( RoundTripRoutingWidth )
102{
103 DRC_RE_ROUTING_WIDTH_CONSTRAINT_DATA original( 0, 0, "My_Track_Rule", 0.3, 0.1 );
104 original.SetConstraintCode( "track_width" );
105 original.SetRuleCondition( "A.NetClass == 'Signal'" );
106
108 ctx.ruleName = original.GetRuleName();
109 ctx.conditionExpression = original.GetRuleCondition();
110 ctx.constraintCode = original.GetConstraintCode();
111
112 wxString ruleText = original.GenerateRule( ctx );
113
114 auto parsedRules = DRC_RULE_EDITOR_UTILS::ParseRules( ruleText );
115
116 BOOST_REQUIRE_EQUAL( parsedRules.size(), 1 );
117 auto parsed = std::dynamic_pointer_cast<DRC_RE_ROUTING_WIDTH_CONSTRAINT_DATA>( parsedRules[0] );
118 BOOST_REQUIRE( parsed );
119
120 BOOST_CHECK_EQUAL( parsed->GetRuleName(), original.GetRuleName() );
121 BOOST_CHECK_EQUAL( parsed->GetRuleCondition(), original.GetRuleCondition() );
122 BOOST_CHECK_CLOSE( parsed->GetOptWidth(), original.GetOptWidth(), 0.0001 );
123 BOOST_CHECK_CLOSE( parsed->GetWidthTolerance(), original.GetWidthTolerance(), 0.0001 );
124}
125
127{
128 std::vector<std::shared_ptr<DRC_RE_BASE_CONSTRAINT_DATA>> rules;
129
130 auto rule1 = std::make_shared<DRC_RE_VIA_STYLE_CONSTRAINT_DATA>( 0, 0, "ViaRule", 0.5, 0.8, 0.2, 0.4 );
131 rule1->SetConstraintCode( "via_style" );
132 rule1->SetRuleCondition( "A.NetClass == 'Power'" );
133 rules.push_back( rule1 );
134
135 auto rule2 = std::make_shared<DRC_RE_ROUTING_WIDTH_CONSTRAINT_DATA>( 0, 0, "TrackRule", 0.3, 0.1 );
136 rule2->SetConstraintCode( "track_width" );
137 rule2->SetRuleCondition( "A.NetClass == 'Signal'" );
138 rules.push_back( rule2 );
139
140 wxString filename = "test_rules.dru";
141
142 // We pass nullptr for board, so layers won't be saved, which is fine for this test
143 bool result = DRC_RULE_EDITOR_UTILS::SaveRules( filename, rules, nullptr );
145
146 wxFFile file( filename );
147 BOOST_REQUIRE( file.IsOpened() );
148 wxString content;
149 file.ReadAll( &content );
150
151 // Verify content
152 BOOST_CHECK( content.Contains( "(version 2)" ) );
153 BOOST_CHECK( content.Contains( "(rule ViaRule" ) );
154 BOOST_CHECK( content.Contains( "(constraint via_diameter" ) );
155 BOOST_CHECK( content.Contains( "(constraint hole_size" ) );
156 BOOST_CHECK( content.Contains( "(rule TrackRule" ) );
157 BOOST_CHECK( content.Contains( "(constraint track_width" ) );
158
159 // Clean up
160 wxRemoveFile( filename );
161}
162
163BOOST_AUTO_TEST_CASE( SaveRulesToFileLogic )
164{
165 // Mimic m_ruleTreeNodeDatas
166 std::vector<RULE_TREE_NODE> ruleTreeNodeDatas;
167
168 // Add a rule node
169 RULE_TREE_NODE node;
170 node.m_nodeId = 1;
171 node.m_nodeName = "TestRule";
172 node.m_nodeType = RULE; // 4
173
174 auto data = std::make_shared<DRC_RE_BASE_CONSTRAINT_DATA>( 1, 0, "TestRule" );
175 data->SetConstraintCode( "clearance" );
176 node.m_nodeData = data;
177
178 ruleTreeNodeDatas.push_back( node );
179
180 // Mimic SaveRulesToFile logic
181 std::vector<std::shared_ptr<DRC_RE_BASE_CONSTRAINT_DATA>> rules;
182
183 for( const RULE_TREE_NODE& n : ruleTreeNodeDatas )
184 {
185 if( n.m_nodeType != RULE )
186 continue;
187
188 auto d = std::dynamic_pointer_cast<DRC_RE_BASE_CONSTRAINT_DATA>( n.m_nodeData );
189
190 if( d )
191 rules.push_back( d );
192 }
193
194 BOOST_CHECK_EQUAL( rules.size(), 1 );
195}
196
197BOOST_AUTO_TEST_CASE( SaveNumericRules )
198{
199 auto rule = std::make_shared<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>( 0, 0, 0.2, "ClearanceRule" );
200 rule->SetConstraintCode( "clearance" );
201
203 ctx.ruleName = rule->GetRuleName();
204 ctx.constraintCode = rule->GetConstraintCode();
205
206 wxString ruleText = rule->GenerateRule( ctx );
207
208 BOOST_CHECK( ruleText.Contains( "(constraint clearance (min 0.2mm))" ) );
209
210 // Test via_count (max, unitless)
211 auto rule2 = std::make_shared<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>( 0, 0, 10, "ViaCountRule" );
212 rule2->SetConstraintCode( "via_count" );
213 ctx.ruleName = rule2->GetRuleName();
214 ctx.constraintCode = rule2->GetConstraintCode();
215
216 ruleText = rule2->GenerateRule( ctx );
217 BOOST_CHECK( ruleText.Contains( "(constraint via_count (max 10))" ) );
218
219 // Test track_angle (min, deg)
220 auto rule3 = std::make_shared<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>( 0, 0, 45, "AngleRule" );
221 rule3->SetConstraintCode( "track_angle" );
222 ctx.ruleName = rule3->GetRuleName();
223 ctx.constraintCode = rule3->GetConstraintCode();
224
225 ruleText = rule3->GenerateRule( ctx );
226 BOOST_CHECK( ruleText.Contains( "(constraint track_angle (min 45deg))" ) );
227}
228
229BOOST_AUTO_TEST_CASE( SaveBoolRules )
230{
231 auto rule = std::make_shared<DRC_RE_BOOL_INPUT_CONSTRAINT_DATA>( 0, 0, true, "DisallowTestRule" );
232 rule->SetConstraintCode( "disallow" );
233
235 ctx.ruleName = rule->GetRuleName();
236 ctx.constraintCode = rule->GetConstraintCode();
237
238 wxString ruleText = rule->GenerateRule( ctx );
239
240 BOOST_CHECK( ruleText.Contains( "(constraint disallow)" ) );
241
242 // Test false value (should not generate constraint)
243 auto rule2 = std::make_shared<DRC_RE_BOOL_INPUT_CONSTRAINT_DATA>( 0, 0, false, "NoDisallowTestRule" );
244 rule2->SetConstraintCode( "disallow" );
245 ctx.ruleName = rule2->GetRuleName();
246 ctx.constraintCode = rule2->GetConstraintCode();
247
248 ruleText = rule2->GenerateRule( ctx );
249 BOOST_CHECK( ruleText.IsEmpty() );
250}
251
252BOOST_AUTO_TEST_CASE( ParseRulesCategories )
253{
254 wxString rules =
255 "(version 1)\n"
256 "(rule \"Clearance Rule\"\n"
257 "\t(constraint clearance (min 0.2mm))\n"
258 "\t(condition \"A.NetClass == 'Power'\"))\n"
259 "(rule \"Via Style Rule\"\n"
260 "\t(constraint via_diameter (min 0.5mm))\n"
261 "\t(constraint hole_size (min 0.3mm))\n"
262 "\t(condition \"A.NetClass == 'Power'\"))\n";
263
264 auto parsedRules = DRC_RULE_EDITOR_UTILS::ParseRules( rules );
265
266 BOOST_REQUIRE_EQUAL( parsedRules.size(), 2 );
267
268 // Check Clearance Rule
269 auto clearanceRule = parsedRules[0];
270 BOOST_CHECK_EQUAL( clearanceRule->GetRuleName(), "Clearance Rule" );
271 BOOST_CHECK_EQUAL( clearanceRule->GetConstraintCode(), "clearance" );
272
273 // Verify it is a numeric constraint data
274 auto numericData = std::dynamic_pointer_cast<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>( clearanceRule );
275 BOOST_REQUIRE( numericData );
276 BOOST_CHECK_CLOSE( numericData->GetNumericInputValue(), 0.2, 0.0001 );
277
278 // Check Via Style Rule
279 auto viaStyleRule = parsedRules[1];
280 BOOST_CHECK_EQUAL( viaStyleRule->GetRuleName(), "Via Style Rule" );
281 BOOST_CHECK_EQUAL( viaStyleRule->GetConstraintCode(), "via_style" );
282
283 auto viaData = std::dynamic_pointer_cast<DRC_RE_VIA_STYLE_CONSTRAINT_DATA>( viaStyleRule );
284 BOOST_REQUIRE( viaData );
285 BOOST_CHECK_CLOSE( viaData->GetMinViaDiameter(), 0.5, 0.0001 );
286 BOOST_CHECK_CLOSE( viaData->GetMinViaHoleSize(), 0.3, 0.0001 );
287}
288
289BOOST_AUTO_TEST_CASE( FactoryRegistration )
290{
291 // Register a custom parser for 'clearance' to override default
292 bool parserCalled = false;
294 [&]( const std::shared_ptr<DRC_RULE>& aRule ) -> std::shared_ptr<DRC_RE_BASE_CONSTRAINT_DATA>
295 {
296 if( aRule->FindConstraint( CLEARANCE_CONSTRAINT ) )
297 {
298 parserCalled = true;
299 auto data = std::make_shared<DRC_RE_CUSTOM_RULE_CONSTRAINT_DATA>( 0, 0, aRule->m_Name );
300 data->SetConstraintCode( "custom_clearance" );
301 return data;
302 }
303 return nullptr;
304 } );
305
306 wxString ruleText = "(version 1) (rule \"Test Rule\" (constraint clearance (min 1.0mm)) (condition \"A.Type == 'Pad'\"))";
307 auto parsedRules = DRC_RULE_EDITOR_UTILS::ParseRules( ruleText );
308
309 BOOST_CHECK( parserCalled );
310 BOOST_REQUIRE_EQUAL( parsedRules.size(), 1 );
311 BOOST_CHECK_EQUAL( parsedRules[0]->GetConstraintCode(), "custom_clearance" );
312}
313
314BOOST_AUTO_TEST_CASE( ValidateViaStyleValid )
315{
316 DRC_RE_VIA_STYLE_CONSTRAINT_DATA data( 0, 0, "ValidRule", 0.5, 0.8, 0.2, 0.4 );
317
319
320 BOOST_CHECK( result.isValid );
321 BOOST_CHECK( result.errors.empty() );
322}
323
324BOOST_AUTO_TEST_CASE( ValidateViaStyleInvalidMinGreaterThanMax )
325{
326 // min > max for via diameter
327 DRC_RE_VIA_STYLE_CONSTRAINT_DATA data( 0, 0, "InvalidRule", 0.9, 0.5, 0.2, 0.4 );
328
330
331 BOOST_CHECK( !result.isValid );
332 BOOST_CHECK( !result.errors.empty() );
333
334 // Should have error about min > max
335 bool foundMinMaxError = false;
336 for( const auto& error : result.errors )
337 {
338 if( error.find( "Minimum Via Diameter cannot be greater than Maximum Via Diameter" ) != std::string::npos )
339 foundMinMaxError = true;
340 }
341 BOOST_CHECK( foundMinMaxError );
342}
343
344BOOST_AUTO_TEST_CASE( ValidateViaStyleInvalidNegativeValues )
345{
346 // Negative values
347 DRC_RE_VIA_STYLE_CONSTRAINT_DATA data( 0, 0, "NegativeRule", -0.5, 0.8, 0.2, 0.4 );
348
350
351 BOOST_CHECK( !result.isValid );
352 BOOST_CHECK( !result.errors.empty() );
353
354 // Should have error about negative value
355 bool foundNegativeError = false;
356 for( const auto& error : result.errors )
357 {
358 if( error.find( "must be greater than 0" ) != std::string::npos )
359 foundNegativeError = true;
360 }
361 BOOST_CHECK( foundNegativeError );
362}
363
364BOOST_AUTO_TEST_CASE( FactoryOverwrite )
365{
366 // Register a parser
368 []( const std::shared_ptr<DRC_RULE>& aRule ) -> std::shared_ptr<DRC_RE_BASE_CONSTRAINT_DATA> {
369 if( aRule->m_Name == "Test" )
370 return std::make_shared<DRC_RE_CUSTOM_RULE_CONSTRAINT_DATA>( 0, 0, aRule->m_Name + "_1" );
371 return nullptr;
372 } );
373
374 // Overwrite it (prepend)
376 []( const std::shared_ptr<DRC_RULE>& aRule ) -> std::shared_ptr<DRC_RE_BASE_CONSTRAINT_DATA> {
377 if( aRule->m_Name == "Test" )
378 return std::make_shared<DRC_RE_CUSTOM_RULE_CONSTRAINT_DATA>( 0, 0, aRule->m_Name + "_2" );
379 return nullptr;
380 } );
381
382 wxString ruleText = "(version 1) (rule \"Test\" (constraint clearance (min 1.0mm)))";
383 auto parsedRules = DRC_RULE_EDITOR_UTILS::ParseRules( ruleText );
384
385 BOOST_REQUIRE_EQUAL( parsedRules.size(), 1 );
386 BOOST_CHECK_EQUAL( parsedRules[0]->GetRuleName(), "Test_2" );
387}
388
389BOOST_AUTO_TEST_CASE( ValidateAbsLengthTwoValid )
390{
391 // Valid: min < opt < max, all positive
392 DRC_RE_ABSOLUTE_LENGTH_TWO_CONSTRAINT_DATA data( 0, 0, 3.0, 2.0, "ValidLengthRule" );
393
395
396 BOOST_CHECK( result.isValid );
397 BOOST_CHECK( result.errors.empty() );
398}
399
400BOOST_AUTO_TEST_CASE( ValidateAbsLengthTwoInvalid )
401{
402 // Invalid: tolerance too large, resulting minimum length is not positive
403 DRC_RE_ABSOLUTE_LENGTH_TWO_CONSTRAINT_DATA data1( 0, 0, 2.0, 5.0, "InvalidToleranceRule" );
404
405 VALIDATION_RESULT result1 = data1.Validate();
406
407 BOOST_CHECK( !result1.isValid );
408 BOOST_CHECK( !result1.errors.empty() );
409
410 bool foundToleranceError = false;
411
412 for( const auto& error : result1.errors )
413 {
414 if( error.find( "Tolerance is too large" ) != std::string::npos )
415 foundToleranceError = true;
416 }
417
418 BOOST_CHECK( foundToleranceError );
419
420 // Invalid: negative tolerance
421 DRC_RE_ABSOLUTE_LENGTH_TWO_CONSTRAINT_DATA data2( 0, 0, 3.0, -1.0, "NegativeToleranceRule" );
422
423 VALIDATION_RESULT result2 = data2.Validate();
424
425 BOOST_CHECK( !result2.isValid );
426
427 bool foundNegativeError = false;
428
429 for( const auto& error : result2.errors )
430 {
431 if( error.find( "Tolerance must be greater than or equal to 0" ) != std::string::npos )
432 foundNegativeError = true;
433 }
434
435 BOOST_CHECK( foundNegativeError );
436}
437
438BOOST_AUTO_TEST_CASE( ValidateDiffPairValid )
439{
440 // Valid: all positive, min <= preferred <= max for width and gap
441 // Constructor: id, parentId, ruleName, optWidth, widthTolerance, optGap, gapTolerance, maxUncoupledLength, maxSkew
442 DRC_RE_ROUTING_DIFF_PAIR_CONSTRAINT_DATA data( 0, 0, "ValidDiffPairRule", 0.3, 0.1, 0.15, 0.05, 10.0 );
443
445
446 BOOST_CHECK( result.isValid );
447 BOOST_CHECK( result.errors.empty() );
448}
449
450BOOST_AUTO_TEST_CASE( ValidateDiffPairInvalid )
451{
452 // Invalid: min width > max width
453 DRC_RE_ROUTING_DIFF_PAIR_CONSTRAINT_DATA data1( 0, 0, "InvalidWidthRule", 0.3, 0.5, 0.15, 0.05, 10.0 );
454
455 VALIDATION_RESULT result1 = data1.Validate();
456
457 BOOST_CHECK( !result1.isValid );
458 BOOST_CHECK( !result1.errors.empty() );
459
460 bool foundWidthError = false;
461 for( const auto& error : result1.errors )
462 {
463 if( error.find( "Width Tolerance must be less than Optimum Width" ) != std::string::npos )
464 foundWidthError = true;
465 }
466 BOOST_CHECK( foundWidthError );
467
468 // Invalid: negative gap
469 DRC_RE_ROUTING_DIFF_PAIR_CONSTRAINT_DATA data2( 0, 0, "NegativeGapRule", 0.3, 0.1, 0.15, -0.1, 10.0 );
470
471 VALIDATION_RESULT result2 = data2.Validate();
472
473 BOOST_CHECK( !result2.isValid );
474
475 bool foundNegativeError = false;
476 for( const auto& error : result2.errors )
477 {
478 if( error.find( "must be greater than or equal to 0" ) != std::string::npos )
479 foundNegativeError = true;
480 }
481 BOOST_CHECK( foundNegativeError );
482}
483
484BOOST_AUTO_TEST_CASE( ValidatePermittedLayersValid )
485{
486 // Valid: at least one layer is selected (top layer enabled)
487 DRC_RE_PERMITTED_LAYERS_CONSTRAINT_DATA data1( 0, 0, "ValidTopLayerRule", true, false );
488
489 VALIDATION_RESULT result1 = data1.Validate();
490
491 BOOST_CHECK( result1.isValid );
492 BOOST_CHECK( result1.errors.empty() );
493
494 // Valid: at least one layer is selected (bottom layer enabled)
495 DRC_RE_PERMITTED_LAYERS_CONSTRAINT_DATA data2( 0, 0, "ValidBottomLayerRule", false, true );
496
497 VALIDATION_RESULT result2 = data2.Validate();
498
499 BOOST_CHECK( result2.isValid );
500 BOOST_CHECK( result2.errors.empty() );
501
502 // Valid: both layers enabled
503 DRC_RE_PERMITTED_LAYERS_CONSTRAINT_DATA data3( 0, 0, "ValidBothLayersRule", true, true );
504
505 VALIDATION_RESULT result3 = data3.Validate();
506
507 BOOST_CHECK( result3.isValid );
508 BOOST_CHECK( result3.errors.empty() );
509}
510
511BOOST_AUTO_TEST_CASE( ValidatePermittedLayersInvalid )
512{
513 // Invalid: no layers selected
514 DRC_RE_PERMITTED_LAYERS_CONSTRAINT_DATA data( 0, 0, "NoLayersRule", false, false );
515
517
518 BOOST_CHECK( !result.isValid );
519 BOOST_CHECK( !result.errors.empty() );
520
521 bool foundLayerError = false;
522 for( const auto& error : result.errors )
523 {
524 if( error.find( "At least one layer must be selected" ) != std::string::npos )
525 foundLayerError = true;
526 }
527 BOOST_CHECK( foundLayerError );
528}
529
530BOOST_AUTO_TEST_CASE( ValidateNumericInputValid )
531{
532 DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA data( 0, 0, 0.5, "ValidRule" );
533
535
536 BOOST_CHECK( result.isValid );
537 BOOST_CHECK( result.errors.empty() );
538}
539
540BOOST_AUTO_TEST_CASE( ValidateNumericInputInvalidNegative )
541{
542 DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA data( 0, 0, -0.5, "InvalidRule" );
543
545
546 BOOST_CHECK( !result.isValid );
547 BOOST_CHECK( !result.errors.empty() );
548
549 // Should have error about value not being greater than 0
550 bool foundError = false;
551 for( const auto& error : result.errors )
552 {
553 if( error.find( "must be greater than 0" ) != std::string::npos )
554 foundError = true;
555 }
556 BOOST_CHECK( foundError );
557}
558
559BOOST_AUTO_TEST_CASE( ValidateRoutingWidthValid )
560{
561 DRC_RE_ROUTING_WIDTH_CONSTRAINT_DATA data( 0, 0, "ValidRule", 0.3, 0.1 );
562
564
565 BOOST_CHECK( result.isValid );
566 BOOST_CHECK( result.errors.empty() );
567}
568
569BOOST_AUTO_TEST_CASE( ValidateRoutingWidthInvalidToleranceTooLarge )
570{
571 // tolerance >= opt width
572 DRC_RE_ROUTING_WIDTH_CONSTRAINT_DATA data( 0, 0, "InvalidRule", 0.3, 0.5 );
573
575
576 BOOST_CHECK( !result.isValid );
577 BOOST_CHECK( !result.errors.empty() );
578
579 bool foundError = false;
580 for( const auto& error : result.errors )
581 {
582 if( error.find( "Width Tolerance must be less than Optimum Width" ) != std::string::npos )
583 foundError = true;
584 }
585 BOOST_CHECK( foundError );
586}
587
588BOOST_AUTO_TEST_CASE( ValidateAllowedOrientationValid )
589{
590 // Valid: at least one orientation is selected (0 degrees)
591 DRC_RE_ALLOWED_ORIENTATION_CONSTRAINT_DATA data1( 0, 0, true, false, false, false, false, "ValidZeroDegRule" );
592
593 VALIDATION_RESULT result1 = data1.Validate();
594
595 BOOST_CHECK( result1.isValid );
596 BOOST_CHECK( result1.errors.empty() );
597
598 // Valid: 90 degrees selected
599 DRC_RE_ALLOWED_ORIENTATION_CONSTRAINT_DATA data2( 0, 0, false, true, false, false, false, "ValidNinetyDegRule" );
600
601 VALIDATION_RESULT result2 = data2.Validate();
602
603 BOOST_CHECK( result2.isValid );
604 BOOST_CHECK( result2.errors.empty() );
605
606 // Valid: all degrees selected
607 DRC_RE_ALLOWED_ORIENTATION_CONSTRAINT_DATA data3( 0, 0, false, false, false, false, true, "ValidAllDegreesRule" );
608
609 VALIDATION_RESULT result3 = data3.Validate();
610
611 BOOST_CHECK( result3.isValid );
612 BOOST_CHECK( result3.errors.empty() );
613
614 // Valid: multiple orientations selected
615 DRC_RE_ALLOWED_ORIENTATION_CONSTRAINT_DATA data4( 0, 0, true, true, false, false, false, "ValidMultipleRule" );
616
617 VALIDATION_RESULT result4 = data4.Validate();
618
619 BOOST_CHECK( result4.isValid );
620 BOOST_CHECK( result4.errors.empty() );
621}
622
623BOOST_AUTO_TEST_CASE( ValidateAllowedOrientationInvalid )
624{
625 // Invalid: no orientation selected
626 DRC_RE_ALLOWED_ORIENTATION_CONSTRAINT_DATA data( 0, 0, false, false, false, false, false, "NoOrientationRule" );
627
629
630 BOOST_CHECK( !result.isValid );
631 BOOST_CHECK( !result.errors.empty() );
632
633 bool foundOrientationError = false;
634 for( const auto& error : result.errors )
635 {
636 if( error.find( "At least one orientation must be selected" ) != std::string::npos )
637 foundOrientationError = true;
638 }
639 BOOST_CHECK( foundOrientationError );
640}
641
642BOOST_AUTO_TEST_CASE( ValidateBoolInputValid )
643{
644 // Test with true value
645 DRC_RE_BOOL_INPUT_CONSTRAINT_DATA dataTrue( 0, 0, true, "BoolRuleTrue" );
646
647 VALIDATION_RESULT resultTrue = dataTrue.Validate();
648
649 BOOST_CHECK( resultTrue.isValid );
650 BOOST_CHECK( resultTrue.errors.empty() );
651
652 // Test with false value
653 DRC_RE_BOOL_INPUT_CONSTRAINT_DATA dataFalse( 0, 0, false, "BoolRuleFalse" );
654
655 VALIDATION_RESULT resultFalse = dataFalse.Validate();
656
657 BOOST_CHECK( resultFalse.isValid );
658 BOOST_CHECK( resultFalse.errors.empty() );
659}
660
661BOOST_AUTO_TEST_CASE( ValidateMinTxtHtThValid )
662{
663 // Valid: both text height and thickness are positive
664 DRC_RE_MINIMUM_TEXT_HEIGHT_THICKNESS_CONSTRAINT_DATA data( 0, 0, "ValidTextRule", 1.0, 0.15 );
665
667
668 BOOST_CHECK( result.isValid );
669 BOOST_CHECK( result.errors.empty() );
670}
671
672BOOST_AUTO_TEST_CASE( ValidateMinTxtHtThInvalid )
673{
674 // Invalid: negative text height
675 DRC_RE_MINIMUM_TEXT_HEIGHT_THICKNESS_CONSTRAINT_DATA data1( 0, 0, "NegativeHeightRule", -1.0, 0.15 );
676
677 VALIDATION_RESULT result1 = data1.Validate();
678
679 BOOST_CHECK( !result1.isValid );
680 BOOST_CHECK( !result1.errors.empty() );
681
682 bool foundHeightError = false;
683 for( const auto& error : result1.errors )
684 {
685 if( error.find( "Minimum Text Height must be greater than 0" ) != std::string::npos )
686 foundHeightError = true;
687 }
688 BOOST_CHECK( foundHeightError );
689
690 // Invalid: negative text thickness
691 DRC_RE_MINIMUM_TEXT_HEIGHT_THICKNESS_CONSTRAINT_DATA data2( 0, 0, "NegativeThicknessRule", 1.0, -0.15 );
692
693 VALIDATION_RESULT result2 = data2.Validate();
694
695 BOOST_CHECK( !result2.isValid );
696
697 bool foundThicknessError = false;
698 for( const auto& error : result2.errors )
699 {
700 if( error.find( "Minimum Text Thickness must be greater than 0" ) != std::string::npos )
701 foundThicknessError = true;
702 }
703 BOOST_CHECK( foundThicknessError );
704
705 // Invalid: both zero (default values)
707
708 VALIDATION_RESULT result3 = data3.Validate();
709
710 BOOST_CHECK( !result3.isValid );
711 BOOST_CHECK_EQUAL( result3.errors.size(), 2 );
712}
713
714BOOST_AUTO_TEST_CASE( ValidateCustomRuleValid )
715{
716 DRC_RE_CUSTOM_RULE_CONSTRAINT_DATA data( 0, 0, "ValidCustomRule" );
717 data.SetRuleText( "(constraint clearance (min 0.2mm))" );
718
720
721 BOOST_CHECK( result.isValid );
722 BOOST_CHECK( result.errors.empty() );
723}
724
725BOOST_AUTO_TEST_CASE( ValidateCustomRuleInvalid )
726{
727 DRC_RE_CUSTOM_RULE_CONSTRAINT_DATA data( 0, 0, "InvalidCustomRule" );
728 data.SetRuleText( "" );
729
731
732 BOOST_CHECK( !result.isValid );
733 BOOST_CHECK( !result.errors.empty() );
734
735 // Should have error about empty rule text
736 bool foundEmptyError = false;
737 for( const auto& error : result.errors )
738 {
739 if( error.find( "Rule text cannot be empty" ) != std::string::npos )
740 foundEmptyError = true;
741 }
742 BOOST_CHECK( foundEmptyError );
743}
744
745
746// ============================================================================
747// Panel Matcher Tests (Phase 5.1)
748// ============================================================================
749
750BOOST_AUTO_TEST_CASE( PanelMatcherExactMatchViaStyle )
751{
752 // Test: Rule with via_diameter + hole_size → VIA_STYLE panel
753 DRC_RULE rule( "ViaStyleRule" );
754
756 viaDia.Value().SetMin( 500000 );
757 viaDia.Value().SetOpt( 600000 );
758 viaDia.Value().SetMax( 800000 );
759 rule.AddConstraint( viaDia );
760
762 holeSize.Value().SetMin( 200000 );
763 holeSize.Value().SetOpt( 300000 );
764 holeSize.Value().SetMax( 400000 );
765 rule.AddConstraint( holeSize );
766
767 DRC_PANEL_MATCHER matcher;
768 std::vector<DRC_PANEL_MATCH> matches = matcher.MatchRule( rule );
769
770 BOOST_REQUIRE_EQUAL( matches.size(), 1 );
771 BOOST_CHECK_EQUAL( matches[0].panelType, VIA_STYLE );
772 BOOST_CHECK_EQUAL( matches[0].claimedConstraints.size(), 2 );
773 BOOST_CHECK( matches[0].claimedConstraints.count( VIA_DIAMETER_CONSTRAINT ) > 0 );
774 BOOST_CHECK( matches[0].claimedConstraints.count( HOLE_SIZE_CONSTRAINT ) > 0 );
775}
776
777BOOST_AUTO_TEST_CASE( PanelMatcherPartialMatchViaDiameter )
778{
779 // Test: Rule with only via_diameter → MINIMUM_VIA_DIAMETER panel
780 DRC_RULE rule( "ViaDiameterOnlyRule" );
781
783 viaDia.Value().SetMin( 500000 );
784 rule.AddConstraint( viaDia );
785
786 DRC_PANEL_MATCHER matcher;
787 std::vector<DRC_PANEL_MATCH> matches = matcher.MatchRule( rule );
788
789 BOOST_REQUIRE_EQUAL( matches.size(), 1 );
790 BOOST_CHECK_EQUAL( matches[0].panelType, MINIMUM_VIA_DIAMETER );
791 BOOST_CHECK_EQUAL( matches[0].claimedConstraints.size(), 1 );
792 BOOST_CHECK( matches[0].claimedConstraints.count( VIA_DIAMETER_CONSTRAINT ) > 0 );
793}
794
795BOOST_AUTO_TEST_CASE( PanelMatcherSplitRule )
796{
797 // Test: Rule with via_diameter + hole_size + clearance → 2 panels
798 DRC_RULE rule( "SplitRule" );
799
801 viaDia.Value().SetMin( 500000 );
802 rule.AddConstraint( viaDia );
803
805 holeSize.Value().SetMin( 200000 );
806 rule.AddConstraint( holeSize );
807
809 clearance.Value().SetMin( 150000 );
810 rule.AddConstraint( clearance );
811
812 DRC_PANEL_MATCHER matcher;
813 std::vector<DRC_PANEL_MATCH> matches = matcher.MatchRule( rule );
814
815 BOOST_REQUIRE_EQUAL( matches.size(), 2 );
816
817 // VIA_STYLE should claim via_diameter + hole_size
818 BOOST_CHECK_EQUAL( matches[0].panelType, VIA_STYLE );
819 BOOST_CHECK_EQUAL( matches[0].claimedConstraints.size(), 2 );
820
821 // MINIMUM_CLEARANCE should claim clearance
822 BOOST_CHECK_EQUAL( matches[1].panelType, MINIMUM_CLEARANCE );
823 BOOST_CHECK_EQUAL( matches[1].claimedConstraints.size(), 1 );
824 BOOST_CHECK( matches[1].claimedConstraints.count( CLEARANCE_CONSTRAINT ) > 0 );
825}
826
827BOOST_AUTO_TEST_CASE( PanelMatcherPriorityDiffPairOverRoutingWidth )
828{
829 // Test: Rule with track_width + diff_pair_gap → ROUTING_DIFF_PAIR (not ROUTING_WIDTH)
830 DRC_RULE rule( "DiffPairRule" );
831
833 trackWidth.Value().SetMin( 200000 );
834 trackWidth.Value().SetOpt( 250000 );
835 trackWidth.Value().SetMax( 300000 );
836 rule.AddConstraint( trackWidth );
837
839 diffGap.Value().SetMin( 100000 );
840 diffGap.Value().SetOpt( 150000 );
841 diffGap.Value().SetMax( 200000 );
842 rule.AddConstraint( diffGap );
843
844 DRC_PANEL_MATCHER matcher;
845 std::vector<DRC_PANEL_MATCH> matches = matcher.MatchRule( rule );
846
847 BOOST_REQUIRE_EQUAL( matches.size(), 1 );
848 BOOST_CHECK_EQUAL( matches[0].panelType, ROUTING_DIFF_PAIR );
849 BOOST_CHECK( matches[0].claimedConstraints.count( TRACK_WIDTH_CONSTRAINT ) > 0 );
850 BOOST_CHECK( matches[0].claimedConstraints.count( DIFF_PAIR_GAP_CONSTRAINT ) > 0 );
851}
852
853BOOST_AUTO_TEST_CASE( PanelMatcherDiffPairWithOptionalUncoupled )
854{
855 // Test: Rule with track_width + diff_pair_gap + uncoupled → ROUTING_DIFF_PAIR claims all three
856 DRC_RULE rule( "DiffPairWithUncoupledRule" );
857
859 trackWidth.Value().SetMin( 200000 );
860 rule.AddConstraint( trackWidth );
861
863 diffGap.Value().SetMin( 100000 );
864 rule.AddConstraint( diffGap );
865
867 uncoupled.Value().SetMax( 5000000 );
868 rule.AddConstraint( uncoupled );
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_DIFF_PAIR );
875 BOOST_CHECK_EQUAL( matches[0].claimedConstraints.size(), 3 );
876 BOOST_CHECK( matches[0].claimedConstraints.count( TRACK_WIDTH_CONSTRAINT ) > 0 );
877 BOOST_CHECK( matches[0].claimedConstraints.count( DIFF_PAIR_GAP_CONSTRAINT ) > 0 );
878 BOOST_CHECK( matches[0].claimedConstraints.count( MAX_UNCOUPLED_CONSTRAINT ) > 0 );
879}
880
881BOOST_AUTO_TEST_CASE( PanelMatcherTrackWidthOnly )
882{
883 // Test: Rule with only track_width → ROUTING_WIDTH (not ROUTING_DIFF_PAIR)
884 DRC_RULE rule( "TrackWidthOnlyRule" );
885
887 trackWidth.Value().SetMin( 200000 );
888 trackWidth.Value().SetOpt( 250000 );
889 trackWidth.Value().SetMax( 300000 );
890 rule.AddConstraint( trackWidth );
891
892 DRC_PANEL_MATCHER matcher;
893 std::vector<DRC_PANEL_MATCH> matches = matcher.MatchRule( rule );
894
895 BOOST_REQUIRE_EQUAL( matches.size(), 1 );
896 BOOST_CHECK_EQUAL( matches[0].panelType, ROUTING_WIDTH );
897 BOOST_CHECK( matches[0].claimedConstraints.count( TRACK_WIDTH_CONSTRAINT ) > 0 );
898}
899
900BOOST_AUTO_TEST_CASE( PanelMatcherTextHeightAndThickness )
901{
902 // Test: Rule with text_height + text_thickness → MINIMUM_TEXT_HEIGHT_AND_THICKNESS
903 DRC_RULE rule( "TextRule" );
904
906 textHeight.Value().SetMin( 1000000 );
907 rule.AddConstraint( textHeight );
908
910 textThickness.Value().SetMin( 150000 );
911 rule.AddConstraint( textThickness );
912
913 DRC_PANEL_MATCHER matcher;
914 std::vector<DRC_PANEL_MATCH> matches = matcher.MatchRule( rule );
915
916 BOOST_REQUIRE_EQUAL( matches.size(), 1 );
918 BOOST_CHECK_EQUAL( matches[0].claimedConstraints.size(), 2 );
919}
920
921BOOST_AUTO_TEST_CASE( PanelMatcherLengthConstraint )
922{
923 // Test: Rule with length constraint matches ABSOLUTE_LENGTH panel
924 DRC_RULE rule( "LengthRule" );
925
927 length.Value().SetMin( 10000000 );
928 length.Value().SetOpt( 30000000 );
929 length.Value().SetMax( 50000000 );
930 rule.AddConstraint( length );
931
932 DRC_PANEL_MATCHER matcher;
933 std::vector<DRC_PANEL_MATCH> matches = matcher.MatchRule( rule );
934
935 BOOST_REQUIRE_EQUAL( matches.size(), 1 );
936 BOOST_CHECK_EQUAL( matches[0].panelType, ABSOLUTE_LENGTH );
937 BOOST_CHECK( matches[0].claimedConstraints.count( LENGTH_CONSTRAINT ) > 0 );
938}
939
940BOOST_AUTO_TEST_CASE( PanelMatcherLengthAndSkewConstraint )
941{
942 // Rule with length + skew should match MATCHED_LENGTH_DIFF_PAIR, not ABSOLUTE_LENGTH
943 DRC_RULE rule( "MatchedLengthRule" );
944
946 length.Value().SetMin( 10000000 );
947 length.Value().SetOpt( 30000000 );
948 length.Value().SetMax( 50000000 );
949 rule.AddConstraint( length );
950
952 skew.Value().SetMax( 1000000 );
953 rule.AddConstraint( skew );
954
955 DRC_PANEL_MATCHER matcher;
956 std::vector<DRC_PANEL_MATCH> matches = matcher.MatchRule( rule );
957
958 BOOST_REQUIRE_EQUAL( matches.size(), 1 );
959 BOOST_CHECK_EQUAL( matches[0].panelType, MATCHED_LENGTH_DIFF_PAIR );
960 BOOST_CHECK( matches[0].claimedConstraints.count( LENGTH_CONSTRAINT ) > 0 );
961 BOOST_CHECK( matches[0].claimedConstraints.count( SKEW_CONSTRAINT ) > 0 );
962}
963
964BOOST_AUTO_TEST_CASE( PanelMatcherEmptyRule )
965{
966 // Test: Rule with no constraints → no matches
967 DRC_RULE rule( "EmptyRule" );
968
969 DRC_PANEL_MATCHER matcher;
970 std::vector<DRC_PANEL_MATCH> matches = matcher.MatchRule( rule );
971
972 BOOST_CHECK_EQUAL( matches.size(), 0 );
973}
974
975BOOST_AUTO_TEST_CASE( PanelMatcherCanPanelLoad )
976{
977 DRC_PANEL_MATCHER matcher;
978
979 // VIA_STYLE can load via_diameter + hole_size
980 std::set<DRC_CONSTRAINT_T> viaStyleConstraints = { VIA_DIAMETER_CONSTRAINT, HOLE_SIZE_CONSTRAINT };
981 BOOST_CHECK( matcher.CanPanelLoad( VIA_STYLE, viaStyleConstraints ) );
982
983 // VIA_STYLE cannot load clearance
984 std::set<DRC_CONSTRAINT_T> clearanceConstraint = { CLEARANCE_CONSTRAINT };
985 BOOST_CHECK( !matcher.CanPanelLoad( VIA_STYLE, clearanceConstraint ) );
986
987 // CUSTOM_RULE can load anything
988 BOOST_CHECK( matcher.CanPanelLoad( CUSTOM_RULE, viaStyleConstraints ) );
989 BOOST_CHECK( matcher.CanPanelLoad( CUSTOM_RULE, clearanceConstraint ) );
990}
991
1004
1005
1006// ============================================================================
1007// Rule Loader Tests (Phase 5.1)
1008// ============================================================================
1009
1010BOOST_AUTO_TEST_CASE( RuleLoaderViaStyleFromText )
1011{
1012 // Test: Load a via style rule from text and verify values
1013 wxString ruleText =
1014 "(version 1)\n"
1015 "(rule \"Via Test\"\n"
1016 " (constraint via_diameter (min 0.5mm) (opt 0.6mm) (max 0.8mm))\n"
1017 " (constraint hole_size (min 0.2mm) (opt 0.3mm) (max 0.4mm)))";
1018
1019 DRC_RULE_LOADER loader;
1020 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1021
1022 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
1023 BOOST_CHECK_EQUAL( entries[0].panelType, VIA_STYLE );
1024 BOOST_CHECK_EQUAL( entries[0].ruleName, "Via Test" );
1025
1026 auto viaData = std::dynamic_pointer_cast<DRC_RE_VIA_STYLE_CONSTRAINT_DATA>( entries[0].constraintData );
1027 BOOST_REQUIRE( viaData );
1028
1029 BOOST_CHECK_CLOSE( viaData->GetMinViaDiameter(), 0.5, 0.0001 );
1030 BOOST_CHECK_CLOSE( viaData->GetMaxViaDiameter(), 0.8, 0.0001 );
1031 BOOST_CHECK_CLOSE( viaData->GetMinViaHoleSize(), 0.2, 0.0001 );
1032 BOOST_CHECK_CLOSE( viaData->GetMaxViaHoleSize(), 0.4, 0.0001 );
1033}
1034
1035BOOST_AUTO_TEST_CASE( RuleLoaderRoutingWidthFromText )
1036{
1037 // Test: Load a routing width rule from text
1038 wxString ruleText =
1039 "(version 1)\n"
1040 "(rule \"Track Test\"\n"
1041 " (constraint track_width (min 0.2mm) (opt 0.25mm) (max 0.3mm)))";
1042
1043 DRC_RULE_LOADER loader;
1044 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1045
1046 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
1047 BOOST_CHECK_EQUAL( entries[0].panelType, ROUTING_WIDTH );
1048 BOOST_CHECK_EQUAL( entries[0].ruleName, "Track Test" );
1049
1050 auto trackData = std::dynamic_pointer_cast<DRC_RE_ROUTING_WIDTH_CONSTRAINT_DATA>( entries[0].constraintData );
1051 BOOST_REQUIRE( trackData );
1052
1053 BOOST_CHECK_CLOSE( trackData->GetOptWidth(), 0.25, 0.0001 );
1054 BOOST_CHECK_CLOSE( trackData->GetWidthTolerance(), 0.05, 0.0001 );
1055}
1056
1057BOOST_AUTO_TEST_CASE( RuleLoaderDiffPairFromText )
1058{
1059 // Test: Load a diff pair rule from text
1060 wxString ruleText =
1061 "(version 1)\n"
1062 "(rule \"Diff Pair Test\"\n"
1063 " (constraint track_width (min 0.2mm) (opt 0.25mm) (max 0.3mm))\n"
1064 " (constraint diff_pair_gap (min 0.1mm) (opt 0.15mm) (max 0.2mm))\n"
1065 " (constraint diff_pair_uncoupled (max 5mm)))";
1066
1067 DRC_RULE_LOADER loader;
1068 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1069
1070 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
1071 BOOST_CHECK_EQUAL( entries[0].panelType, ROUTING_DIFF_PAIR );
1072
1073 auto dpData = std::dynamic_pointer_cast<DRC_RE_ROUTING_DIFF_PAIR_CONSTRAINT_DATA>( entries[0].constraintData );
1074 BOOST_REQUIRE( dpData );
1075
1076 BOOST_CHECK_CLOSE( dpData->GetOptWidth(), 0.25, 0.0001 );
1077 BOOST_CHECK_CLOSE( dpData->GetWidthTolerance(), 0.05, 0.0001 );
1078 BOOST_CHECK_CLOSE( dpData->GetOptGap(), 0.15, 0.0001 );
1079 BOOST_CHECK_CLOSE( dpData->GetGapTolerance(), 0.05, 0.0001 );
1080 BOOST_CHECK_CLOSE( dpData->GetMaxUncoupledLength(), 5.0, 0.0001 );
1081}
1082
1083BOOST_AUTO_TEST_CASE( RuleLoaderSplitRuleFromText )
1084{
1085 // Test: Load a rule that splits into multiple panels
1086 wxString ruleText =
1087 "(version 1)\n"
1088 "(rule \"Split Test\"\n"
1089 " (constraint via_diameter (min 0.5mm))\n"
1090 " (constraint hole_size (min 0.2mm))\n"
1091 " (constraint clearance (min 0.15mm)))";
1092
1093 DRC_RULE_LOADER loader;
1094 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1095
1096 BOOST_REQUIRE_EQUAL( entries.size(), 2 );
1097
1098 // First entry should be VIA_STYLE
1099 BOOST_CHECK_EQUAL( entries[0].panelType, VIA_STYLE );
1100 BOOST_CHECK_EQUAL( entries[0].ruleName, "Split Test" );
1101
1102 // Second entry should be MINIMUM_CLEARANCE
1103 BOOST_CHECK_EQUAL( entries[1].panelType, MINIMUM_CLEARANCE );
1104 BOOST_CHECK_EQUAL( entries[1].ruleName, "Split Test" );
1105
1106 auto numericData = std::dynamic_pointer_cast<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>( entries[1].constraintData );
1107 BOOST_REQUIRE( numericData );
1108 BOOST_CHECK_CLOSE( numericData->GetNumericInputValue(), 0.15, 0.0001 );
1109}
1110
1111BOOST_AUTO_TEST_CASE( RuleLoaderVmeWrenClearanceUnderFpga )
1112{
1113 // Test: Load the clearance_under_fpga rule from vme-wren demo which should split
1114 // This rule has: clearance, hole_size, via_diameter
1115 // Should split into: VIA_STYLE (hole_size + via_diameter) + MINIMUM_CLEARANCE (clearance)
1116 wxString ruleText =
1117 "(version 1)\n"
1118 "(rule \"clearance_under_fpga\"\n"
1119 " (constraint clearance (min 0.1mm))\n"
1120 " (constraint hole_size (min 0.2mm))\n"
1121 " (constraint via_diameter (min 0.4mm))\n"
1122 " (condition \"A.intersectsArea('underFPGA') || A.intersectsArea('underDDR')\"))";
1123
1124 DRC_RULE_LOADER loader;
1125 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1126
1127 // Should produce 2 entries from the split
1128 BOOST_REQUIRE_EQUAL( entries.size(), 2 );
1129
1130 // First entry should be VIA_STYLE (claims via_diameter + hole_size at priority 90)
1131 BOOST_CHECK_EQUAL( entries[0].panelType, VIA_STYLE );
1132 BOOST_CHECK_EQUAL( entries[0].ruleName, "clearance_under_fpga" );
1133 BOOST_CHECK_EQUAL( entries[0].condition, "A.intersectsArea('underFPGA') || A.intersectsArea('underDDR')" );
1134
1135 // Second entry should be MINIMUM_CLEARANCE (claims clearance at priority 30)
1136 BOOST_CHECK_EQUAL( entries[1].panelType, MINIMUM_CLEARANCE );
1137 BOOST_CHECK_EQUAL( entries[1].ruleName, "clearance_under_fpga" );
1138 BOOST_CHECK_EQUAL( entries[1].condition, "A.intersectsArea('underFPGA') || A.intersectsArea('underDDR')" );
1139
1140 // Verify VIA_STYLE has correct values
1141 auto viaData = std::dynamic_pointer_cast<DRC_RE_VIA_STYLE_CONSTRAINT_DATA>( entries[0].constraintData );
1142 BOOST_REQUIRE( viaData );
1143 BOOST_CHECK_CLOSE( viaData->GetMinViaDiameter(), 0.4, 0.0001 );
1144 BOOST_CHECK_CLOSE( viaData->GetMinViaHoleSize(), 0.2, 0.0001 );
1145
1146 // Verify MINIMUM_CLEARANCE has correct value
1147 auto clearanceData = std::dynamic_pointer_cast<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>( entries[1].constraintData );
1148 BOOST_REQUIRE( clearanceData );
1149 BOOST_CHECK_CLOSE( clearanceData->GetNumericInputValue(), 0.1, 0.0001 );
1150}
1151
1152BOOST_AUTO_TEST_CASE( RuleLoaderTextHeightThicknessFromText )
1153{
1154 // Test: Load text height and thickness rule
1155 wxString ruleText =
1156 "(version 1)\n"
1157 "(rule \"Text Test\"\n"
1158 " (constraint text_height (min 1mm))\n"
1159 " (constraint text_thickness (min 0.15mm)))";
1160
1161 DRC_RULE_LOADER loader;
1162 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1163
1164 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
1166
1167 auto textData = std::dynamic_pointer_cast<DRC_RE_MINIMUM_TEXT_HEIGHT_THICKNESS_CONSTRAINT_DATA>( entries[0].constraintData );
1168 BOOST_REQUIRE( textData );
1169
1170 BOOST_CHECK_CLOSE( textData->GetMinTextHeight(), 1.0, 0.0001 );
1171 BOOST_CHECK_CLOSE( textData->GetMinTextThickness(), 0.15, 0.0001 );
1172}
1173
1174BOOST_AUTO_TEST_CASE( RuleLoaderAbsoluteLengthFromText )
1175{
1176 // Test: Load absolute length rule with min/opt/max
1177 wxString ruleText =
1178 "(version 1)\n"
1179 "(rule \"Length Test\"\n"
1180 " (constraint length (min 10mm) (opt 30mm) (max 50mm)))";
1181
1182 DRC_RULE_LOADER loader;
1183 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1184
1185 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
1186 BOOST_CHECK_EQUAL( entries[0].panelType, ABSOLUTE_LENGTH );
1187 BOOST_CHECK_EQUAL( entries[0].ruleName, "Length Test" );
1188
1189 auto lengthData = dynamic_pointer_cast<DRC_RE_ABSOLUTE_LENGTH_TWO_CONSTRAINT_DATA>( entries[0].constraintData );
1190 BOOST_REQUIRE( lengthData );
1191 BOOST_CHECK_CLOSE( lengthData->GetMinimumLength(), 10.0, 0.0001 );
1192 BOOST_CHECK_CLOSE( lengthData->GetOptimumLength(), 30.0, 0.0001 );
1193 BOOST_CHECK_CLOSE( lengthData->GetMaximumLength(), 50.0, 0.0001 );
1194}
1195
1196BOOST_AUTO_TEST_CASE( RuleLoaderMatchedLengthWithSkew )
1197{
1198 // Rule with length + skew should load as MATCHED_LENGTH_DIFF_PAIR and preserve skew
1199 wxString ruleText =
1200 "(version 1)\n"
1201 "(rule \"DP Length Match\"\n"
1202 " (constraint length (min 10mm) (opt 30mm) (max 50mm))\n"
1203 " (constraint skew (max 1mm)))";
1204
1205 DRC_RULE_LOADER loader;
1206 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1207
1208 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
1209 BOOST_CHECK_EQUAL( entries[0].panelType, MATCHED_LENGTH_DIFF_PAIR );
1210 BOOST_CHECK_EQUAL( entries[0].ruleName, "DP Length Match" );
1211
1212 auto matchedData =
1213 std::dynamic_pointer_cast<DRC_RE_MATCHED_LENGTH_DIFF_PAIR_CONSTRAINT_DATA>( entries[0].constraintData );
1214 BOOST_REQUIRE( matchedData );
1215
1216 BOOST_CHECK_CLOSE( matchedData->GetMinimumLength(), 10.0, 0.0001 );
1217 BOOST_CHECK_CLOSE( matchedData->GetOptimumLength(), 30.0, 0.0001 );
1218 BOOST_CHECK_CLOSE( matchedData->GetMaximumLength(), 50.0, 0.0001 );
1219 BOOST_CHECK_CLOSE( matchedData->GetMaxSkew(), 1.0, 0.0001 );
1220}
1221
1222BOOST_AUTO_TEST_CASE( RuleLoaderMatchedLengthRoundTrip )
1223{
1224 // Verify length+skew rule round-trips without losing skew
1225 wxString ruleText =
1226 "(version 1)\n"
1227 "(rule \"DP Match RT\"\n"
1228 " (constraint length (min 10mm) (opt 30mm) (max 50mm))\n"
1229 " (constraint skew (max 2.5mm)))";
1230
1231 DRC_RULE_LOADER loader;
1232 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1233
1234 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
1235 entries[0].wasEdited = true;
1236
1237 DRC_RULE_SAVER saver;
1238 wxString savedText = saver.GenerateRulesText( entries, nullptr );
1239
1240 BOOST_CHECK( savedText.Contains( "length" ) );
1241 BOOST_CHECK( savedText.Contains( "skew" ) );
1242 BOOST_CHECK( savedText.Contains( "2.5mm" ) );
1243
1244 std::vector<DRC_RE_LOADED_PANEL_ENTRY> reloaded = loader.LoadFromString( savedText );
1245
1246 BOOST_REQUIRE_EQUAL( reloaded.size(), 1 );
1247 BOOST_CHECK_EQUAL( reloaded[0].panelType, MATCHED_LENGTH_DIFF_PAIR );
1248
1249 auto reloadedData =
1250 std::dynamic_pointer_cast<DRC_RE_MATCHED_LENGTH_DIFF_PAIR_CONSTRAINT_DATA>( reloaded[0].constraintData );
1251 BOOST_REQUIRE( reloadedData );
1252
1253 BOOST_CHECK_CLOSE( reloadedData->GetMaxSkew(), 2.5, 0.0001 );
1254 BOOST_CHECK_CLOSE( reloadedData->GetMinimumLength(), 10.0, 0.0001 );
1255 BOOST_CHECK_CLOSE( reloadedData->GetOptimumLength(), 30.0, 0.0001 );
1256 BOOST_CHECK_CLOSE( reloadedData->GetMaximumLength(), 50.0, 0.0001 );
1257}
1258
1259BOOST_AUTO_TEST_CASE( RuleLoaderMatchedLengthWithinDiffPairs )
1260{
1261 // Rule with skew + within_diff_pairs should load and preserve the option
1262 wxString ruleText = "(version 1)\n"
1263 "(rule \"DP Skew Within\"\n"
1264 " (constraint length (min 10mm) (opt 30mm) (max 50mm))\n"
1265 " (constraint skew (max 1mm) (within_diff_pairs)))";
1266
1267 DRC_RULE_LOADER loader;
1268 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1269
1270 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
1271 BOOST_CHECK_EQUAL( entries[0].panelType, MATCHED_LENGTH_DIFF_PAIR );
1272
1273 auto matchedData =
1274 std::dynamic_pointer_cast<DRC_RE_MATCHED_LENGTH_DIFF_PAIR_CONSTRAINT_DATA>( entries[0].constraintData );
1275 BOOST_REQUIRE( matchedData );
1276
1277 BOOST_CHECK_CLOSE( matchedData->GetMaxSkew(), 1.0, 0.0001 );
1278 BOOST_CHECK( matchedData->GetWithinDiffPairs() );
1279
1280 // Round-trip: save and reload
1281 entries[0].wasEdited = true;
1282
1283 DRC_RULE_SAVER saver;
1284 wxString savedText = saver.GenerateRulesText( entries, nullptr );
1285
1286 BOOST_CHECK( savedText.Contains( "within_diff_pairs" ) );
1287
1288 std::vector<DRC_RE_LOADED_PANEL_ENTRY> reloaded = loader.LoadFromString( savedText );
1289
1290 BOOST_REQUIRE_EQUAL( reloaded.size(), 1 );
1291
1292 auto reloadedData =
1293 std::dynamic_pointer_cast<DRC_RE_MATCHED_LENGTH_DIFF_PAIR_CONSTRAINT_DATA>( reloaded[0].constraintData );
1294 BOOST_REQUIRE( reloadedData );
1295
1296 BOOST_CHECK_CLOSE( reloadedData->GetMaxSkew(), 1.0, 0.0001 );
1297 BOOST_CHECK( reloadedData->GetWithinDiffPairs() );
1298}
1299
1300BOOST_AUTO_TEST_CASE( RuleLoaderMatchedLengthWithoutWithinDiffPairs )
1301{
1302 // Rule without within_diff_pairs should default to false
1303 wxString ruleText = "(version 1)\n"
1304 "(rule \"DP Skew Bus\"\n"
1305 " (constraint length (min 10mm) (opt 30mm) (max 50mm))\n"
1306 " (constraint skew (max 2mm)))";
1307
1308 DRC_RULE_LOADER loader;
1309 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1310
1311 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
1312
1313 auto matchedData =
1314 std::dynamic_pointer_cast<DRC_RE_MATCHED_LENGTH_DIFF_PAIR_CONSTRAINT_DATA>( entries[0].constraintData );
1315 BOOST_REQUIRE( matchedData );
1316
1317 BOOST_CHECK( !matchedData->GetWithinDiffPairs() );
1318
1319 // Round-trip should NOT contain within_diff_pairs
1320 entries[0].wasEdited = true;
1321
1322 DRC_RULE_SAVER saver;
1323 wxString savedText = saver.GenerateRulesText( entries, nullptr );
1324
1325 BOOST_CHECK( !savedText.Contains( "within_diff_pairs" ) );
1326}
1327
1328BOOST_AUTO_TEST_CASE( RuleLoaderWithCondition )
1329{
1330 // Test: Load rule with condition
1331 wxString ruleText =
1332 "(version 1)\n"
1333 "(rule \"Conditional Test\"\n"
1334 " (condition \"A.NetClass == 'Power'\")\n"
1335 " (constraint clearance (min 0.3mm)))";
1336
1337 DRC_RULE_LOADER loader;
1338 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1339
1340 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
1341 BOOST_CHECK_EQUAL( entries[0].ruleName, "Conditional Test" );
1342 BOOST_CHECK_EQUAL( entries[0].condition, "A.NetClass == 'Power'" );
1343 BOOST_CHECK_EQUAL( entries[0].constraintData->GetRuleCondition(), "A.NetClass == 'Power'" );
1344}
1345
1346BOOST_AUTO_TEST_CASE( RuleLoaderMultipleRules )
1347{
1348 // Test: Load multiple rules and verify order preservation
1349 wxString ruleText =
1350 "(version 1)\n"
1351 "(rule \"Rule A\"\n"
1352 " (constraint clearance (min 0.2mm)))\n"
1353 "(rule \"Rule B\"\n"
1354 " (constraint track_width (min 0.15mm)))\n"
1355 "(rule \"Rule C\"\n"
1356 " (constraint via_diameter (min 0.5mm))\n"
1357 " (constraint hole_size (min 0.3mm)))";
1358
1359 DRC_RULE_LOADER loader;
1360 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1361
1362 BOOST_REQUIRE_EQUAL( entries.size(), 3 );
1363
1364 BOOST_CHECK_EQUAL( entries[0].ruleName, "Rule A" );
1365 BOOST_CHECK_EQUAL( entries[0].panelType, MINIMUM_CLEARANCE );
1366
1367 BOOST_CHECK_EQUAL( entries[1].ruleName, "Rule B" );
1368 BOOST_CHECK_EQUAL( entries[1].panelType, ROUTING_WIDTH );
1369
1370 BOOST_CHECK_EQUAL( entries[2].ruleName, "Rule C" );
1371 BOOST_CHECK_EQUAL( entries[2].panelType, VIA_STYLE );
1372}
1373
1374BOOST_AUTO_TEST_CASE( RuleLoaderEmptyRule )
1375{
1376 // Test: Rule with no constraints creates custom rule fallback
1377 wxString ruleText =
1378 "(version 1)\n"
1379 "(rule \"Empty Rule\")";
1380
1381 DRC_RULE_LOADER loader;
1382 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1383
1384 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
1385 BOOST_CHECK_EQUAL( entries[0].panelType, CUSTOM_RULE );
1386 BOOST_CHECK_EQUAL( entries[0].ruleName, "Empty Rule" );
1387}
1388
1389BOOST_AUTO_TEST_CASE( RuleLoaderInvalidText )
1390{
1391 // Test: Invalid rule text returns empty vector
1392 wxString ruleText = "not valid rule text at all";
1393
1394 DRC_RULE_LOADER loader;
1395 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1396
1397 BOOST_CHECK_EQUAL( entries.size(), 0 );
1398}
1399
1400BOOST_AUTO_TEST_CASE( RuleLoaderNumericConstraints )
1401{
1402 // Test various numeric constraints map to correct panels
1403 wxString ruleText =
1404 "(version 1)\n"
1405 "(rule \"Edge Clearance\"\n"
1406 " (constraint edge_clearance (min 0.5mm)))\n"
1407 "(rule \"Hole Clearance\"\n"
1408 " (constraint hole_clearance (min 0.3mm)))\n"
1409 "(rule \"Courtyard\"\n"
1410 " (constraint courtyard_clearance (min 0.25mm)))";
1411
1412 DRC_RULE_LOADER loader;
1413 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1414
1415 BOOST_REQUIRE_EQUAL( entries.size(), 3 );
1416
1417 BOOST_CHECK_EQUAL( entries[0].panelType, COPPER_TO_EDGE_CLEARANCE );
1418 BOOST_CHECK_EQUAL( entries[1].panelType, COPPER_TO_HOLE_CLEARANCE );
1419 BOOST_CHECK_EQUAL( entries[2].panelType, COURTYARD_CLEARANCE );
1420}
1421
1422
1423// A ratio is stored scaled by 1000, so the editor has to scale it back. The depth beside it is
1424// a plain count and must come through untouched.
1425BOOST_AUTO_TEST_CASE( RuleLoaderMicroviaRatioRoundTrip )
1426{
1427 wxString ruleText = "(version 1)\n"
1428 "(rule \"Ratio\"\n"
1429 " (constraint microvia_aspect_ratio (max 1.0)))\n"
1430 "(rule \"Depth\"\n"
1431 " (constraint microvia_stack_depth (max 3)))";
1432
1433 DRC_RULE_LOADER loader;
1434 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1435
1436 BOOST_REQUIRE_EQUAL( entries.size(), 2 );
1437 BOOST_CHECK_EQUAL( entries[0].panelType, MICROVIA_ASPECT_RATIO );
1438 BOOST_CHECK_EQUAL( entries[1].panelType, MICROVIA_STACK_DEPTH );
1439
1440 auto ratio = std::dynamic_pointer_cast<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>( entries[0].constraintData );
1441 auto depth = std::dynamic_pointer_cast<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>( entries[1].constraintData );
1442
1443 BOOST_REQUIRE( ratio );
1444 BOOST_REQUIRE( depth );
1445 BOOST_CHECK_CLOSE( ratio->GetNumericInputValue(), 1.0, 0.0001 );
1446 BOOST_CHECK_CLOSE( depth->GetNumericInputValue(), 3.0, 0.0001 );
1447
1448 entries[0].wasEdited = true;
1449 entries[1].wasEdited = true;
1450
1451 DRC_RULE_SAVER saver;
1452 std::vector<DRC_RE_LOADED_PANEL_ENTRY> reloaded =
1453 loader.LoadFromString( saver.GenerateRulesText( entries, nullptr ) );
1454
1455 BOOST_REQUIRE_EQUAL( reloaded.size(), 2 );
1456
1457 auto ratioAgain = std::dynamic_pointer_cast<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>( reloaded[0].constraintData );
1458 auto depthAgain = std::dynamic_pointer_cast<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>( reloaded[1].constraintData );
1459
1460 BOOST_REQUIRE( ratioAgain );
1461 BOOST_REQUIRE( depthAgain );
1462 BOOST_CHECK_CLOSE( ratioAgain->GetNumericInputValue(), 1.0, 0.0001 );
1463 BOOST_CHECK_CLOSE( depthAgain->GetNumericInputValue(), 3.0, 0.0001 );
1464}
1465
1466
1467// ============================================================================
1468// Rule Saver Tests (Phase 3)
1469// ============================================================================
1470
1471BOOST_AUTO_TEST_CASE( RuleSaverBasicGeneration )
1472{
1473 // Test: Basic rule text generation from panel entry
1476 entry.ruleName = "TestClearance";
1477 entry.wasEdited = true;
1478
1479 auto numericData = std::make_shared<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>();
1480 numericData->SetRuleName( "TestClearance" );
1481 numericData->SetConstraintCode( "clearance" );
1482 numericData->SetNumericInputValue( 0.2 );
1483 entry.constraintData = numericData;
1484
1485 DRC_RULE_SAVER saver;
1486 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
1487 wxString result = saver.GenerateRulesText( entries, nullptr );
1488
1489 BOOST_CHECK( result.Contains( "(version 2)" ) );
1490 BOOST_CHECK( result.Contains( "TestClearance" ) );
1491 BOOST_CHECK( result.Contains( "clearance" ) );
1492}
1493
1494BOOST_AUTO_TEST_CASE( RuleSaverRoundTripPreservation )
1495{
1496 // Test: Original text preserved when not edited
1499 entry.ruleName = "Preserved Rule";
1500 entry.originalRuleText = "(rule \"Preserved Rule\"\n\t(constraint clearance (min 0.5mm)))";
1501 entry.wasEdited = false;
1502
1503 auto numericData = std::make_shared<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>();
1504 numericData->SetRuleName( "Preserved Rule" );
1505 numericData->SetConstraintCode( "clearance" );
1506 numericData->SetNumericInputValue( 0.5 );
1507 entry.constraintData = numericData;
1508
1509 DRC_RULE_SAVER saver;
1510 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
1511 wxString result = saver.GenerateRulesText( entries, nullptr );
1512
1513 BOOST_CHECK( result.Contains( entry.originalRuleText ) );
1514}
1515
1516BOOST_AUTO_TEST_CASE( RuleSaverEditedRuleRegenerated )
1517{
1518 // Test: Edited rules regenerate from panel data
1521 entry.ruleName = "EditedRule";
1522 entry.originalRuleText = "(rule \"EditedRule\"\n\t(constraint clearance (min 0.5mm)))";
1523 entry.wasEdited = true;
1524
1525 auto numericData = std::make_shared<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>();
1526 numericData->SetRuleName( "EditedRule" );
1527 numericData->SetConstraintCode( "clearance" );
1528 numericData->SetNumericInputValue( 0.3 );
1529 entry.constraintData = numericData;
1530
1531 DRC_RULE_SAVER saver;
1532 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
1533 wxString result = saver.GenerateRulesText( entries, nullptr );
1534
1535 // Original text should NOT be preserved since wasEdited is true
1536 BOOST_CHECK( !result.Contains( "0.5mm" ) );
1537 BOOST_CHECK( result.Contains( "EditedRule" ) );
1538}
1539
1540BOOST_AUTO_TEST_CASE( RuleSaverViaStyleRule )
1541{
1542 // Test: Via style rule generation
1544 entry.panelType = VIA_STYLE;
1545 entry.ruleName = "ViaTest";
1546
1547 auto viaData = std::make_shared<DRC_RE_VIA_STYLE_CONSTRAINT_DATA>();
1548 viaData->SetRuleName( "ViaTest" );
1549 viaData->SetMinViaDiameter( 0.5 );
1550 viaData->SetMaxViaDiameter( 0.8 );
1551 viaData->SetMinViaHoleSize( 0.2 );
1552 viaData->SetMaxViaHoleSize( 0.4 );
1553 entry.constraintData = viaData;
1554 entry.wasEdited = true;
1555
1556 DRC_RULE_SAVER saver;
1557 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
1558 wxString result = saver.GenerateRulesText( entries, nullptr );
1559
1560 BOOST_CHECK( result.Contains( "ViaTest" ) );
1561 BOOST_CHECK( result.Contains( "via_diameter" ) );
1562 BOOST_CHECK( result.Contains( "hole_size" ) );
1563}
1564
1565BOOST_AUTO_TEST_CASE( RuleSaverMultipleEntries )
1566{
1567 // Test: Multiple entries saved in order
1568 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries;
1569
1572 entry1.ruleName = "RuleA";
1573 auto data1 = std::make_shared<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>();
1574 data1->SetRuleName( "RuleA" );
1575 data1->SetConstraintCode( "clearance" );
1576 data1->SetNumericInputValue( 0.2 );
1577 entry1.constraintData = data1;
1578 entry1.wasEdited = true;
1579 entries.push_back( entry1 );
1580
1582 entry2.panelType = ROUTING_WIDTH;
1583 entry2.ruleName = "RuleB";
1584 auto data2 = std::make_shared<DRC_RE_ROUTING_WIDTH_CONSTRAINT_DATA>();
1585 data2->SetRuleName( "RuleB" );
1586 data2->SetOptWidth( 0.2 );
1587 data2->SetWidthTolerance( 0.1 );
1588 entry2.constraintData = data2;
1589 entry2.wasEdited = true;
1590 entries.push_back( entry2 );
1591
1592 DRC_RULE_SAVER saver;
1593 wxString result = saver.GenerateRulesText( entries, nullptr );
1594
1595 // Both rules should appear
1596 BOOST_CHECK( result.Contains( "RuleA" ) );
1597 BOOST_CHECK( result.Contains( "RuleB" ) );
1598
1599 // Check order: RuleA should appear before RuleB
1600 size_t posA = result.Find( "RuleA" );
1601 size_t posB = result.Find( "RuleB" );
1602 BOOST_CHECK( posA < posB );
1603}
1604
1605BOOST_AUTO_TEST_CASE( RuleSaverWithCondition )
1606{
1607 // Test: Rule with condition
1610 entry.ruleName = "ConditionalRule";
1611 entry.condition = "A.NetClass == 'Power'";
1612
1613 auto numericData = std::make_shared<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>();
1614 numericData->SetRuleName( "ConditionalRule" );
1615 numericData->SetConstraintCode( "clearance" );
1616 numericData->SetRuleCondition( "A.NetClass == 'Power'" );
1617 numericData->SetNumericInputValue( 0.3 );
1618 entry.constraintData = numericData;
1619 entry.wasEdited = true;
1620
1621 DRC_RULE_SAVER saver;
1622 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
1623 wxString result = saver.GenerateRulesText( entries, nullptr );
1624
1625 BOOST_CHECK( result.Contains( "ConditionalRule" ) );
1626 BOOST_CHECK( result.Contains( "condition" ) );
1627 BOOST_CHECK( result.Contains( "Power" ) );
1628}
1629
1630BOOST_AUTO_TEST_CASE( RuleSaverEmptyEntries )
1631{
1632 // Test: Empty entries vector
1633 DRC_RULE_SAVER saver;
1634 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries;
1635 wxString result = saver.GenerateRulesText( entries, nullptr );
1636
1637 BOOST_CHECK( result.Contains( "(version 2)" ) );
1638 // Should have version header but nothing else
1639 BOOST_CHECK_EQUAL( result.Trim(), "(version 2)" );
1640}
1641
1642BOOST_AUTO_TEST_CASE( RuleSaverNullConstraintData )
1643{
1644 // Test: Entry with null constraint data is skipped
1647 entry.ruleName = "Null Data Rule";
1648 entry.constraintData = nullptr;
1649 entry.wasEdited = true;
1650
1651 DRC_RULE_SAVER saver;
1652 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
1653 wxString result = saver.GenerateRulesText( entries, nullptr );
1654
1655 // Should have version header but rule text is skipped
1656 BOOST_CHECK( result.Contains( "(version 2)" ) );
1657 BOOST_CHECK( !result.Contains( "Null Data Rule" ) );
1658}
1659
1660BOOST_AUTO_TEST_CASE( RuleSaverLoadSaveRoundTrip )
1661{
1662 // Test: Full round-trip: Load → mark edited → Save → Load produces equivalent data
1663 wxString originalText =
1664 "(version 1)\n"
1665 "(rule \"RoundTripTest\"\n"
1666 " (constraint clearance (min 0.25mm)))";
1667
1668 DRC_RULE_LOADER loader;
1669 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( originalText );
1670
1671 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
1672 BOOST_CHECK_EQUAL( entries[0].ruleName, "RoundTripTest" );
1673
1674 // Mark as edited so it regenerates from data (loader doesn't preserve exact original text)
1675 entries[0].wasEdited = true;
1676
1677 // Save
1678 DRC_RULE_SAVER saver;
1679 wxString savedText = saver.GenerateRulesText( entries, nullptr );
1680
1681 // Verify saved text is valid by reloading
1682 std::vector<DRC_RE_LOADED_PANEL_ENTRY> reloadedEntries = loader.LoadFromString( savedText );
1683
1684 BOOST_REQUIRE_EQUAL( reloadedEntries.size(), 1 );
1685 BOOST_CHECK_EQUAL( reloadedEntries[0].ruleName, "RoundTripTest" );
1686 BOOST_CHECK_EQUAL( reloadedEntries[0].panelType, entries[0].panelType );
1687}
1688
1689BOOST_AUTO_TEST_CASE( RuleSaverPreservesQuotedNameWithSpaces )
1690{
1691 // Issue 23852: editing a rule named like "Annular width: LEDs" must not rewrite
1692 // the name to Annular_width__LEDs. Names with whitespace or other characters
1693 // that require quoting must be emitted as a quoted string and round-trip intact.
1694 const wxString originalName = wxS( "Annular width: LEDs" );
1695
1698 entry.ruleName = originalName;
1699 entry.wasEdited = true;
1700
1701 auto numericData = std::make_shared<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>();
1702 numericData->SetRuleName( originalName );
1703 numericData->SetConstraintCode( "annular_width" );
1704 numericData->SetNumericInputValue( 0.085 );
1705 entry.constraintData = numericData;
1706
1707 DRC_RULE_SAVER saver;
1708 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
1709 wxString result = saver.GenerateRulesText( entries, nullptr );
1710
1711 // The saver must emit the name in quotes, not mangle it with underscores.
1712 BOOST_CHECK( result.Contains( wxS( "\"Annular width: LEDs\"" ) ) );
1713 BOOST_CHECK( !result.Contains( wxS( "Annular_width__LEDs" ) ) );
1714
1715 // Reload the saved text and verify the rule name survives unchanged.
1716 DRC_RULE_LOADER loader;
1717 std::vector<DRC_RE_LOADED_PANEL_ENTRY> reloaded = loader.LoadFromString( result );
1718
1719 BOOST_REQUIRE_EQUAL( reloaded.size(), 1 );
1720 BOOST_CHECK_EQUAL( reloaded[0].ruleName, originalName );
1721}
1722
1723
1724BOOST_AUTO_TEST_CASE( RuleSaverPreservesBareSymbolNameUnquoted )
1725{
1726 // A bare symbol rule name should still be emitted without quotes.
1729 entry.ruleName = wxS( "My_Bare_Rule-1.0" );
1730 entry.wasEdited = true;
1731
1732 auto numericData = std::make_shared<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>();
1733 numericData->SetRuleName( entry.ruleName );
1734 numericData->SetConstraintCode( "clearance" );
1735 numericData->SetNumericInputValue( 0.2 );
1736 entry.constraintData = numericData;
1737
1738 DRC_RULE_SAVER saver;
1739 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
1740 wxString result = saver.GenerateRulesText( entries, nullptr );
1741
1742 BOOST_CHECK( result.Contains( wxS( "(rule My_Bare_Rule-1.0\n" ) ) );
1743 BOOST_CHECK( !result.Contains( wxS( "\"My_Bare_Rule-1.0\"" ) ) );
1744}
1745
1746
1747BOOST_AUTO_TEST_CASE( RuleSaverQuotesNameStartingWithDigit )
1748{
1749 // Names starting with a digit must be quoted so S-expression parsers treat them
1750 // as a symbol rather than a numeric literal.
1753 entry.ruleName = wxS( "3.3V_Power" );
1754 entry.wasEdited = true;
1755
1756 auto numericData = std::make_shared<DRC_RE_NUMERIC_INPUT_CONSTRAINT_DATA>();
1757 numericData->SetRuleName( entry.ruleName );
1758 numericData->SetConstraintCode( "clearance" );
1759 numericData->SetNumericInputValue( 0.2 );
1760 entry.constraintData = numericData;
1761
1762 DRC_RULE_SAVER saver;
1763 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
1764 wxString result = saver.GenerateRulesText( entries, nullptr );
1765
1766 BOOST_CHECK( result.Contains( wxS( "\"3.3V_Power\"" ) ) );
1767
1768 DRC_RULE_LOADER loader;
1769 std::vector<DRC_RE_LOADED_PANEL_ENTRY> reloaded = loader.LoadFromString( result );
1770 BOOST_REQUIRE_EQUAL( reloaded.size(), 1 );
1771 BOOST_CHECK_EQUAL( reloaded[0].ruleName, "3.3V_Power" );
1772}
1773
1774
1775BOOST_AUTO_TEST_CASE( RuleSaverDiffPairRule )
1776{
1777 // Test: Diff pair rule generation
1780 entry.ruleName = "DiffPairTest";
1781
1782 auto dpData = std::make_shared<DRC_RE_ROUTING_DIFF_PAIR_CONSTRAINT_DATA>();
1783 dpData->SetRuleName( "DiffPairTest" );
1784 dpData->SetOptWidth( 0.25 );
1785 dpData->SetWidthTolerance( 0.05 );
1786 dpData->SetOptGap( 0.15 );
1787 dpData->SetGapTolerance( 0.05 );
1788 dpData->SetMaxUncoupledLength( 5.0 );
1789 entry.constraintData = dpData;
1790 entry.wasEdited = true;
1791
1792 DRC_RULE_SAVER saver;
1793 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
1794 wxString result = saver.GenerateRulesText( entries, nullptr );
1795
1796 BOOST_CHECK( result.Contains( "DiffPairTest" ) );
1797 BOOST_CHECK( result.Contains( "track_width" ) );
1798 BOOST_CHECK( result.Contains( "diff_pair_gap" ) );
1799}
1800
1801
1802// ============================================================================
1803// Integration Tests (Phase 5.2)
1804// ============================================================================
1805
1806BOOST_AUTO_TEST_CASE( IntegrationLoadHoleClearanceRules )
1807{
1808 // Test: Load issue6879.kicad_dru which has hole_clearance and hole constraints
1809 wxString ruleText =
1810 "(version 1)\n"
1811 "(rule \"PTH to Track Clearance\"\n"
1812 " (constraint hole_clearance (min 1.0mm))\n"
1813 " (condition \"A.Type == 'Pad' && A.Pad_Type == 'Through-hole' && B.Type =='Track'\"))\n"
1814 "(rule \"Max Drill Hole Size Mechanical\"\n"
1815 " (constraint hole (max 2.0mm))\n"
1816 " (condition \"A.Type == 'Pad'\"))";
1817
1818 DRC_RULE_LOADER loader;
1819 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1820
1821 BOOST_REQUIRE_GE( entries.size(), 2 );
1822
1823 // First rule should be COPPER_TO_HOLE_CLEARANCE
1824 BOOST_CHECK_EQUAL( entries[0].panelType, COPPER_TO_HOLE_CLEARANCE );
1825 BOOST_CHECK_EQUAL( entries[0].ruleName, "PTH to Track Clearance" );
1826 BOOST_CHECK( !entries[0].condition.IsEmpty() );
1827
1828 // Second rule should be HOLE_SIZE
1829 BOOST_CHECK_EQUAL( entries[1].panelType, HOLE_SIZE );
1830 BOOST_CHECK_EQUAL( entries[1].ruleName, "Max Drill Hole Size Mechanical" );
1831}
1832
1833BOOST_AUTO_TEST_CASE( IntegrationLoadEdgeClearanceWithSeverity )
1834{
1835 // Test: Load severities.kicad_dru which has severity clause
1836 wxString ruleText =
1837 "(version 1)\n"
1838 "(rule board_edge\n"
1839 " (constraint edge_clearance (min 1mm))\n"
1840 " (condition \"A.memberOf('board_edge')\")\n"
1841 " (severity ignore))";
1842
1843 DRC_RULE_LOADER loader;
1844 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1845
1846 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
1847 BOOST_CHECK_EQUAL( entries[0].panelType, COPPER_TO_EDGE_CLEARANCE );
1848 BOOST_CHECK_EQUAL( entries[0].ruleName, "board_edge" );
1849 BOOST_CHECK_EQUAL( entries[0].severity, RPT_SEVERITY_IGNORE );
1850}
1851
1852BOOST_AUTO_TEST_CASE( IntegrationLoadConnectionWidthRules )
1853{
1854 // Test: Load connection_width_rules.kicad_dru
1855 wxString ruleText =
1856 "(version 1)\n"
1857 "(rule high_current_netclass\n"
1858 " (constraint connection_width (min 0.16mm))\n"
1859 " (condition \"A.NetClass == 'High_current'\"))\n"
1860 "(rule high_current_area\n"
1861 " (constraint connection_width (min 0.16mm))\n"
1862 " (condition \"A.insideArea('high_current')\"))";
1863
1864 DRC_RULE_LOADER loader;
1865 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1866
1867 BOOST_REQUIRE_EQUAL( entries.size(), 2 );
1868
1869 // Both should be MINIMUM_CONNECTION_WIDTH
1870 BOOST_CHECK_EQUAL( entries[0].panelType, MINIMUM_CONNECTION_WIDTH );
1871 BOOST_CHECK_EQUAL( entries[1].panelType, MINIMUM_CONNECTION_WIDTH );
1872
1873 // Both should have conditions
1874 BOOST_CHECK( !entries[0].condition.IsEmpty() );
1875 BOOST_CHECK( !entries[1].condition.IsEmpty() );
1876}
1877
1878BOOST_AUTO_TEST_CASE( IntegrationSaveLoadRoundTripMultipleRules )
1879{
1880 // Test: Load multiple rules, save, reload - verify equivalent data
1881 // Use rule names without spaces to avoid sanitization differences
1882 wxString originalText =
1883 "(version 1)\n"
1884 "(rule ClearanceRule\n"
1885 " (constraint clearance (min 0.2mm))\n"
1886 " (condition \"A.NetClass == 'Power'\"))\n"
1887 "(rule TrackWidthRule\n"
1888 " (constraint track_width (min 0.15mm) (opt 0.2mm) (max 0.3mm)))";
1889
1890 DRC_RULE_LOADER loader;
1891 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( originalText );
1892
1893 BOOST_REQUIRE_EQUAL( entries.size(), 2 );
1894
1895 // Mark as edited to force regeneration
1896 for( auto& entry : entries )
1897 entry.wasEdited = true;
1898
1899 // Save
1900 DRC_RULE_SAVER saver;
1901 wxString savedText = saver.GenerateRulesText( entries, nullptr );
1902
1903 // Reload
1904 std::vector<DRC_RE_LOADED_PANEL_ENTRY> reloadedEntries = loader.LoadFromString( savedText );
1905
1906 BOOST_REQUIRE_EQUAL( reloadedEntries.size(), 2 );
1907
1908 // Verify panel types preserved correctly
1909 BOOST_CHECK_EQUAL( reloadedEntries[0].panelType, entries[0].panelType );
1910 BOOST_CHECK_EQUAL( reloadedEntries[1].panelType, entries[1].panelType );
1911
1912 // Verify rule names preserved
1913 BOOST_CHECK_EQUAL( reloadedEntries[0].ruleName, "ClearanceRule" );
1914 BOOST_CHECK_EQUAL( reloadedEntries[1].ruleName, "TrackWidthRule" );
1915}
1916
1917BOOST_AUTO_TEST_CASE( IntegrationSaveLoadViaStyleRoundTrip )
1918{
1919 // Test: Via style rules round-trip correctly
1920 wxString originalText =
1921 "(version 1)\n"
1922 "(rule \"Via Style\"\n"
1923 " (constraint via_diameter (min 0.5mm) (opt 0.6mm) (max 0.8mm))\n"
1924 " (constraint hole_size (min 0.2mm) (opt 0.3mm) (max 0.4mm)))";
1925
1926 DRC_RULE_LOADER loader;
1927 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( originalText );
1928
1929 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
1930 BOOST_CHECK_EQUAL( entries[0].panelType, VIA_STYLE );
1931
1932 auto viaData = std::dynamic_pointer_cast<DRC_RE_VIA_STYLE_CONSTRAINT_DATA>( entries[0].constraintData );
1933 BOOST_REQUIRE( viaData );
1934
1935 double originalMinDia = viaData->GetMinViaDiameter();
1936 double originalMinHole = viaData->GetMinViaHoleSize();
1937
1938 // Mark as edited and save
1939 entries[0].wasEdited = true;
1940 DRC_RULE_SAVER saver;
1941 wxString savedText = saver.GenerateRulesText( entries, nullptr );
1942
1943 // Reload and verify values preserved
1944 std::vector<DRC_RE_LOADED_PANEL_ENTRY> reloadedEntries = loader.LoadFromString( savedText );
1945
1946 BOOST_REQUIRE_EQUAL( reloadedEntries.size(), 1 );
1947 BOOST_CHECK_EQUAL( reloadedEntries[0].panelType, VIA_STYLE );
1948
1949 auto reloadedViaData =
1950 std::dynamic_pointer_cast<DRC_RE_VIA_STYLE_CONSTRAINT_DATA>( reloadedEntries[0].constraintData );
1951 BOOST_REQUIRE( reloadedViaData );
1952
1953 BOOST_CHECK_CLOSE( reloadedViaData->GetMinViaDiameter(), originalMinDia, 0.0001 );
1954 BOOST_CHECK_CLOSE( reloadedViaData->GetMinViaHoleSize(), originalMinHole, 0.0001 );
1955}
1956
1957BOOST_AUTO_TEST_CASE( IntegrationSplitRulePreservesOrder )
1958{
1959 // Test: A rule with multiple constraint types splits and preserves order
1960 wxString ruleText =
1961 "(version 1)\n"
1962 "(rule \"Rule A\"\n"
1963 " (constraint clearance (min 0.2mm)))\n"
1964 "(rule \"Rule B\"\n"
1965 " (constraint via_diameter (min 0.5mm))\n"
1966 " (constraint hole_size (min 0.2mm))\n"
1967 " (constraint track_width (min 0.15mm)))\n"
1968 "(rule \"Rule C\"\n"
1969 " (constraint edge_clearance (min 0.3mm)))";
1970
1971 DRC_RULE_LOADER loader;
1972 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
1973
1974 // Rule B splits into VIA_STYLE and ROUTING_WIDTH
1975 BOOST_REQUIRE_GE( entries.size(), 4 );
1976
1977 // Find entries by rule name
1978 int ruleACount = 0, ruleBCount = 0, ruleCCount = 0;
1979
1980 for( const auto& entry : entries )
1981 {
1982 if( entry.ruleName == "Rule A" )
1983 ruleACount++;
1984 else if( entry.ruleName == "Rule B" )
1985 ruleBCount++;
1986 else if( entry.ruleName == "Rule C" )
1987 ruleCCount++;
1988 }
1989
1990 BOOST_CHECK_EQUAL( ruleACount, 1 );
1991 BOOST_CHECK_GE( ruleBCount, 2 );
1992 BOOST_CHECK_EQUAL( ruleCCount, 1 );
1993
1994 // Verify original order: all Rule A entries before Rule B, Rule B before Rule C
1995 int lastAIndex = -1, firstBIndex = entries.size(), lastBIndex = -1, firstCIndex = entries.size();
1996
1997 for( size_t i = 0; i < entries.size(); i++ )
1998 {
1999 if( entries[i].ruleName == "Rule A" )
2000 lastAIndex = i;
2001 else if( entries[i].ruleName == "Rule B" )
2002 {
2003 if( (int)i < firstBIndex )
2004 firstBIndex = i;
2005
2006 lastBIndex = i;
2007 }
2008 else if( entries[i].ruleName == "Rule C" )
2009 {
2010 if( (int)i < firstCIndex )
2011 firstCIndex = i;
2012 }
2013 }
2014
2015 BOOST_CHECK( lastAIndex < firstBIndex );
2016 BOOST_CHECK( lastBIndex < firstCIndex );
2017}
2018
2019
2020// ============================================================================
2021// Condition Group Panel Tests
2022// ============================================================================
2023
2024BOOST_AUTO_TEST_CASE( ConditionGroupBuildSingleCondition )
2025{
2026 // Test: Single condition builds correctly
2027 // This tests the expression building logic used by DRC_RE_CONDITION_GROUP_PANEL
2028 wxString expr = "A.NetName == 'VCC'";
2029
2030 // When parsed and rebuilt, the result should be equivalent
2031 BOOST_CHECK( !expr.IsEmpty() );
2032 BOOST_CHECK( expr.Contains( "NetName" ) );
2033 BOOST_CHECK( expr.Contains( "VCC" ) );
2034}
2035
2036BOOST_AUTO_TEST_CASE( ConditionGroupTokenizeAndOperator )
2037{
2038 // Test: Tokenizing conditions with AND operator
2039 wxString expr = "A.NetName == 'VCC' && B.NetClass == 'Power'";
2040
2041 // Verify the expression contains expected operators
2042 BOOST_CHECK( expr.Contains( "&&" ) );
2043 BOOST_CHECK( expr.Contains( "NetName" ) );
2044 BOOST_CHECK( expr.Contains( "NetClass" ) );
2045}
2046
2047BOOST_AUTO_TEST_CASE( ConditionGroupTokenizeOrOperator )
2048{
2049 // Test: Tokenizing conditions with OR operator
2050 wxString expr = "A.intersectsArea('underFPGA') || A.intersectsArea('underDDR')";
2051
2052 // Verify the expression contains expected operators
2053 BOOST_CHECK( expr.Contains( "||" ) );
2054 BOOST_CHECK( expr.Contains( "intersectsArea" ) );
2055}
2056
2057BOOST_AUTO_TEST_CASE( ConditionGroupTokenizeAndNotOperator )
2058{
2059 // Test: Tokenizing conditions with AND NOT operator
2060 wxString expr = "A.NetClass == 'Power' && !A.intersectsArea('NoRouting')";
2061
2062 // Verify the expression contains expected operators and negation
2063 BOOST_CHECK( expr.Contains( "&&" ) );
2064 BOOST_CHECK( expr.Contains( "!" ) );
2065}
2066
2067BOOST_AUTO_TEST_CASE( ConditionGroupTokenizeComplexExpression )
2068{
2069 // Test: Complex expression from vme-wren.kicad_dru
2070 wxString expr = "A.intersectsArea('underFPGA') || A.intersectsArea('underDDR')";
2071
2072 // The expression should contain the expected patterns
2073 BOOST_CHECK( expr.Contains( "||" ) );
2074 BOOST_CHECK( expr.Contains( "underFPGA" ) );
2075 BOOST_CHECK( expr.Contains( "underDDR" ) );
2076
2077 // Count number of conditions (2 in this case)
2078 size_t orCount = 0;
2079 size_t pos = 0;
2080
2081 while( ( pos = expr.find( "||", pos ) ) != wxString::npos )
2082 {
2083 orCount++;
2084 pos += 2;
2085 }
2086
2087 BOOST_CHECK_EQUAL( orCount, 1 );
2088}
2089
2090
2091// ============================================================================
2092// DRC Engine Item Filtering Tests
2093// ============================================================================
2094
2095BOOST_AUTO_TEST_CASE( ItemFilterExcludesNetInfoAndGenerator )
2096{
2097 // Test: Verify the item type filter logic matches expected exclusions
2098 // This is a unit test for the switch statement in GetItemsMatchingCondition
2099
2100 std::vector<KICAD_T> excludedTypes = { PCB_NETINFO_T, PCB_GENERATOR_T, PCB_GROUP_T };
2101 std::vector<KICAD_T> includedTypes = { PCB_TRACE_T, PCB_VIA_T, PCB_PAD_T, PCB_FOOTPRINT_T,
2103
2104 for( KICAD_T type : excludedTypes )
2105 {
2106 switch( type )
2107 {
2108 case PCB_NETINFO_T:
2109 case PCB_GENERATOR_T:
2110 case PCB_GROUP_T:
2111 BOOST_CHECK( true ); // These should be excluded (continue in real code)
2112 break;
2113
2114 default:
2115 BOOST_FAIL( "Excluded type not handled in filter" );
2116 break;
2117 }
2118 }
2119
2120 for( KICAD_T type : includedTypes )
2121 {
2122 switch( type )
2123 {
2124 case PCB_NETINFO_T:
2125 case PCB_GENERATOR_T:
2126 case PCB_GROUP_T:
2127 BOOST_FAIL( "Included type incorrectly matched by filter" );
2128 break;
2129
2130 default:
2131 BOOST_CHECK( true ); // These should be included
2132 break;
2133 }
2134 }
2135}
2136
2137BOOST_AUTO_TEST_CASE( RuleSaverSilkToSoldermaskWithCondition )
2138{
2139 // Test: Silk-to-soldermask merges layer condition with user condition
2140 // into a single (condition ...) clause, not two separate ones.
2143 entry.ruleName = "SilkMaskTest";
2144 entry.condition = "A.NetClass == 'Power'";
2145 entry.wasEdited = true;
2146 entry.layerCondition = LSET( { F_SilkS } );
2147
2148 auto data = std::make_shared<DRC_RE_SILK_TO_SOLDERMASK_CLEARANCE_CONSTRAINT_DATA>();
2149 data->SetRuleName( "SilkMaskTest" );
2150 data->SetConstraintCode( "silk_clearance" );
2151 data->SetRuleCondition( "A.NetClass == 'Power'" );
2152 data->SetNumericInputValue( 0.15 );
2153 entry.constraintData = data;
2154
2155 DRC_RULE_SAVER saver;
2156 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
2157 wxString result = saver.GenerateRulesText( entries, nullptr );
2158
2159 // Should have a single merged condition, not two (condition ...) lines
2160 BOOST_CHECK( result.Contains( "L == 'F.Mask'" ) );
2161 BOOST_CHECK( result.Contains( "Power" ) );
2162 BOOST_CHECK( !result.Contains( "(layer" ) );
2163
2164 // Count occurrences of "(condition" — must be exactly one
2165 int condCount = 0;
2166 size_t pos = 0;
2167 while( ( pos = result.find( "(condition", pos ) ) != wxString::npos )
2168 {
2169 condCount++;
2170 pos++;
2171 }
2172 BOOST_CHECK_EQUAL( condCount, 1 );
2173}
2174
2175BOOST_AUTO_TEST_CASE( RuleSaverSilkToSoldermaskNoExtraCondition )
2176{
2177 // Test: Silk-to-soldermask without extra condition still produces
2178 // a condition clause, not a (layer ...) clause.
2181 entry.ruleName = "SilkMaskSimple";
2182 entry.wasEdited = true;
2183 entry.layerCondition = LSET( { B_SilkS } );
2184
2185 auto data = std::make_shared<DRC_RE_SILK_TO_SOLDERMASK_CLEARANCE_CONSTRAINT_DATA>();
2186 data->SetRuleName( "SilkMaskSimple" );
2187 data->SetConstraintCode( "silk_clearance" );
2188 data->SetNumericInputValue( 0.1 );
2189 entry.constraintData = data;
2190
2191 DRC_RULE_SAVER saver;
2192 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
2193 wxString result = saver.GenerateRulesText( entries, nullptr );
2194
2195 BOOST_CHECK( result.Contains( "L == 'B.Mask'" ) );
2196 BOOST_CHECK( result.Contains( "(condition" ) );
2197 BOOST_CHECK( !result.Contains( "(layer" ) );
2198}
2199
2200BOOST_AUTO_TEST_CASE( RuleSaverSilkToSilkBothLayers )
2201{
2202 // Silk-to-silk with both layers generates L == 'F.SilkS' || L == 'B.SilkS'
2205 entry.ruleName = "SilkSilkBoth";
2206 entry.wasEdited = true;
2207 entry.layerCondition = LSET( { F_SilkS, B_SilkS } );
2208
2209 auto data = std::make_shared<DRC_RE_SILK_TO_SILK_CLEARANCE_CONSTRAINT_DATA>();
2210 data->SetRuleName( "SilkSilkBoth" );
2211 data->SetConstraintCode( "silk_clearance" );
2212 data->SetNumericInputValue( 0.2 );
2213 entry.constraintData = data;
2214
2215 DRC_RULE_SAVER saver;
2216 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
2217 wxString result = saver.GenerateRulesText( entries, nullptr );
2218
2219 BOOST_CHECK( result.Contains( "L == 'F.SilkS' || L == 'B.SilkS'" ) );
2220 BOOST_CHECK( result.Contains( "(condition" ) );
2221 BOOST_CHECK( !result.Contains( "(layer" ) );
2222}
2223
2224BOOST_AUTO_TEST_CASE( RuleSaverSilkToSilkFrontOnly )
2225{
2226 // Silk-to-silk front only generates L == 'F.SilkS'
2229 entry.ruleName = "SilkSilkFront";
2230 entry.wasEdited = true;
2231 entry.layerCondition = LSET( { F_SilkS } );
2232
2233 auto data = std::make_shared<DRC_RE_SILK_TO_SILK_CLEARANCE_CONSTRAINT_DATA>();
2234 data->SetRuleName( "SilkSilkFront" );
2235 data->SetConstraintCode( "silk_clearance" );
2236 data->SetNumericInputValue( 0.2 );
2237 entry.constraintData = data;
2238
2239 DRC_RULE_SAVER saver;
2240 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
2241 wxString result = saver.GenerateRulesText( entries, nullptr );
2242
2243 BOOST_CHECK( result.Contains( "L == 'F.SilkS'" ) );
2244 BOOST_CHECK( !result.Contains( "B.SilkS" ) );
2245 BOOST_CHECK( result.Contains( "(condition" ) );
2246 BOOST_CHECK( !result.Contains( "(layer" ) );
2247}
2248
2249BOOST_AUTO_TEST_CASE( RuleSaverSilkToSilkWithCondition )
2250{
2251 // Silk-to-silk merges layer condition with user condition
2254 entry.ruleName = "SilkSilkCond";
2255 entry.condition = "A.NetClass == 'Signal'";
2256 entry.wasEdited = true;
2257 entry.layerCondition = LSET( { F_SilkS } );
2258
2259 auto data = std::make_shared<DRC_RE_SILK_TO_SILK_CLEARANCE_CONSTRAINT_DATA>();
2260 data->SetRuleName( "SilkSilkCond" );
2261 data->SetConstraintCode( "silk_clearance" );
2262 data->SetRuleCondition( "A.NetClass == 'Signal'" );
2263 data->SetNumericInputValue( 0.2 );
2264 entry.constraintData = data;
2265
2266 DRC_RULE_SAVER saver;
2267 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = { entry };
2268 wxString result = saver.GenerateRulesText( entries, nullptr );
2269
2270 BOOST_CHECK( result.Contains( "L == 'F.SilkS'" ) );
2271 BOOST_CHECK( result.Contains( "Signal" ) );
2272
2273 // Single merged condition
2274 int condCount = 0;
2275 size_t pos = 0;
2276 while( ( pos = result.find( "(condition", pos ) ) != wxString::npos )
2277 {
2278 condCount++;
2279 pos++;
2280 }
2281 BOOST_CHECK_EQUAL( condCount, 1 );
2282}
2283
2284BOOST_AUTO_TEST_CASE( ValidateViasUnderSmdValid )
2285{
2286 // Valid: at least one via type is selected
2287 DRC_RE_VIAS_UNDER_SMD_CONSTRAINT_DATA data1( 0, 0, "OnlyMicro", false, true, false, false );
2288 VALIDATION_RESULT result1 = data1.Validate();
2289 BOOST_CHECK( result1.isValid );
2290 BOOST_CHECK( result1.errors.empty() );
2291
2292 // Valid: all selected
2293 DRC_RE_VIAS_UNDER_SMD_CONSTRAINT_DATA data2( 0, 0, "AllVias", true, true, true, true );
2294 VALIDATION_RESULT result2 = data2.Validate();
2295 BOOST_CHECK( result2.isValid );
2296 BOOST_CHECK( result2.errors.empty() );
2297}
2298
2299BOOST_AUTO_TEST_CASE( ValidateViasUnderSmdInvalid )
2300{
2301 // Invalid: no via type selected
2302 DRC_RE_VIAS_UNDER_SMD_CONSTRAINT_DATA data( 0, 0, "NoneSelected", false, false, false, false );
2304 BOOST_CHECK( !result.isValid );
2305 BOOST_CHECK( !result.errors.empty() );
2306
2307 bool foundError = false;
2308 for( const auto& error : result.errors )
2309 {
2310 if( error.find( "At least one via type must be selected" ) != std::string::npos )
2311 foundError = true;
2312 }
2313 BOOST_CHECK( foundError );
2314}
2315
2316BOOST_AUTO_TEST_CASE( ViasUnderSmdClauseGeneration )
2317{
2319 ctx.ruleName = "TestRule";
2320 ctx.constraintCode = "disallow_via";
2321
2322 // All four selected - shorthand "via" used
2323 DRC_RE_VIAS_UNDER_SMD_CONSTRAINT_DATA allData( 0, 0, "AllRule", true, true, true, true );
2324 auto clauses = allData.GetConstraintClauses( ctx );
2325 BOOST_REQUIRE_EQUAL( clauses.size(), 1 );
2326 BOOST_CHECK( clauses[0].Contains( "disallow via" ) );
2327 BOOST_CHECK( !clauses[0].Contains( "through_via" ) );
2328
2329 // Only micro_via selected - specific token
2330 DRC_RE_VIAS_UNDER_SMD_CONSTRAINT_DATA microOnly( 0, 0, "MicroRule", false, true, false, false );
2331 clauses = microOnly.GetConstraintClauses( ctx );
2332 BOOST_REQUIRE_EQUAL( clauses.size(), 1 );
2333 BOOST_CHECK( clauses[0].Contains( "micro_via" ) );
2334 BOOST_CHECK( !clauses[0].Contains( "through_via" ) );
2335 BOOST_CHECK( !clauses[0].Contains( "blind_via" ) );
2336 BOOST_CHECK( !clauses[0].Contains( "buried_via" ) );
2337
2338 // Through + blind selected - both tokens, no shorthand
2339 DRC_RE_VIAS_UNDER_SMD_CONSTRAINT_DATA twoTypes( 0, 0, "TwoRule", true, false, true, false );
2340 clauses = twoTypes.GetConstraintClauses( ctx );
2341 BOOST_REQUIRE_EQUAL( clauses.size(), 1 );
2342 BOOST_CHECK( clauses[0].Contains( "through_via" ) );
2343 BOOST_CHECK( clauses[0].Contains( "blind_via" ) );
2344 BOOST_CHECK( !clauses[0].Contains( "micro_via" ) );
2345 BOOST_CHECK( !clauses[0].Contains( "buried_via" ) );
2346
2347 // None selected - empty
2348 DRC_RE_VIAS_UNDER_SMD_CONSTRAINT_DATA noneData( 0, 0, "NoneRule", false, false, false, false );
2349 clauses = noneData.GetConstraintClauses( ctx );
2350 BOOST_CHECK( clauses.empty() );
2351}
2352
2353BOOST_AUTO_TEST_CASE( IntegrationViasUnderSmdRoundTrip )
2354{
2355 // Load a rule with only micro_via disallowed
2356 wxString originalText = "(version 1)\n"
2357 "(rule \"ViasUnderSmdTest\"\n"
2358 " (constraint disallow micro_via)\n"
2359 " (condition \"A.Pad_Type == 'SMD' && A.insideArea('BGA*')\"))";
2360
2361 DRC_RULE_LOADER loader;
2362 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( originalText );
2363
2364 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
2365 BOOST_CHECK_EQUAL( entries[0].panelType, VIAS_UNDER_SMD );
2366
2367 auto viaData = std::dynamic_pointer_cast<DRC_RE_VIAS_UNDER_SMD_CONSTRAINT_DATA>( entries[0].constraintData );
2368 BOOST_REQUIRE( viaData );
2369
2370 // Only micro_via should be checked
2371 BOOST_CHECK( !viaData->GetDisallowThroughVias() );
2372 BOOST_CHECK( viaData->GetDisallowMicroVias() );
2373 BOOST_CHECK( !viaData->GetDisallowBlindVias() );
2374 BOOST_CHECK( !viaData->GetDisallowBuriedVias() );
2375
2376 // Mark as edited and save
2377 entries[0].wasEdited = true;
2378 DRC_RULE_SAVER saver;
2379 wxString savedText = saver.GenerateRulesText( entries, nullptr );
2380
2381 // Verify saved text preserves specific via type (not blanket "via")
2382 BOOST_CHECK( savedText.Contains( "micro_via" ) );
2383 BOOST_CHECK( !savedText.Contains( "through_via" ) );
2384 BOOST_CHECK( !savedText.Contains( "blind_via" ) );
2385 BOOST_CHECK( !savedText.Contains( "buried_via" ) );
2386
2387 // Reload and verify roundtrip
2388 std::vector<DRC_RE_LOADED_PANEL_ENTRY> reloadedEntries = loader.LoadFromString( savedText );
2389 BOOST_REQUIRE_EQUAL( reloadedEntries.size(), 1 );
2390
2391 auto reloadedData =
2392 std::dynamic_pointer_cast<DRC_RE_VIAS_UNDER_SMD_CONSTRAINT_DATA>( reloadedEntries[0].constraintData );
2393 BOOST_REQUIRE( reloadedData );
2394
2395 BOOST_CHECK( !reloadedData->GetDisallowThroughVias() );
2396 BOOST_CHECK( reloadedData->GetDisallowMicroVias() );
2397 BOOST_CHECK( !reloadedData->GetDisallowBlindVias() );
2398 BOOST_CHECK( !reloadedData->GetDisallowBuriedVias() );
2399
2400 // Test blanket "via" sets all four flags
2401 wxString blanketText = "(version 1)\n"
2402 "(rule \"BlanketVia\"\n"
2403 " (constraint disallow via)\n"
2404 " (condition \"A.Pad_Type == 'SMD' && A.insideArea('BGA*')\"))";
2405
2406 entries = loader.LoadFromString( blanketText );
2407 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
2408
2409 auto blanketData = std::dynamic_pointer_cast<DRC_RE_VIAS_UNDER_SMD_CONSTRAINT_DATA>( entries[0].constraintData );
2410 BOOST_REQUIRE( blanketData );
2411
2412 BOOST_CHECK( blanketData->GetDisallowThroughVias() );
2413 BOOST_CHECK( blanketData->GetDisallowMicroVias() );
2414 BOOST_CHECK( blanketData->GetDisallowBlindVias() );
2415 BOOST_CHECK( blanketData->GetDisallowBuriedVias() );
2416
2417 // Test multiple specific types roundtrip
2418 wxString multiText = "(version 1)\n"
2419 "(rule \"TwoVias\"\n"
2420 " (constraint disallow through_via blind_via)\n"
2421 " (condition \"A.Pad_Type == 'SMD' && A.insideArea('BGA*')\"))";
2422
2423 entries = loader.LoadFromString( multiText );
2424 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
2425
2426 auto multiData = std::dynamic_pointer_cast<DRC_RE_VIAS_UNDER_SMD_CONSTRAINT_DATA>( entries[0].constraintData );
2427 BOOST_REQUIRE( multiData );
2428
2429 BOOST_CHECK( multiData->GetDisallowThroughVias() );
2430 BOOST_CHECK( !multiData->GetDisallowMicroVias() );
2431 BOOST_CHECK( multiData->GetDisallowBlindVias() );
2432 BOOST_CHECK( !multiData->GetDisallowBuriedVias() );
2433
2434 entries[0].wasEdited = true;
2435 savedText = saver.GenerateRulesText( entries, nullptr );
2436
2437 BOOST_CHECK( savedText.Contains( "through_via" ) );
2438 BOOST_CHECK( savedText.Contains( "blind_via" ) );
2439 BOOST_CHECK( !savedText.Contains( "micro_via" ) );
2440 BOOST_CHECK( !savedText.Contains( "buried_via" ) );
2441}
2442
2443BOOST_AUTO_TEST_CASE( RuleLoaderNonStandardOrientationFallsBackToCustom )
2444{
2445 // Quoted name
2446 wxString ruleText = "(version 1)\n"
2447 "(rule \"Angled\"\n"
2448 " (constraint assertion \"A.Orientation == 46 deg\")\n"
2449 ")";
2450
2451 DRC_RULE_LOADER loader;
2452 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
2453
2454 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
2455 BOOST_CHECK_EQUAL( entries[0].panelType, CUSTOM_RULE );
2456
2457 auto customData = std::dynamic_pointer_cast<DRC_RE_CUSTOM_RULE_CONSTRAINT_DATA>( entries[0].constraintData );
2458 BOOST_REQUIRE( customData );
2459 BOOST_CHECK( customData->GetRuleText().Contains( wxS( "46 deg" ) ) );
2460 BOOST_CHECK( !customData->GetRuleText().Contains( wxS( "Angled" ) ) );
2461
2462 // Unquoted name
2463 wxString ruleTextUnquoted = "(version 1)\n"
2464 "(rule Angled\n"
2465 " (constraint assertion \"A.Orientation == 46 deg\")\n"
2466 ")";
2467
2468 entries = loader.LoadFromString( ruleTextUnquoted );
2469
2470 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
2471 BOOST_CHECK_EQUAL( entries[0].panelType, CUSTOM_RULE );
2472
2473 auto customDataUnquoted =
2474 std::dynamic_pointer_cast<DRC_RE_CUSTOM_RULE_CONSTRAINT_DATA>( entries[0].constraintData );
2475 BOOST_REQUIRE( customDataUnquoted );
2476 BOOST_CHECK( customDataUnquoted->GetRuleText().Contains( wxS( "46 deg" ) ) );
2477 BOOST_CHECK( !customDataUnquoted->GetRuleText().Contains( wxS( "Angled" ) ) );
2478}
2479
2480BOOST_AUTO_TEST_CASE( RuleLoaderStandardOrientationLoadsStructured )
2481{
2482 wxString ruleText = "(version 1)\n"
2483 "(rule \"Standard\"\n"
2484 " (constraint assertion \"A.Orientation == 0 deg || A.Orientation == 90 deg\")\n"
2485 ")";
2486
2487 DRC_RULE_LOADER loader;
2488 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
2489
2490 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
2491 BOOST_CHECK_EQUAL( entries[0].panelType, ALLOWED_ORIENTATION );
2492
2493 auto orientData =
2494 std::dynamic_pointer_cast<DRC_RE_ALLOWED_ORIENTATION_CONSTRAINT_DATA>( entries[0].constraintData );
2495 BOOST_REQUIRE( orientData );
2496 BOOST_CHECK( orientData->GetIsZeroDegreesAllowed() );
2497 BOOST_CHECK( orientData->GetIsNinetyDegreesAllowed() );
2498 BOOST_CHECK( !orientData->GetIsOneEightyDegreesAllowed() );
2499 BOOST_CHECK( !orientData->GetIsAllDegreesAllowed() );
2500
2501 // Single-line variant
2502 wxString singleLine = "(version 1) (rule \"SingleLine\" (constraint assertion \"A.Orientation == 0 deg || "
2503 "A.Orientation == 90 deg\"))";
2504
2505 entries = loader.LoadFromString( singleLine );
2506
2507 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
2508 BOOST_CHECK_EQUAL( entries[0].panelType, ALLOWED_ORIENTATION );
2509
2510 auto singleLineData =
2511 std::dynamic_pointer_cast<DRC_RE_ALLOWED_ORIENTATION_CONSTRAINT_DATA>( entries[0].constraintData );
2512 BOOST_REQUIRE( singleLineData );
2513 BOOST_CHECK( singleLineData->GetIsZeroDegreesAllowed() );
2514 BOOST_CHECK( singleLineData->GetIsNinetyDegreesAllowed() );
2515 BOOST_CHECK( !singleLineData->GetIsOneEightyDegreesAllowed() );
2516 BOOST_CHECK( !singleLineData->GetIsAllDegreesAllowed() );
2517}
2518
2519BOOST_AUTO_TEST_CASE( RuleLoaderAllowAllOrientationLoadsStructured )
2520{
2521 wxString ruleText = "(version 1)\n"
2522 "(rule \"AllAngles\"\n"
2523 " (constraint assertion \"A.Orientation == 0 deg || A.Orientation == 90 deg || "
2524 "A.Orientation == 180 deg || A.Orientation == 270 deg\")\n"
2525 ")";
2526
2527 DRC_RULE_LOADER loader;
2528 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
2529
2530 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
2531 BOOST_CHECK_EQUAL( entries[0].panelType, ALLOWED_ORIENTATION );
2532
2533 auto orientData =
2534 std::dynamic_pointer_cast<DRC_RE_ALLOWED_ORIENTATION_CONSTRAINT_DATA>( entries[0].constraintData );
2535 BOOST_REQUIRE( orientData );
2536 BOOST_CHECK( orientData->GetIsAllDegreesAllowed() );
2537 BOOST_CHECK( orientData->GetIsZeroDegreesAllowed() );
2538 BOOST_CHECK( orientData->GetIsNinetyDegreesAllowed() );
2539 BOOST_CHECK( orientData->GetIsOneEightyDegreesAllowed() );
2540 BOOST_CHECK( orientData->GetIsTwoSeventyDegreesAllowed() );
2541}
2542
2543BOOST_AUTO_TEST_CASE( RuleLoaderPermittedLayersInnerLayerFallsBackToCustom )
2544{
2545 wxString ruleText = "(version 1)\n"
2546 "(rule \"InnerLayers\"\n"
2547 " (constraint assertion \"A.Layer == 'F.Cu' || A.Layer == 'In1.Cu'\")\n"
2548 ")";
2549
2550 DRC_RULE_LOADER loader;
2551 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
2552
2553 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
2554 BOOST_CHECK_EQUAL( entries[0].panelType, CUSTOM_RULE );
2555
2556 auto customData = std::dynamic_pointer_cast<DRC_RE_CUSTOM_RULE_CONSTRAINT_DATA>( entries[0].constraintData );
2557 BOOST_REQUIRE( customData );
2558 BOOST_CHECK( customData->GetRuleText().Contains( wxS( "In1.Cu" ) ) );
2559}
2560
2561BOOST_AUTO_TEST_CASE( RuleLoaderPermittedLayersStandardLoadsStructured )
2562{
2563 wxString ruleText = "(version 1)\n"
2564 "(rule \"StandardLayers\"\n"
2565 " (constraint assertion \"A.Layer == 'F.Cu' || A.Layer == 'B.Cu'\")\n"
2566 ")";
2567
2568 DRC_RULE_LOADER loader;
2569 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
2570
2571 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
2572 BOOST_CHECK_EQUAL( entries[0].panelType, PERMITTED_LAYERS );
2573
2574 auto layerData = std::dynamic_pointer_cast<DRC_RE_PERMITTED_LAYERS_CONSTRAINT_DATA>( entries[0].constraintData );
2575 BOOST_REQUIRE( layerData );
2576 BOOST_CHECK( layerData->GetTopLayerEnabled() );
2577 BOOST_CHECK( layerData->GetBottomLayerEnabled() );
2578}
2579
2580
2581BOOST_AUTO_TEST_CASE( CustomPropertyHelperMatches )
2582{
2583 BOARD board;
2584
2585 FOOTPRINT* vendorFp = new FOOTPRINT( &board );
2586 vendorFp->SetReference( wxT( "U1" ) );
2587 vendorFp->SetCustomProperty( wxT( "Vendor" ), wxT( "ACME" ) );
2588 board.Add( vendorFp );
2589
2590 FOOTPRINT* otherFp = new FOOTPRINT( &board );
2591 otherFp->SetReference( wxT( "U2" ) );
2592 board.Add( otherFp );
2593
2594 auto rule = std::make_shared<DRC_RULE>( wxT( "Custom property" ) );
2595 rule->m_Condition = new DRC_RULE_CONDITION( wxT( "A.customProperty('Vendor') == 'ACME'" ) );
2596 BOOST_REQUIRE( rule->m_Condition->Compile( nullptr ) );
2597
2599 rule->AddConstraint( constraint );
2600
2601 DRC_ENGINE engine( &board, &board.GetDesignSettings() );
2602
2603 std::vector<BOARD_ITEM*> matches = engine.GetItemsMatchingRule( rule, nullptr );
2604
2605 BOOST_REQUIRE_EQUAL( matches.size(), 1u );
2606 BOOST_CHECK( matches.front() == vendorFp );
2607}
2608
2609
2610BOOST_AUTO_TEST_CASE( HasCustomPropertyHelperMatches )
2611{
2612 BOARD board;
2613
2614 FOOTPRINT* vendorFp = new FOOTPRINT( &board );
2615 vendorFp->SetReference( wxT( "U1" ) );
2616 vendorFp->SetCustomProperty( wxT( "Vendor" ), wxT( "ACME" ) );
2617 board.Add( vendorFp );
2618
2619 FOOTPRINT* otherFp = new FOOTPRINT( &board );
2620 otherFp->SetReference( wxT( "U2" ) );
2621 board.Add( otherFp );
2622
2623 auto rule = std::make_shared<DRC_RULE>( wxT( "Has custom property" ) );
2624 rule->m_Condition = new DRC_RULE_CONDITION( wxT( "A.hasCustomProperty('Vendor')" ) );
2625 BOOST_REQUIRE( rule->m_Condition->Compile( nullptr ) );
2626
2628 rule->AddConstraint( constraint );
2629
2630 DRC_ENGINE engine( &board, &board.GetDesignSettings() );
2631
2632 std::vector<BOARD_ITEM*> matches = engine.GetItemsMatchingRule( rule, nullptr );
2633
2634 BOOST_REQUIRE_EQUAL( matches.size(), 1u );
2635 BOOST_CHECK( matches.front() == vendorFp );
2636}
2637
2638
2639// A time domain length rule loads into the length panel with its values in ps.
2640BOOST_AUTO_TEST_CASE( RuleLoaderTimeDomainLengthLoadsStructured )
2641{
2642 wxString ruleText = "(version 1)\n"
2643 "(rule \"clk_delay\"\n"
2644 " (constraint length (min 210ps) (max 230ps))\n"
2645 ")";
2646
2647 DRC_RULE_LOADER loader;
2648 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
2649
2650 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
2651 BOOST_CHECK_EQUAL( entries[0].panelType, ABSOLUTE_LENGTH );
2652
2653 auto data = std::dynamic_pointer_cast<DRC_RE_ABSOLUTE_LENGTH_TWO_CONSTRAINT_DATA>( entries[0].constraintData );
2654 BOOST_REQUIRE( data );
2655 BOOST_CHECK( data->IsTimeDomain() );
2656 BOOST_CHECK_CLOSE( data->GetOptimumLength(), 220.0, 0.0001 );
2657 BOOST_CHECK_CLOSE( data->GetTolerance(), 10.0, 0.0001 );
2658}
2659
2660
2661BOOST_AUTO_TEST_CASE( RoundTripTimeDomainLength )
2662{
2663 DRC_RE_ABSOLUTE_LENGTH_TWO_CONSTRAINT_DATA original( 0, 0, 220.0, 10.0, "clk_delay" );
2664 original.SetConstraintCode( "length" );
2665 original.SetTimeDomain( true );
2666
2668 ctx.ruleName = original.GetRuleName();
2669 ctx.constraintCode = original.GetConstraintCode();
2670
2671 wxString ruleText = original.GenerateRule( ctx );
2672
2673 BOOST_CHECK( ruleText.Contains( wxS( "210ps" ) ) );
2674 BOOST_CHECK( ruleText.Contains( wxS( "220ps" ) ) );
2675 BOOST_CHECK( ruleText.Contains( wxS( "230ps" ) ) );
2676
2677 DRC_RULE_LOADER loader;
2678 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
2679
2680 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
2681
2682 auto parsed = std::dynamic_pointer_cast<DRC_RE_ABSOLUTE_LENGTH_TWO_CONSTRAINT_DATA>( entries[0].constraintData );
2683 BOOST_REQUIRE( parsed );
2684 BOOST_CHECK( parsed->IsTimeDomain() );
2685 BOOST_CHECK_CLOSE( parsed->GetOptimumLength(), 220.0, 0.0001 );
2686 BOOST_CHECK_CLOSE( parsed->GetTolerance(), 10.0, 0.0001 );
2687}
2688
2689
2690// A rule with no optimum must keep its min and max through an open and save cycle.
2691BOOST_AUTO_TEST_CASE( RoundTripLengthWithoutOpt )
2692{
2693 wxString ruleText = "(version 1)\n"
2694 "(rule \"len_window\"\n"
2695 " (constraint length (min 30mm) (max 50mm))\n"
2696 ")";
2697
2698 DRC_RULE_LOADER loader;
2699 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
2700
2701 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
2702 BOOST_CHECK_EQUAL( entries[0].panelType, ABSOLUTE_LENGTH );
2703
2704 auto data = std::dynamic_pointer_cast<DRC_RE_ABSOLUTE_LENGTH_TWO_CONSTRAINT_DATA>( entries[0].constraintData );
2705 BOOST_REQUIRE( data );
2706 BOOST_CHECK_CLOSE( data->GetOptimumLength(), 40.0, 0.0001 );
2707 BOOST_CHECK_CLOSE( data->GetTolerance(), 10.0, 0.0001 );
2708
2710 ctx.ruleName = entries[0].ruleName;
2711 ctx.constraintCode = data->GetConstraintCode();
2712
2713 wxString saved = data->GenerateRule( ctx );
2714
2715 BOOST_CHECK( saved.Contains( wxS( "30mm" ) ) );
2716 BOOST_CHECK( saved.Contains( wxS( "40mm" ) ) );
2717 BOOST_CHECK( saved.Contains( wxS( "50mm" ) ) );
2718}
2719
2720
2721// A commented out constraint must not appear in the body as active rule text.
2722BOOST_AUTO_TEST_CASE( RuleBodyExcludesCommentLines )
2723{
2724 wxString ruleText = "(rule \"length_DDR_Byte0\"\n"
2725 "\t# byte lane 0\n"
2726 "\t# (constraint length (min 29.5mm) (max 30.5mm) (opt 30mm))\n"
2727 "(constraint length (min 210ps) (max 230ps) (opt 223ps))\n"
2728 "\t# tightened after bring-up\n"
2729 "\t(condition \"A.NetClass == 'DDR4_BYTE0'\"))";
2730
2731 wxString body = DRC_RULE_LOADER::ExtractRuleBody( ruleText );
2732
2733 BOOST_CHECK( body.StartsWith( wxS( "(constraint length (min 210ps)" ) ) );
2734 BOOST_CHECK( !body.Contains( wxS( "29.5mm" ) ) );
2735 BOOST_CHECK( !body.Contains( wxS( "byte lane" ) ) );
2736 BOOST_CHECK( !body.Contains( wxS( "bring-up" ) ) );
2737 BOOST_CHECK( body.Contains( wxS( "condition" ) ) );
2738
2739 wxString comment = DRC_RULE_LOADER::ExtractRuleComment( ruleText );
2740
2741 BOOST_CHECK( comment.Contains( wxS( "byte lane 0" ) ) );
2742 BOOST_CHECK( comment.Contains( wxS( "29.5mm" ) ) );
2743 BOOST_CHECK( comment.Contains( wxS( "bring-up" ) ) );
2744}
2745
2746
2747// The matched length pair panel still cannot hold time, so a ps skew stays as text.
2748BOOST_AUTO_TEST_CASE( RuleLoaderTimeDomainSkewStillFallsBackToCustom )
2749{
2750 wxString ruleText = "(version 1)\n"
2751 "(rule \"pair_ps\"\n"
2752 " (constraint length (min 10mm) (opt 11mm) (max 12mm))\n"
2753 " (constraint skew (max 5ps))\n"
2754 ")";
2755
2756 DRC_RULE_LOADER loader;
2757 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
2758
2759 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
2760 BOOST_CHECK_EQUAL( entries[0].panelType, CUSTOM_RULE );
2761
2762 auto customData = std::dynamic_pointer_cast<DRC_RE_CUSTOM_RULE_CONSTRAINT_DATA>( entries[0].constraintData );
2763 BOOST_REQUIRE( customData );
2764 BOOST_CHECK( customData->GetRuleText().Contains( wxS( "5ps" ) ) );
2765}
2766
2767
2768BOOST_AUTO_TEST_CASE( RuleLoaderSpatialLengthStaysStructured )
2769{
2770 wxString ruleText = "(version 1)\n"
2771 "(rule \"len_mm\"\n"
2772 " (constraint length (min 30mm) (opt 40mm) (max 50mm))\n"
2773 ")";
2774
2775 DRC_RULE_LOADER loader;
2776 std::vector<DRC_RE_LOADED_PANEL_ENTRY> entries = loader.LoadFromString( ruleText );
2777
2778 BOOST_REQUIRE_EQUAL( entries.size(), 1 );
2779 BOOST_CHECK_EQUAL( entries[0].panelType, ABSOLUTE_LENGTH );
2780}
2781
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:409
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Removes an item from the container.
Definition board.cpp:1497
BOARD_DESIGN_SETTINGS & GetDesignSettings() const
Definition board.cpp:1299
MINOPTMAX< int > & Value()
Definition drc_rule.h:201
Design Rule Checker object that performs all the DRC tests.
Definition drc_engine.h:129
std::vector< BOARD_ITEM * > GetItemsMatchingRule(const std::shared_ptr< DRC_RULE > &aRule, REPORTER *aReporter=nullptr)
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.
wxString GenerateRule(const RULE_GENERATION_CONTEXT &aContext) override
VALIDATION_RESULT Validate() const override
Validates the constraint data.
void SetConstraintCode(const wxString &aCode)
void SetRuleCondition(const 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.
std::vector< wxString > GetConstraintClauses(const RULE_GENERATION_CONTEXT &aContext) const override
Returns just the constraint clauses without the rule wrapper.
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.
static wxString ExtractRuleComment(const wxString &aOriginalText)
Extract comment lines from a rule.
static wxString ExtractRuleBody(const wxString &aOriginalText)
Extract the body of a rule from its original text, stripping the (rule "name" ...) wrapper.
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:68
void SetCustomProperty(const wxString &aKey, const wxString &aValue)
Definition eda_item.h:255
void SetReference(const wxString &aReference)
Definition footprint.h:907
LSET is a set of PCB_LAYER_IDs.
Definition lset.h:37
void SetMin(T v)
Definition minoptmax.h:38
void SetOpt(T v)
Definition minoptmax.h:40
void SetMax(T v)
Definition minoptmax.h:39
wxString GetRuleName()
Get the name of the rule.
@ COURTYARD_CLEARANCE_CONSTRAINT
Definition drc_rule.h:57
@ VIA_DIAMETER_CONSTRAINT
Definition drc_rule.h:72
@ DIFF_PAIR_GAP_CONSTRAINT
Definition drc_rule.h:78
@ TRACK_WIDTH_CONSTRAINT
Definition drc_rule.h:61
@ SILK_CLEARANCE_CONSTRAINT
Definition drc_rule.h:58
@ EDGE_CLEARANCE_CONSTRAINT
Definition drc_rule.h:55
@ TEXT_THICKNESS_CONSTRAINT
Definition drc_rule.h:60
@ LENGTH_CONSTRAINT
Definition drc_rule.h:73
@ CLEARANCE_CONSTRAINT
Definition drc_rule.h:51
@ MAX_UNCOUPLED_CONSTRAINT
Definition drc_rule.h:79
@ SKEW_CONSTRAINT
Definition drc_rule.h:77
@ HOLE_CLEARANCE_CONSTRAINT
Definition drc_rule.h:53
@ HOLE_SIZE_CONSTRAINT
Definition drc_rule.h:56
@ TEXT_HEIGHT_CONSTRAINT
Definition drc_rule.h:59
@ HOLE_TO_HOLE_CONSTRAINT
Definition drc_rule.h:54
@ ALLOWED_ORIENTATION
@ SILK_TO_SILK_CLEARANCE
@ ROUTING_DIFF_PAIR
@ SILK_TO_SOLDERMASK_CLEARANCE
@ COURTYARD_CLEARANCE
@ MINIMUM_CONNECTION_WIDTH
@ ABSOLUTE_LENGTH
@ MINIMUM_CLEARANCE
@ PERMITTED_LAYERS
@ MICROVIA_STACK_DEPTH
@ MINIMUM_TEXT_HEIGHT_AND_THICKNESS
@ COPPER_TO_HOLE_CLEARANCE
@ HOLE_TO_HOLE_DISTANCE
@ ROUTING_WIDTH
@ MATCHED_LENGTH_DIFF_PAIR
@ COPPER_TO_EDGE_CLEARANCE
@ MICROVIA_ASPECT_RATIO
@ MINIMUM_VIA_DIAMETER
@ F_SilkS
Definition layer_ids.h:96
@ B_SilkS
Definition layer_ids.h:97
@ 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
LSET layerCondition
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:70
@ PCB_SHAPE_T
class PCB_SHAPE, a segment not on copper layers
Definition typeinfo.h:80
@ PCB_GENERATOR_T
class PCB_GENERATOR, generator on a layer
Definition typeinfo.h:83
@ PCB_VIA_T
class PCB_VIA, a via (like a track segment on a copper layer)
Definition typeinfo.h:89
@ PCB_GROUP_T
class PCB_GROUP, a set of BOARD_ITEMs
Definition typeinfo.h:103
@ PCB_ZONE_T
class ZONE, a copper pour area
Definition typeinfo.h:100
@ PCB_TEXT_T
class PCB_TEXT, text on a layer
Definition typeinfo.h:84
@ PCB_FOOTPRINT_T
class FOOTPRINT, a footprint
Definition typeinfo.h:78
@ PCB_PAD_T
class PAD, a pad in a footprint
Definition typeinfo.h:79
@ PCB_NETINFO_T
class NETINFO_ITEM, a description of a net
Definition typeinfo.h:102
@ PCB_TRACE_T
class PCB_TRACK, a track segment (segment on a copper layer)
Definition typeinfo.h:88