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