KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_drc_stub_length.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 The 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
23
24#include <filesystem>
25#include <fstream>
26
27#include <base_units.h>
28#include <board.h>
30#include <drc/drc_engine.h>
31#include <drc/drc_item.h>
32#include <footprint.h>
33#include <netinfo.h>
34#include <pad.h>
35#include <pcb_marker.h>
36#include <pcb_track.h>
38#include <drc/drc_rule_parser.h>
39#include <reporter.h>
40
41
42// Classifier-only fixture: the stub-length check today picks trunk/stub by
43// terminal-pad ownership, not by routed topology, so the three nets need only
44// be tagged into the same chain and given individually routed traces. Pads
45// UA-pad1 and UC-pad1 are assigned as the chain's terminal pads, leaving
46// MID_B as the only chain member without an endpoint assignment. Once real
47// topological stub detection lands the segments will need to actually meet at
48// series components.
49static const char* BOARD_FILE = "net_chains/stub_length.kicad_pcb";
50
51
52// stub_length 0..5 mm. The 30 mm middle net is well over budget; the 2 mm
53// endpoints are within budget *and* are protected by trunk membership anyway.
54static const char* DRU_TEXT = R"KICAD((version 1)
55
56(rule "StubBudget"
57 (condition "A.NetClass == 'Default'")
58 (constraint stub_length (min 0mm) (max 5mm))
59)
60)KICAD";
61
62
63BOOST_AUTO_TEST_SUITE( DRCStubLength )
64
65
66BOOST_AUTO_TEST_CASE( StubLengthFiresOnIntermediateNetOnly )
67{
68 namespace fs = std::filesystem;
69
70 fs::path tmpDir = fs::temp_directory_path() / "kicad_drc_stub_length";
71 fs::create_directories( tmpDir );
72
73 fs::path druPath = tmpDir / "stub_length.kicad_dru";
74
75 {
76 std::ofstream druOut( druPath );
77 druOut << DRU_TEXT;
78 }
79
80 PCB_IO_KICAD_SEXPR plugin;
81 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
82 plugin.LoadBoard( KI_TEST::GetPcbnewTestDataDir() + BOARD_FILE, board.get() );
83 board->BuildConnectivity();
84
85 NETINFO_ITEM* netA = board->FindNet( wxS( "/TRUNK_A" ) );
86 NETINFO_ITEM* netB = board->FindNet( wxS( "/MID_B" ) );
87 NETINFO_ITEM* netC = board->FindNet( wxS( "/TRUNK_C" ) );
88
89 BOOST_REQUIRE( netA );
90 BOOST_REQUIRE( netB );
91 BOOST_REQUIRE( netC );
92
93 netA->SetNetChain( wxS( "SIG" ) );
94 netB->SetNetChain( wxS( "SIG" ) );
95 netC->SetNetChain( wxS( "SIG" ) );
96
97 PAD* padA = nullptr;
98 PAD* padC = nullptr;
99
100 for( FOOTPRINT* fp : board->Footprints() )
101 {
102 for( PAD* pad : fp->Pads() )
103 {
104 if( pad->GetNetCode() == netA->GetNetCode() )
105 padA = pad;
106
107 if( pad->GetNetCode() == netC->GetNetCode() )
108 padC = pad;
109 }
110 }
111
112 BOOST_REQUIRE( padA );
113 BOOST_REQUIRE( padC );
114
115 netA->SetTerminalPad( 0, padA );
116 netC->SetTerminalPad( 1, padC );
117
118 BOARD_DESIGN_SETTINGS& bds = board->GetDesignSettings();
119
120 auto drcEngine = std::make_shared<DRC_ENGINE>( board.get(), &bds );
121 wxFileName ruleFile( druPath.string() );
122 drcEngine->InitEngine( ruleFile );
123 bds.m_DRCEngine = drcEngine;
124
132
133 std::vector<wxString> stubMessages;
134
135 drcEngine->SetViolationHandler(
136 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I&, int,
137 const std::function<void( PCB_MARKER* )>& )
138 {
139 if( aItem->GetErrorCode() == DRCE_NET_CHAIN_STUB_TOO_LONG )
140 stubMessages.push_back( aItem->GetErrorMessage( false ) );
141 } );
142
143 drcEngine->RunTests( EDA_UNITS::MM, true, false );
144
145 auto matchesNet = [&]( const wxString& netName )
146 {
147 for( const wxString& msg : stubMessages )
148 {
149 if( msg.Contains( wxString::Format( wxS( "'%s'" ), netName ) ) )
150 return true;
151 }
152
153 return false;
154 };
155
156 bool aFlagged = matchesNet( netA->GetNetname() );
157 bool bFlagged = matchesNet( netB->GetNetname() );
158 bool cFlagged = matchesNet( netC->GetNetname() );
159
160 BOOST_CHECK_MESSAGE( bFlagged,
161 "Intermediate stub net MID_B should fire stub_length violation" );
162 BOOST_CHECK_MESSAGE( !aFlagged,
163 "Trunk endpoint net TRUNK_A (owns terminal_pad_0) must not fire" );
164 BOOST_CHECK_MESSAGE( !cFlagged,
165 "Trunk endpoint net TRUNK_C (owns terminal_pad_1) must not fire" );
166
167 std::error_code ec;
168 fs::remove( druPath, ec );
169}
170
171
172BOOST_AUTO_TEST_CASE( StubLengthQuietOnTwoNetChain )
173{
174 namespace fs = std::filesystem;
175
176 fs::path tmpDir = fs::temp_directory_path() / "kicad_drc_stub_length";
177 fs::create_directories( tmpDir );
178
179 fs::path druPath = tmpDir / "stub_length_2net.kicad_dru";
180
181 // Two-net chain: every member owns a terminal pad assignment, so no member
182 // is a stub. With the 30 mm trace on each net, a per-net stub_length would
183 // fire if the trunk classifier wrongly excluded one of them.
184 static const char* TWO_NET_BOARD_FILE = "net_chains/stub_length_two_net.kicad_pcb";
185
186 static const char* TWO_NET_DRU = R"KICAD((version 1)
187
188(rule "StubBudget"
189 (condition "A.NetClass == 'Default'")
190 (constraint stub_length (min 0mm) (max 5mm))
191)
192)KICAD";
193
194 {
195 std::ofstream druOut( druPath );
196 druOut << TWO_NET_DRU;
197 }
198
199 PCB_IO_KICAD_SEXPR plugin;
200 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
201 plugin.LoadBoard( KI_TEST::GetPcbnewTestDataDir() + TWO_NET_BOARD_FILE, board.get() );
202 board->BuildConnectivity();
203
204 NETINFO_ITEM* netA = board->FindNet( wxS( "/A" ) );
205 NETINFO_ITEM* netB = board->FindNet( wxS( "/B" ) );
206
207 BOOST_REQUIRE( netA );
208 BOOST_REQUIRE( netB );
209
210 netA->SetNetChain( wxS( "SIG" ) );
211 netB->SetNetChain( wxS( "SIG" ) );
212
213 PAD* padA = nullptr;
214 PAD* padB = nullptr;
215
216 for( FOOTPRINT* fp : board->Footprints() )
217 {
218 for( PAD* pad : fp->Pads() )
219 {
220 if( pad->GetNetCode() == netA->GetNetCode() )
221 padA = pad;
222
223 if( pad->GetNetCode() == netB->GetNetCode() )
224 padB = pad;
225 }
226 }
227
228 BOOST_REQUIRE( padA );
229 BOOST_REQUIRE( padB );
230
231 netA->SetTerminalPad( 0, padA );
232 netB->SetTerminalPad( 1, padB );
233
234 BOARD_DESIGN_SETTINGS& bds = board->GetDesignSettings();
235
236 auto drcEngine = std::make_shared<DRC_ENGINE>( board.get(), &bds );
237 wxFileName ruleFile( druPath.string() );
238 drcEngine->InitEngine( ruleFile );
239 bds.m_DRCEngine = drcEngine;
240
248
249 int stubCount = 0;
250
251 drcEngine->SetViolationHandler(
252 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I&, int,
253 const std::function<void( PCB_MARKER* )>& )
254 {
255 if( aItem->GetErrorCode() == DRCE_NET_CHAIN_STUB_TOO_LONG )
256 ++stubCount;
257 } );
258
259 drcEngine->RunTests( EDA_UNITS::MM, true, false );
260
261 BOOST_CHECK_MESSAGE( stubCount == 0,
262 "Two-net chain with both ends owning terminal pads must not "
263 "fire any stub_length violation, got "
264 << stubCount );
265
266 std::error_code ec;
267 fs::remove( druPath, ec );
268}
269
270
271BOOST_AUTO_TEST_CASE( StubLengthIncludesPadToDie )
272{
273 namespace fs = std::filesystem;
274
275 fs::path tmpDir = fs::temp_directory_path() / "kicad_drc_stub_length";
276 fs::create_directories( tmpDir );
277
278 fs::path druPath = tmpDir / "stub_length_pad_to_die.kicad_dru";
279
280 // Three-net chain whose middle net (MID_B) is a stub. The stub's routed
281 // copper is a short 2 mm trace, well under the 5 mm budget. A 10 mm
282 // pad-to-die length is attached to the MID_B pad, so the *total* stub
283 // length (route + pad-to-die) is 12 mm and must violate the constraint.
284 static const char* PAD_TO_DIE_BOARD_FILE = "net_chains/stub_length_pad_to_die.kicad_pcb";
285
286 static const char* PAD_TO_DIE_DRU = R"KICAD((version 1)
287
288(rule "StubBudget"
289 (condition "A.NetClass == 'Default'")
290 (constraint stub_length (min 0mm) (max 5mm))
291)
292)KICAD";
293
294 {
295 std::ofstream druOut( druPath );
296 druOut << PAD_TO_DIE_DRU;
297 }
298
299 PCB_IO_KICAD_SEXPR plugin;
300 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
301 plugin.LoadBoard( KI_TEST::GetPcbnewTestDataDir() + PAD_TO_DIE_BOARD_FILE, board.get() );
302
303 NETINFO_ITEM* netA = board->FindNet( wxS( "/TRUNK_A" ) );
304 NETINFO_ITEM* netB = board->FindNet( wxS( "/MID_B" ) );
305 NETINFO_ITEM* netC = board->FindNet( wxS( "/TRUNK_C" ) );
306
307 BOOST_REQUIRE( netA );
308 BOOST_REQUIRE( netB );
309 BOOST_REQUIRE( netC );
310
311 netA->SetNetChain( wxS( "SIG" ) );
312 netB->SetNetChain( wxS( "SIG" ) );
313 netC->SetNetChain( wxS( "SIG" ) );
314
315 PAD* padA = nullptr;
316 PAD* padB = nullptr;
317 PAD* padC = nullptr;
318
319 for( FOOTPRINT* fp : board->Footprints() )
320 {
321 for( PAD* pad : fp->Pads() )
322 {
323 if( pad->GetNetCode() == netA->GetNetCode() )
324 padA = pad;
325
326 if( pad->GetNetCode() == netB->GetNetCode() )
327 padB = pad;
328
329 if( pad->GetNetCode() == netC->GetNetCode() )
330 padC = pad;
331 }
332 }
333
334 BOOST_REQUIRE( padA );
335 BOOST_REQUIRE( padB );
336 BOOST_REQUIRE( padC );
337
338 // 10 mm pad-to-die on the stub net pushes the total stub length above the
339 // 5 mm budget even though the routed copper is only 2 mm.
340 padB->SetPadToDieLength( pcbIUScale.mmToIU( 10 ) );
341
342 netA->SetTerminalPad( 0, padA );
343 netC->SetTerminalPad( 1, padC );
344
345 board->BuildConnectivity();
346
347 BOARD_DESIGN_SETTINGS& bds = board->GetDesignSettings();
348
349 auto drcEngine = std::make_shared<DRC_ENGINE>( board.get(), &bds );
350 wxFileName ruleFile( druPath.string() );
351 drcEngine->InitEngine( ruleFile );
352 bds.m_DRCEngine = drcEngine;
353
361
362 std::vector<wxString> stubMessages;
363
364 drcEngine->SetViolationHandler(
365 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I&, int,
366 const std::function<void( PCB_MARKER* )>& )
367 {
368 if( aItem->GetErrorCode() == DRCE_NET_CHAIN_STUB_TOO_LONG )
369 stubMessages.push_back( aItem->GetErrorMessage( false ) );
370 } );
371
372 drcEngine->RunTests( EDA_UNITS::MM, true, false );
373
374 auto matchesNet = [&]( const wxString& netName )
375 {
376 for( const wxString& msg : stubMessages )
377 {
378 if( msg.Contains( wxString::Format( wxS( "'%s'" ), netName ) ) )
379 return true;
380 }
381
382 return false;
383 };
384
385 BOOST_CHECK_MESSAGE( matchesNet( netB->GetNetname() ),
386 "Stub net MID_B with 2 mm route + 10 mm pad-to-die must violate "
387 "the 5 mm stub_length budget" );
388
389 std::error_code ec;
390 fs::remove( druPath, ec );
391}
392
393
394BOOST_AUTO_TEST_CASE( StubLengthAcceptsTimeDomainUnits )
395{
396 // Regression for H-5: stub_length used to be missing from the
397 // allowsTimeDomain allow-list in DRC_RULES_PARSER, so a (max 100ps)
398 // value would be rejected with "Time based units not allowed for
399 // constraint type." The rule must parse cleanly and the resulting
400 // constraint must be flagged as TIME_DOMAIN.
401 const wxString DRU_TIME = wxS(
402 "(version 1)\n"
403 "(rule \"StubBudgetTime\"\n"
404 " (condition \"A.NetClass == 'Default'\")\n"
405 " (constraint stub_length (max 100ps))\n"
406 ")\n" );
407
408 DRC_RULES_PARSER parser( DRU_TIME, wxS( "stub_length_time_test" ) );
409
411 std::vector<std::shared_ptr<DRC_RULE>> rules;
412
413 parser.Parse( rules, &reporter );
414
415 BOOST_CHECK_MESSAGE( !reporter.HasMessageOfSeverity( RPT_SEVERITY_ERROR ),
416 "stub_length with ps units must parse without error, got: "
417 << reporter.GetMessages().ToStdString() );
418
419 BOOST_REQUIRE_EQUAL( rules.size(), 1u );
420
421 std::optional<DRC_CONSTRAINT> stubConstraint =
422 rules.front()->FindConstraint( NET_CHAIN_STUB_LENGTH_CONSTRAINT );
423
424 BOOST_REQUIRE( stubConstraint.has_value() );
425 BOOST_CHECK( stubConstraint->GetOption( DRC_CONSTRAINT::OPTIONS::TIME_DOMAIN ) );
426 BOOST_CHECK( !stubConstraint->GetOption( DRC_CONSTRAINT::OPTIONS::SPACE_DOMAIN ) );
427 BOOST_REQUIRE( stubConstraint->GetValue().HasMax() );
428 // 1 ps == 1e6 IU (internal time unit is the attosecond).
429 BOOST_CHECK_EQUAL( stubConstraint->GetValue().Max(), 100 * 1000 * 1000 );
430}
431
432
433BOOST_AUTO_TEST_CASE( ReturnPathDoesNotCarryTimeDomainOption )
434{
435 // return_path takes a layer name, not a delay, so the TIME_DOMAIN option
436 // must never be set on a return_path constraint regardless of stray
437 // (max ...) subtrees. The return_path branch in the parser ignores any
438 // unknown sub-options (it only consumes (layer ...)), so we verify the
439 // resulting constraint stays in space-domain configuration.
440 const wxString DRU_LAYER = wxS(
441 "(version 1)\n"
442 "(rule \"GoodReturnPath\"\n"
443 " (condition \"A.NetClass == 'Default'\")\n"
444 " (constraint return_path (layer \"B.Cu\"))\n"
445 ")\n" );
446
447 DRC_RULES_PARSER parser( DRU_LAYER, wxS( "return_path_layer_test" ) );
448
450 std::vector<std::shared_ptr<DRC_RULE>> rules;
451
452 parser.Parse( rules, &reporter );
453
454 BOOST_CHECK_MESSAGE( !reporter.HasMessageOfSeverity( RPT_SEVERITY_ERROR ),
455 "return_path with a layer must parse without error, got: "
456 << reporter.GetMessages().ToStdString() );
457
458 BOOST_REQUIRE_EQUAL( rules.size(), 1u );
459
460 std::optional<DRC_CONSTRAINT> rpConstraint =
461 rules.front()->FindConstraint( NET_CHAIN_RETURN_PATH_CONSTRAINT );
462
463 BOOST_REQUIRE( rpConstraint.has_value() );
464 BOOST_CHECK( !rpConstraint->GetOption( DRC_CONSTRAINT::OPTIONS::TIME_DOMAIN ) );
465}
466
467
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:121
General utilities for PCB file IO for QA programs.
Container for design settings for a BOARD object.
std::map< int, SEVERITY > m_DRCSeverities
std::shared_ptr< DRC_ENGINE > m_DRCEngine
void Parse(std::vector< std::shared_ptr< DRC_RULE > > &aRules, REPORTER *aReporter)
Handle the data for a net.
Definition netinfo.h:46
const wxString & GetNetname() const
Definition netinfo.h:100
int GetNetCode() const
Definition netinfo.h:94
void SetNetChain(const wxString &aNetChain)
Definition netinfo.h:113
void SetTerminalPad(int aIndex, PAD *aPad)
Definition netinfo.h:116
Definition pad.h:61
void SetPadToDieLength(int aLength)
Definition pad.h:572
A #PLUGIN derivation for saving and loading Pcbnew s-expression formatted files.
BOARD * LoadBoard(const wxString &aFileName, BOARD *aAppendToMe, const std::map< std::string, UTF8 > *aProperties=nullptr, PROJECT *aProject=nullptr) override
Load information from some input file format that this PCB_IO implementation knows about into either ...
A wrapper for reporting to a wxString object.
Definition reporter.h:225
@ DRCE_UNCONNECTED_ITEMS
Definition drc_item.h:37
@ DRCE_LIB_FOOTPRINT_ISSUES
Definition drc_item.h:80
@ DRCE_INVALID_OUTLINE
Definition drc_item.h:70
@ DRCE_NET_CHAIN_STUB_TOO_LONG
Definition drc_item.h:103
@ DRCE_DRILL_OUT_OF_RANGE
Definition drc_item.h:58
@ DRCE_DANGLING_VIA
Definition drc_item.h:48
@ DRCE_LIB_FOOTPRINT_MISMATCH
Definition drc_item.h:81
@ NET_CHAIN_STUB_LENGTH_CONSTRAINT
Definition drc_rule.h:75
@ NET_CHAIN_RETURN_PATH_CONSTRAINT
Definition drc_rule.h:76
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
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
static const char * BOARD_FILE
static const char * DRU_TEXT
BOOST_AUTO_TEST_CASE(StubLengthFiresOnIntermediateNetOnly)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
IbisParser parser & reporter
BOOST_CHECK_EQUAL(result, "25.4")
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683