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#include <qa_utils/file_utils.h>
22
24
25#include <filesystem>
26#include <fstream>
27
28#include <base_units.h>
29#include <board.h>
31#include <drc/drc_engine.h>
32#include <drc/drc_item.h>
33#include <footprint.h>
34#include <netinfo.h>
35#include <pad.h>
36#include <pcb_marker.h>
37#include <pcb_track.h>
39#include <drc/drc_rule_parser.h>
40#include <reporter.h>
41
42
43// Classifier-only fixture: the stub-length check today picks trunk/stub by
44// terminal-pad ownership, not by routed topology, so the three nets need only
45// be tagged into the same chain and given individually routed traces. Pads
46// UA-pad1 and UC-pad1 are assigned as the chain's terminal pads, leaving
47// MID_B as the only chain member without an endpoint assignment. Once real
48// topological stub detection lands the segments will need to actually meet at
49// series components.
50static const char* BOARD_FILE = "net_chains/stub_length.kicad_pcb";
51
52
53// stub_length 0..5 mm. The 30 mm middle net is well over budget; the 2 mm
54// endpoints are within budget *and* are protected by trunk membership anyway.
55static const char* DRU_TEXT = R"KICAD((version 1)
56
57(rule "StubBudget"
58 (condition "A.NetClass == 'Default'")
59 (constraint stub_length (min 0mm) (max 5mm))
60)
61)KICAD";
62
63
64BOOST_AUTO_TEST_SUITE( DRCStubLength )
65
66
67BOOST_AUTO_TEST_CASE( StubLengthFiresOnIntermediateNetOnly )
68{
69 namespace fs = std::filesystem;
70
71 KI_TEST::SCOPED_TEMP_DIR tmpDir( wxS( "kicad_drc_stub_length" ) );
72
73 fs::path druPath = tmpDir.Path() / "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 KI_TEST::SCOPED_TEMP_DIR tmpDir( wxS( "kicad_drc_stub_length_2net" ) );
176
177 fs::path druPath = tmpDir.Path() / "stub_length_2net.kicad_dru";
178
179 // Two-net chain: every member owns a terminal pad assignment, so no member
180 // is a stub. With the 30 mm trace on each net, a per-net stub_length would
181 // fire if the trunk classifier wrongly excluded one of them.
182 static const char* TWO_NET_BOARD_FILE = "net_chains/stub_length_two_net.kicad_pcb";
183
184 static const char* TWO_NET_DRU = R"KICAD((version 1)
185
186(rule "StubBudget"
187 (condition "A.NetClass == 'Default'")
188 (constraint stub_length (min 0mm) (max 5mm))
189)
190)KICAD";
191
192 {
193 std::ofstream druOut( druPath );
194 druOut << TWO_NET_DRU;
195 }
196
197 PCB_IO_KICAD_SEXPR plugin;
198 std::unique_ptr<BOARD> board = plugin.LoadBoard( KI_TEST::GetPcbnewTestDataDir() + TWO_NET_BOARD_FILE );
199 board->BuildConnectivity();
200
201 NETINFO_ITEM* netA = board->FindNet( wxS( "/A" ) );
202 NETINFO_ITEM* netB = board->FindNet( wxS( "/B" ) );
203
204 BOOST_REQUIRE( netA );
205 BOOST_REQUIRE( netB );
206
207 netA->SetNetChain( wxS( "SIG" ) );
208 netB->SetNetChain( wxS( "SIG" ) );
209
210 PAD* padA = nullptr;
211 PAD* padB = nullptr;
212
213 for( FOOTPRINT* fp : board->Footprints() )
214 {
215 for( PAD* pad : fp->Pads() )
216 {
217 if( pad->GetNetCode() == netA->GetNetCode() )
218 padA = pad;
219
220 if( pad->GetNetCode() == netB->GetNetCode() )
221 padB = pad;
222 }
223 }
224
225 BOOST_REQUIRE( padA );
226 BOOST_REQUIRE( padB );
227
228 netA->SetTerminalPad( 0, padA );
229 netB->SetTerminalPad( 1, padB );
230
231 BOARD_DESIGN_SETTINGS& bds = board->GetDesignSettings();
232
233 auto drcEngine = std::make_shared<DRC_ENGINE>( board.get(), &bds );
234 wxFileName ruleFile( druPath.string() );
235 drcEngine->InitEngine( ruleFile );
236 bds.m_DRCEngine = drcEngine;
237
245
246 int stubCount = 0;
247
248 drcEngine->SetViolationHandler(
249 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I&, int,
250 const std::function<void( PCB_MARKER* )>& )
251 {
252 if( aItem->GetErrorCode() == DRCE_NET_CHAIN_STUB_TOO_LONG )
253 ++stubCount;
254 } );
255
256 drcEngine->RunTests( EDA_UNITS::MM, true, false );
257
258 BOOST_CHECK_MESSAGE( stubCount == 0,
259 "Two-net chain with both ends owning terminal pads must not "
260 "fire any stub_length violation, got "
261 << stubCount );
262
263 std::error_code ec;
264 fs::remove( druPath, ec );
265}
266
267
268BOOST_AUTO_TEST_CASE( StubLengthIncludesPadToDie )
269{
270 namespace fs = std::filesystem;
271
272 KI_TEST::SCOPED_TEMP_DIR tmpDir( wxS( "kicad_drc_stub_length_pad_to_die" ) );
273
274 fs::path druPath = tmpDir.Path() / "stub_length_pad_to_die.kicad_dru";
275
276 // Three-net chain whose middle net (MID_B) is a stub. The stub's routed
277 // copper is a short 2 mm trace, well under the 5 mm budget. A 10 mm
278 // pad-to-die length is attached to the MID_B pad, so the *total* stub
279 // length (route + pad-to-die) is 12 mm and must violate the constraint.
280 static const char* PAD_TO_DIE_BOARD_FILE = "net_chains/stub_length_pad_to_die.kicad_pcb";
281
282 static const char* PAD_TO_DIE_DRU = R"KICAD((version 1)
283
284(rule "StubBudget"
285 (condition "A.NetClass == 'Default'")
286 (constraint stub_length (min 0mm) (max 5mm))
287)
288)KICAD";
289
290 {
291 std::ofstream druOut( druPath );
292 druOut << PAD_TO_DIE_DRU;
293 }
294
295 PCB_IO_KICAD_SEXPR plugin;
296 std::unique_ptr<BOARD> board = plugin.LoadBoard( KI_TEST::GetPcbnewTestDataDir() + PAD_TO_DIE_BOARD_FILE );
297
298 NETINFO_ITEM* netA = board->FindNet( wxS( "/TRUNK_A" ) );
299 NETINFO_ITEM* netB = board->FindNet( wxS( "/MID_B" ) );
300 NETINFO_ITEM* netC = board->FindNet( wxS( "/TRUNK_C" ) );
301
302 BOOST_REQUIRE( netA );
303 BOOST_REQUIRE( netB );
304 BOOST_REQUIRE( netC );
305
306 netA->SetNetChain( wxS( "SIG" ) );
307 netB->SetNetChain( wxS( "SIG" ) );
308 netC->SetNetChain( wxS( "SIG" ) );
309
310 PAD* padA = nullptr;
311 PAD* padB = nullptr;
312 PAD* padC = nullptr;
313
314 for( FOOTPRINT* fp : board->Footprints() )
315 {
316 for( PAD* pad : fp->Pads() )
317 {
318 if( pad->GetNetCode() == netA->GetNetCode() )
319 padA = pad;
320
321 if( pad->GetNetCode() == netB->GetNetCode() )
322 padB = pad;
323
324 if( pad->GetNetCode() == netC->GetNetCode() )
325 padC = pad;
326 }
327 }
328
329 BOOST_REQUIRE( padA );
330 BOOST_REQUIRE( padB );
331 BOOST_REQUIRE( padC );
332
333 // 10 mm pad-to-die on the stub net pushes the total stub length above the
334 // 5 mm budget even though the routed copper is only 2 mm.
335 padB->SetPadToDieLength( pcbIUScale.mmToIU( 10 ) );
336
337 netA->SetTerminalPad( 0, padA );
338 netC->SetTerminalPad( 1, padC );
339
340 board->BuildConnectivity();
341
342 BOARD_DESIGN_SETTINGS& bds = board->GetDesignSettings();
343
344 auto drcEngine = std::make_shared<DRC_ENGINE>( board.get(), &bds );
345 wxFileName ruleFile( druPath.string() );
346 drcEngine->InitEngine( ruleFile );
347 bds.m_DRCEngine = drcEngine;
348
356
357 std::vector<wxString> stubMessages;
358
359 drcEngine->SetViolationHandler(
360 [&]( const std::shared_ptr<DRC_ITEM>& aItem, const VECTOR2I&, int,
361 const std::function<void( PCB_MARKER* )>& )
362 {
363 if( aItem->GetErrorCode() == DRCE_NET_CHAIN_STUB_TOO_LONG )
364 stubMessages.push_back( aItem->GetErrorMessage( false ) );
365 } );
366
367 drcEngine->RunTests( EDA_UNITS::MM, true, false );
368
369 auto matchesNet = [&]( const wxString& netName )
370 {
371 for( const wxString& msg : stubMessages )
372 {
373 if( msg.Contains( wxString::Format( wxS( "'%s'" ), netName ) ) )
374 return true;
375 }
376
377 return false;
378 };
379
380 BOOST_CHECK_MESSAGE( matchesNet( netB->GetNetname() ),
381 "Stub net MID_B with 2 mm route + 10 mm pad-to-die must violate "
382 "the 5 mm stub_length budget" );
383
384 std::error_code ec;
385 fs::remove( druPath, ec );
386}
387
388
389BOOST_AUTO_TEST_CASE( StubLengthAcceptsTimeDomainUnits )
390{
391 // Regression for H-5: stub_length used to be missing from the
392 // allowsTimeDomain allow-list in DRC_RULES_PARSER, so a (max 100ps)
393 // value would be rejected with "Time based units not allowed for
394 // constraint type." The rule must parse cleanly and the resulting
395 // constraint must be flagged as TIME_DOMAIN.
396 const wxString DRU_TIME = wxS(
397 "(version 1)\n"
398 "(rule \"StubBudgetTime\"\n"
399 " (condition \"A.NetClass == 'Default'\")\n"
400 " (constraint stub_length (max 100ps))\n"
401 ")\n" );
402
403 DRC_RULES_PARSER parser( DRU_TIME, wxS( "stub_length_time_test" ) );
404
406 std::vector<std::shared_ptr<DRC_RULE>> rules;
407
408 parser.Parse( rules, &reporter );
409
410 BOOST_CHECK_MESSAGE( !reporter.HasMessageOfSeverity( RPT_SEVERITY_ERROR ),
411 "stub_length with ps units must parse without error, got: "
412 << reporter.GetMessages().ToStdString() );
413
414 BOOST_REQUIRE_EQUAL( rules.size(), 1u );
415
416 std::optional<DRC_CONSTRAINT> stubConstraint =
417 rules.front()->FindConstraint( NET_CHAIN_STUB_LENGTH_CONSTRAINT );
418
419 BOOST_REQUIRE( stubConstraint.has_value() );
420 BOOST_CHECK( stubConstraint->GetOption( DRC_CONSTRAINT::OPTIONS::TIME_DOMAIN ) );
421 BOOST_CHECK( !stubConstraint->GetOption( DRC_CONSTRAINT::OPTIONS::SPACE_DOMAIN ) );
422 BOOST_REQUIRE( stubConstraint->GetValue().HasMax() );
423 // 1 ps == 1e6 IU (internal time unit is the attosecond).
424 BOOST_CHECK_EQUAL( stubConstraint->GetValue().Max(), 100 * 1000 * 1000 );
425}
426
427
428BOOST_AUTO_TEST_CASE( ReturnPathDoesNotCarryTimeDomainOption )
429{
430 // return_path takes a layer name, not a delay, so the TIME_DOMAIN option
431 // must never be set on a return_path constraint regardless of stray
432 // (max ...) subtrees. The return_path branch in the parser ignores any
433 // unknown sub-options (it only consumes (layer ...)), so we verify the
434 // resulting constraint stays in space-domain configuration.
435 const wxString DRU_LAYER = wxS(
436 "(version 1)\n"
437 "(rule \"GoodReturnPath\"\n"
438 " (condition \"A.NetClass == 'Default'\")\n"
439 " (constraint return_path (layer \"B.Cu\"))\n"
440 ")\n" );
441
442 DRC_RULES_PARSER parser( DRU_LAYER, wxS( "return_path_layer_test" ) );
443
445 std::vector<std::shared_ptr<DRC_RULE>> rules;
446
447 parser.Parse( rules, &reporter );
448
449 BOOST_CHECK_MESSAGE( !reporter.HasMessageOfSeverity( RPT_SEVERITY_ERROR ),
450 "return_path with a layer must parse without error, got: "
451 << reporter.GetMessages().ToStdString() );
452
453 BOOST_REQUIRE_EQUAL( rules.size(), 1u );
454
455 std::optional<DRC_CONSTRAINT> rpConstraint =
456 rules.front()->FindConstraint( NET_CHAIN_RETURN_PATH_CONSTRAINT );
457
458 BOOST_REQUIRE( rpConstraint.has_value() );
459 BOOST_CHECK( !rpConstraint->GetOption( DRC_CONSTRAINT::OPTIONS::TIME_DOMAIN ) );
460}
461
462
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:128
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)
const std::filesystem::path & Path() const
Get the path to the temporary directory as a std::filesystem::path.
Definition file_utils.h:61
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:577
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
A wrapper for reporting to a wxString object.
Definition reporter.h:242
@ NET_CHAIN_STUB_LENGTH_CONSTRAINT
@ NET_CHAIN_RETURN_PATH_CONSTRAINT
@ DRCE_UNCONNECTED_ITEMS
Definition drc_item.h:48
@ DRCE_LIB_FOOTPRINT_ISSUES
Definition drc_item.h:96
@ DRCE_INVALID_OUTLINE
Definition drc_item.h:86
@ DRCE_NET_CHAIN_STUB_TOO_LONG
Definition drc_item.h:119
@ DRCE_DRILL_OUT_OF_RANGE
Definition drc_item.h:69
@ DRCE_DANGLING_VIA
Definition drc_item.h:59
@ DRCE_LIB_FOOTPRINT_MISMATCH
Definition drc_item.h:97
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:708