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 = plugin.LoadBoard( KI_TEST::GetPcbnewTestDataDir() + BOARD_FILE );
82 board->BuildConnectivity();
83
84 NETINFO_ITEM* netA = board->FindNet( wxS( "/TRUNK_A" ) );
85 NETINFO_ITEM* netB = board->FindNet( wxS( "/MID_B" ) );
86 NETINFO_ITEM* netC = board->FindNet( wxS( "/TRUNK_C" ) );
87
88 BOOST_REQUIRE( netA );
89 BOOST_REQUIRE( netB );
90 BOOST_REQUIRE( netC );
91
92 netA->SetNetChain( wxS( "SIG" ) );
93 netB->SetNetChain( wxS( "SIG" ) );
94 netC->SetNetChain( wxS( "SIG" ) );
95
96 PAD* padA = nullptr;
97 PAD* padC = nullptr;
98
99 for( FOOTPRINT* fp : board->Footprints() )
100 {
101 for( PAD* pad : fp->Pads() )
102 {
103 if( pad->GetNetCode() == netA->GetNetCode() )
104 padA = pad;
105
106 if( pad->GetNetCode() == netC->GetNetCode() )
107 padC = pad;
108 }
109 }
110
111 BOOST_REQUIRE( padA );
112 BOOST_REQUIRE( padC );
113
114 netA->SetTerminalPad( 0, padA );
115 netC->SetTerminalPad( 1, padC );
116
117 BOARD_DESIGN_SETTINGS& bds = board->GetDesignSettings();
118
119 auto drcEngine = std::make_shared<DRC_ENGINE>( board.get(), &bds );
120 wxFileName ruleFile( druPath.string() );
121 drcEngine->InitEngine( ruleFile );
122 bds.m_DRCEngine = drcEngine;
123
131
132 std::vector<wxString> stubMessages;
133
134 drcEngine->SetViolationHandler(
135 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I&, int,
136 const std::function<void( PCB_MARKER* )>& )
137 {
138 if( aItem->GetErrorCode() == DRCE_NET_CHAIN_STUB_TOO_LONG )
139 stubMessages.push_back( aItem->GetErrorMessage( false ) );
140 } );
141
142 drcEngine->RunTests( EDA_UNITS::MM, true, false );
143
144 auto matchesNet = [&]( const wxString& netName )
145 {
146 for( const wxString& msg : stubMessages )
147 {
148 if( msg.Contains( wxString::Format( wxS( "'%s'" ), netName ) ) )
149 return true;
150 }
151
152 return false;
153 };
154
155 bool aFlagged = matchesNet( netA->GetNetname() );
156 bool bFlagged = matchesNet( netB->GetNetname() );
157 bool cFlagged = matchesNet( netC->GetNetname() );
158
159 BOOST_CHECK_MESSAGE( bFlagged,
160 "Intermediate stub net MID_B should fire stub_length violation" );
161 BOOST_CHECK_MESSAGE( !aFlagged,
162 "Trunk endpoint net TRUNK_A (owns terminal_pad_0) must not fire" );
163 BOOST_CHECK_MESSAGE( !cFlagged,
164 "Trunk endpoint net TRUNK_C (owns terminal_pad_1) must not fire" );
165
166 std::error_code ec;
167 fs::remove( druPath, ec );
168}
169
170
171BOOST_AUTO_TEST_CASE( StubLengthQuietOnTwoNetChain )
172{
173 namespace fs = std::filesystem;
174
175 fs::path tmpDir = fs::temp_directory_path() / "kicad_drc_stub_length";
176 fs::create_directories( tmpDir );
177
178 fs::path druPath = tmpDir / "stub_length_2net.kicad_dru";
179
180 // Two-net chain: every member owns a terminal pad assignment, so no member
181 // is a stub. With the 30 mm trace on each net, a per-net stub_length would
182 // fire if the trunk classifier wrongly excluded one of them.
183 static const char* TWO_NET_BOARD_FILE = "net_chains/stub_length_two_net.kicad_pcb";
184
185 static const char* TWO_NET_DRU = R"KICAD((version 1)
186
187(rule "StubBudget"
188 (condition "A.NetClass == 'Default'")
189 (constraint stub_length (min 0mm) (max 5mm))
190)
191)KICAD";
192
193 {
194 std::ofstream druOut( druPath );
195 druOut << TWO_NET_DRU;
196 }
197
198 PCB_IO_KICAD_SEXPR plugin;
199 std::unique_ptr<BOARD> board = plugin.LoadBoard( KI_TEST::GetPcbnewTestDataDir() + TWO_NET_BOARD_FILE );
200 board->BuildConnectivity();
201
202 NETINFO_ITEM* netA = board->FindNet( wxS( "/A" ) );
203 NETINFO_ITEM* netB = board->FindNet( wxS( "/B" ) );
204
205 BOOST_REQUIRE( netA );
206 BOOST_REQUIRE( netB );
207
208 netA->SetNetChain( wxS( "SIG" ) );
209 netB->SetNetChain( wxS( "SIG" ) );
210
211 PAD* padA = nullptr;
212 PAD* padB = nullptr;
213
214 for( FOOTPRINT* fp : board->Footprints() )
215 {
216 for( PAD* pad : fp->Pads() )
217 {
218 if( pad->GetNetCode() == netA->GetNetCode() )
219 padA = pad;
220
221 if( pad->GetNetCode() == netB->GetNetCode() )
222 padB = pad;
223 }
224 }
225
226 BOOST_REQUIRE( padA );
227 BOOST_REQUIRE( padB );
228
229 netA->SetTerminalPad( 0, padA );
230 netB->SetTerminalPad( 1, padB );
231
232 BOARD_DESIGN_SETTINGS& bds = board->GetDesignSettings();
233
234 auto drcEngine = std::make_shared<DRC_ENGINE>( board.get(), &bds );
235 wxFileName ruleFile( druPath.string() );
236 drcEngine->InitEngine( ruleFile );
237 bds.m_DRCEngine = drcEngine;
238
246
247 int stubCount = 0;
248
249 drcEngine->SetViolationHandler(
250 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I&, int,
251 const std::function<void( PCB_MARKER* )>& )
252 {
253 if( aItem->GetErrorCode() == DRCE_NET_CHAIN_STUB_TOO_LONG )
254 ++stubCount;
255 } );
256
257 drcEngine->RunTests( EDA_UNITS::MM, true, false );
258
259 BOOST_CHECK_MESSAGE( stubCount == 0,
260 "Two-net chain with both ends owning terminal pads must not "
261 "fire any stub_length violation, got "
262 << stubCount );
263
264 std::error_code ec;
265 fs::remove( druPath, ec );
266}
267
268
269BOOST_AUTO_TEST_CASE( StubLengthIncludesPadToDie )
270{
271 namespace fs = std::filesystem;
272
273 fs::path tmpDir = fs::temp_directory_path() / "kicad_drc_stub_length";
274 fs::create_directories( tmpDir );
275
276 fs::path druPath = tmpDir / "stub_length_pad_to_die.kicad_dru";
277
278 // Three-net chain whose middle net (MID_B) is a stub. The stub's routed
279 // copper is a short 2 mm trace, well under the 5 mm budget. A 10 mm
280 // pad-to-die length is attached to the MID_B pad, so the *total* stub
281 // length (route + pad-to-die) is 12 mm and must violate the constraint.
282 static const char* PAD_TO_DIE_BOARD_FILE = "net_chains/stub_length_pad_to_die.kicad_pcb";
283
284 static const char* PAD_TO_DIE_DRU = R"KICAD((version 1)
285
286(rule "StubBudget"
287 (condition "A.NetClass == 'Default'")
288 (constraint stub_length (min 0mm) (max 5mm))
289)
290)KICAD";
291
292 {
293 std::ofstream druOut( druPath );
294 druOut << PAD_TO_DIE_DRU;
295 }
296
297 PCB_IO_KICAD_SEXPR plugin;
298 std::unique_ptr<BOARD> board = plugin.LoadBoard( KI_TEST::GetPcbnewTestDataDir() + PAD_TO_DIE_BOARD_FILE );
299
300 NETINFO_ITEM* netA = board->FindNet( wxS( "/TRUNK_A" ) );
301 NETINFO_ITEM* netB = board->FindNet( wxS( "/MID_B" ) );
302 NETINFO_ITEM* netC = board->FindNet( wxS( "/TRUNK_C" ) );
303
304 BOOST_REQUIRE( netA );
305 BOOST_REQUIRE( netB );
306 BOOST_REQUIRE( netC );
307
308 netA->SetNetChain( wxS( "SIG" ) );
309 netB->SetNetChain( wxS( "SIG" ) );
310 netC->SetNetChain( wxS( "SIG" ) );
311
312 PAD* padA = nullptr;
313 PAD* padB = nullptr;
314 PAD* padC = nullptr;
315
316 for( FOOTPRINT* fp : board->Footprints() )
317 {
318 for( PAD* pad : fp->Pads() )
319 {
320 if( pad->GetNetCode() == netA->GetNetCode() )
321 padA = pad;
322
323 if( pad->GetNetCode() == netB->GetNetCode() )
324 padB = pad;
325
326 if( pad->GetNetCode() == netC->GetNetCode() )
327 padC = pad;
328 }
329 }
330
331 BOOST_REQUIRE( padA );
332 BOOST_REQUIRE( padB );
333 BOOST_REQUIRE( padC );
334
335 // 10 mm pad-to-die on the stub net pushes the total stub length above the
336 // 5 mm budget even though the routed copper is only 2 mm.
337 padB->SetPadToDieLength( pcbIUScale.mmToIU( 10 ) );
338
339 netA->SetTerminalPad( 0, padA );
340 netC->SetTerminalPad( 1, padC );
341
342 board->BuildConnectivity();
343
344 BOARD_DESIGN_SETTINGS& bds = board->GetDesignSettings();
345
346 auto drcEngine = std::make_shared<DRC_ENGINE>( board.get(), &bds );
347 wxFileName ruleFile( druPath.string() );
348 drcEngine->InitEngine( ruleFile );
349 bds.m_DRCEngine = drcEngine;
350
358
359 std::vector<wxString> stubMessages;
360
361 drcEngine->SetViolationHandler(
362 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I&, int,
363 const std::function<void( PCB_MARKER* )>& )
364 {
365 if( aItem->GetErrorCode() == DRCE_NET_CHAIN_STUB_TOO_LONG )
366 stubMessages.push_back( aItem->GetErrorMessage( false ) );
367 } );
368
369 drcEngine->RunTests( EDA_UNITS::MM, true, false );
370
371 auto matchesNet = [&]( const wxString& netName )
372 {
373 for( const wxString& msg : stubMessages )
374 {
375 if( msg.Contains( wxString::Format( wxS( "'%s'" ), netName ) ) )
376 return true;
377 }
378
379 return false;
380 };
381
382 BOOST_CHECK_MESSAGE( matchesNet( netB->GetNetname() ),
383 "Stub net MID_B with 2 mm route + 10 mm pad-to-die must violate "
384 "the 5 mm stub_length budget" );
385
386 std::error_code ec;
387 fs::remove( druPath, ec );
388}
389
390
391BOOST_AUTO_TEST_CASE( StubLengthAcceptsTimeDomainUnits )
392{
393 // Regression for H-5: stub_length used to be missing from the
394 // allowsTimeDomain allow-list in DRC_RULES_PARSER, so a (max 100ps)
395 // value would be rejected with "Time based units not allowed for
396 // constraint type." The rule must parse cleanly and the resulting
397 // constraint must be flagged as TIME_DOMAIN.
398 const wxString DRU_TIME = wxS(
399 "(version 1)\n"
400 "(rule \"StubBudgetTime\"\n"
401 " (condition \"A.NetClass == 'Default'\")\n"
402 " (constraint stub_length (max 100ps))\n"
403 ")\n" );
404
405 DRC_RULES_PARSER parser( DRU_TIME, wxS( "stub_length_time_test" ) );
406
408 std::vector<std::shared_ptr<DRC_RULE>> rules;
409
410 parser.Parse( rules, &reporter );
411
412 BOOST_CHECK_MESSAGE( !reporter.HasMessageOfSeverity( RPT_SEVERITY_ERROR ),
413 "stub_length with ps units must parse without error, got: "
414 << reporter.GetMessages().ToStdString() );
415
416 BOOST_REQUIRE_EQUAL( rules.size(), 1u );
417
418 std::optional<DRC_CONSTRAINT> stubConstraint =
419 rules.front()->FindConstraint( NET_CHAIN_STUB_LENGTH_CONSTRAINT );
420
421 BOOST_REQUIRE( stubConstraint.has_value() );
422 BOOST_CHECK( stubConstraint->GetOption( DRC_CONSTRAINT::OPTIONS::TIME_DOMAIN ) );
423 BOOST_CHECK( !stubConstraint->GetOption( DRC_CONSTRAINT::OPTIONS::SPACE_DOMAIN ) );
424 BOOST_REQUIRE( stubConstraint->GetValue().HasMax() );
425 // 1 ps == 1e6 IU (internal time unit is the attosecond).
426 BOOST_CHECK_EQUAL( stubConstraint->GetValue().Max(), 100 * 1000 * 1000 );
427}
428
429
430BOOST_AUTO_TEST_CASE( ReturnPathDoesNotCarryTimeDomainOption )
431{
432 // return_path takes a layer name, not a delay, so the TIME_DOMAIN option
433 // must never be set on a return_path constraint regardless of stray
434 // (max ...) subtrees. The return_path branch in the parser ignores any
435 // unknown sub-options (it only consumes (layer ...)), so we verify the
436 // resulting constraint stays in space-domain configuration.
437 const wxString DRU_LAYER = wxS(
438 "(version 1)\n"
439 "(rule \"GoodReturnPath\"\n"
440 " (condition \"A.NetClass == 'Default'\")\n"
441 " (constraint return_path (layer \"B.Cu\"))\n"
442 ")\n" );
443
444 DRC_RULES_PARSER parser( DRU_LAYER, wxS( "return_path_layer_test" ) );
445
447 std::vector<std::shared_ptr<DRC_RULE>> rules;
448
449 parser.Parse( rules, &reporter );
450
451 BOOST_CHECK_MESSAGE( !reporter.HasMessageOfSeverity( RPT_SEVERITY_ERROR ),
452 "return_path with a layer must parse without error, got: "
453 << reporter.GetMessages().ToStdString() );
454
455 BOOST_REQUIRE_EQUAL( rules.size(), 1u );
456
457 std::optional<DRC_CONSTRAINT> rpConstraint =
458 rules.front()->FindConstraint( NET_CHAIN_RETURN_PATH_CONSTRAINT );
459
460 BOOST_REQUIRE( rpConstraint.has_value() );
461 BOOST_CHECK( !rpConstraint->GetOption( DRC_CONSTRAINT::OPTIONS::TIME_DOMAIN ) );
462}
463
464
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:50
const wxString & GetNetname() const
Definition netinfo.h:110
int GetNetCode() const
Definition netinfo.h:104
void SetNetChain(const wxString &aNetChain)
Definition netinfo.h:123
void SetTerminalPad(int aIndex, PAD *aPad)
Definition netinfo.h:126
Definition pad.h:61
void SetPadToDieLength(int aLength)
Definition pad.h:575
A #PLUGIN 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
A wrapper for reporting to a wxString object.
Definition reporter.h:242
@ DRCE_UNCONNECTED_ITEMS
Definition drc_item.h:37
@ DRCE_LIB_FOOTPRINT_ISSUES
Definition drc_item.h:85
@ DRCE_INVALID_OUTLINE
Definition drc_item.h:75
@ DRCE_NET_CHAIN_STUB_TOO_LONG
Definition drc_item.h:108
@ 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:86
@ 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