KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_drc_chain_topology.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, you may find one here:
18 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
19 */
20
21#include <boost/test/unit_test.hpp>
22
23#include <filesystem>
24#include <fstream>
25#include <set>
26
27#include <board.h>
30#include <footprint.h>
31#include <netinfo.h>
32#include <pad.h>
33#include <pcb_track.h>
35
36
37namespace
38{
39
40// Header that produces a minimal layer set sufficient for the test fixtures.
41constexpr const char* PCB_HEADER = R"(
42(kicad_pcb
43 (version 20250904)
44 (generator "pcbnew")
45 (generator_version "9.99")
46 (layers
47 (0 "F.Cu" signal)
48 (2 "B.Cu" signal)
49 (44 "Edge.Cuts" user)
50 )
51)";
52
53constexpr int MM = 1000000;
54
55
56std::unique_ptr<BOARD> loadFromString( const std::string& aPcbText,
57 const std::string& aSubdir )
58{
59 namespace fs = std::filesystem;
60 fs::path tmpDir = fs::temp_directory_path() / aSubdir;
61 fs::create_directories( tmpDir );
62 fs::path pcbPath = tmpDir / "topo.kicad_pcb";
63
64 {
65 std::ofstream out( pcbPath );
66 out << aPcbText;
67 }
68
69 PCB_IO_KICAD_SEXPR plugin;
70 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
71 plugin.LoadBoard( pcbPath.string(), board.get() );
72 board->BuildConnectivity();
73
74 fs::remove( pcbPath );
75 return board;
76}
77
78
79std::set<BOARD_CONNECTED_ITEM*> chainItems( BOARD* aBoard, const wxString& aChain )
80{
81 std::set<BOARD_CONNECTED_ITEM*> items;
82
83 for( PCB_TRACK* t : aBoard->Tracks() )
84 {
85 if( t->GetNet() && t->GetNet()->GetNetChain() == aChain )
86 items.insert( t );
87 }
88
89 for( FOOTPRINT* fp : aBoard->Footprints() )
90 {
91 for( PAD* p : fp->Pads() )
92 {
93 if( p->GetNet() && p->GetNet()->GetNetChain() == aChain )
94 items.insert( p );
95 }
96 }
97
98 return items;
99}
100
101
102// Tag every net whose name starts with "/NET_" into the named chain, and set
103// terminal pads to the first/last footprint's first pad on that chain. fp1
104// pad-1 is terminal[0], lastFp pad-1 is terminal[1].
105void tagChain( BOARD* aBoard, const wxString& aChain, const wxString& aFirstFpRef,
106 const wxString& aLastFpRef )
107{
108 for( NETINFO_ITEM* n : aBoard->GetNetInfo() )
109 {
110 if( n && n->GetNetname().StartsWith( wxS( "/NET_" ) ) )
111 n->SetNetChain( aChain );
112 }
113
114 PAD* termA = nullptr;
115 PAD* termB = nullptr;
116
117 for( FOOTPRINT* fp : aBoard->Footprints() )
118 {
119 if( fp->GetReference() == aFirstFpRef && !fp->Pads().empty() )
120 termA = fp->Pads().front();
121
122 if( fp->GetReference() == aLastFpRef && !fp->Pads().empty() )
123 termB = fp->Pads().back();
124 }
125
126 if( termA )
127 {
128 for( NETINFO_ITEM* n : aBoard->GetNetInfo() )
129 if( n && n->GetNetChain() == aChain )
130 n->SetTerminalPad( 0, termA );
131 }
132
133 if( termB )
134 {
135 for( NETINFO_ITEM* n : aBoard->GetNetInfo() )
136 if( n && n->GetNetChain() == aChain )
137 n->SetTerminalPad( 1, termB );
138 }
139}
140
141
142// Footprint stamper for SMD passives. Each pad on its own net. Pad-net
143// references use the (net N "name") format the modern parser expects.
144std::string passive( const wxString& aRef, double aXmm, double aYmm,
145 double aPadSpanMm,
146 int aNetA, const wxString& aNetAName,
147 int aNetB, const wxString& aNetBName )
148{
149 return wxString::Format(
150 R"(
151 (footprint "Passive" (layer "F.Cu") (uuid "00000000-0000-0000-0000-000000%06d")
152 (at %f %f)
153 (property "Reference" "%s" (at 0 -1) (layer "F.Cu") (hide yes) (uuid "00000000-0000-0000-0000-000000%06da") (effects (font (size 1 1) (thickness 0.15))))
154 (pad "1" smd rect (at %f 0) (size 0.8 0.8) (layers "F.Cu") (net %d "%s") (uuid "00000000-0000-0000-0000-000000%06d1"))
155 (pad "2" smd rect (at %f 0) (size 0.8 0.8) (layers "F.Cu") (net %d "%s") (uuid "00000000-0000-0000-0000-000000%06d2"))
156 )
157)", static_cast<int>( aXmm ), aXmm, aYmm, aRef, static_cast<int>( aXmm ),
158 -aPadSpanMm / 2, aNetA, aNetAName, static_cast<int>( aXmm ),
159 aPadSpanMm / 2, aNetB, aNetBName, static_cast<int>( aXmm ) ).ToStdString();
160}
161
162
163std::string segment( double aX1mm, double aY1mm, double aX2mm, double aY2mm, int aNet )
164{
165 return wxString::Format(
166 " (segment (start %f %f) (end %f %f) (width 0.2) (layer \"F.Cu\") (net %d))\n",
167 aX1mm, aY1mm, aX2mm, aY2mm, aNet ).ToStdString();
168}
169
170} // namespace
171
172
173BOOST_AUTO_TEST_SUITE( DRCChainTopology )
174
175
176// Three-net trunk through two passives.
177// pad@(0,0) — track 30mm — bridge 5mm — track 30mm — bridge 5mm — track 30mm — pad@(100,0).
178// Trunk = 100 mm, zero stubs.
179BOOST_AUTO_TEST_CASE( TopologyTreeOnSimpleTrunk )
180{
181 std::string pcb = std::string( PCB_HEADER ) +
182 R"(
183 (net 0 "")
184 (net 1 "/NET_A")
185 (net 2 "/NET_B")
186 (net 3 "/NET_C")
187 (gr_line (start -5 -5) (end 110 -5) (layer "Edge.Cuts") (width 0.05))
188 (gr_line (start 110 -5) (end 110 5) (layer "Edge.Cuts") (width 0.05))
189 (gr_line (start 110 5) (end -5 5) (layer "Edge.Cuts") (width 0.05))
190 (gr_line (start -5 5) (end -5 -5) (layer "Edge.Cuts") (width 0.05))
191)" +
192 passive( wxS( "FP_START" ), 0.0, 0.0, 0.0,
193 1, wxS( "/NET_A" ), 1, wxS( "/NET_A" ) ) +
194 passive( wxS( "R1" ), 32.5, 0.0, 5.0,
195 1, wxS( "/NET_A" ), 2, wxS( "/NET_B" ) ) +
196 passive( wxS( "R2" ), 67.5, 0.0, 5.0,
197 2, wxS( "/NET_B" ), 3, wxS( "/NET_C" ) ) +
198 passive( wxS( "FP_END" ), 100.0, 0.0, 0.0,
199 3, wxS( "/NET_C" ), 3, wxS( "/NET_C" ) ) +
200 segment( 0.0, 0.0, 30.0, 0.0, 1 ) +
201 segment( 35.0, 0.0, 65.0, 0.0, 2 ) +
202 segment( 70.0, 0.0, 100.0, 0.0, 3 ) +
203 "\n)";
204
205 auto board = loadFromString( pcb, "kicad_drc_topo_simple_trunk" );
206 tagChain( board.get(), wxS( "SIG" ), wxS( "FP_START" ), wxS( "FP_END" ) );
207
208 auto items = chainItems( board.get(), wxS( "SIG" ) );
209 CHAIN_TOPOLOGY topo( board.get(), wxS( "SIG" ), items );
210
211 BOOST_CHECK_EQUAL( static_cast<int>( topo.GetStatus() ),
212 static_cast<int>( CHAIN_TOPOLOGY::STATUS::OK ) );
213 BOOST_CHECK( topo.IsValid() );
214 BOOST_CHECK_CLOSE( topo.TrunkLength(), 100.0 * MM, 5.0 );
215 BOOST_CHECK( topo.Stubs().empty() );
216}
217
218
219// Single-net trunk plus a perpendicular T-stub of 5 mm at the midpoint.
220BOOST_AUTO_TEST_CASE( TopologyDetectsTStub )
221{
222 std::string pcb = std::string( PCB_HEADER ) +
223 R"(
224 (net 0 "")
225 (net 1 "/NET_A")
226 (gr_line (start -5 -10) (end 60 -10) (layer "Edge.Cuts") (width 0.05))
227 (gr_line (start 60 -10) (end 60 15) (layer "Edge.Cuts") (width 0.05))
228 (gr_line (start 60 15) (end -5 15) (layer "Edge.Cuts") (width 0.05))
229 (gr_line (start -5 15) (end -5 -10) (layer "Edge.Cuts") (width 0.05))
230)" +
231 passive( wxS( "FP_START" ), 0.0, 0.0, 0.0,
232 1, wxS( "/NET_A" ), 1, wxS( "/NET_A" ) ) +
233 passive( wxS( "FP_END" ), 50.0, 0.0, 0.0,
234 1, wxS( "/NET_A" ), 1, wxS( "/NET_A" ) ) +
235 // Trunk 0..50 along y=0; stub branches off at x=25 going to (25,5).
236 segment( 0.0, 0.0, 50.0, 0.0, 1 ) +
237 segment( 25.0, 0.0, 25.0, 5.0, 1 ) +
238 "\n)";
239
240 auto board = loadFromString( pcb, "kicad_drc_topo_t_stub" );
241 tagChain( board.get(), wxS( "TSIG" ), wxS( "FP_START" ), wxS( "FP_END" ) );
242
243 auto items = chainItems( board.get(), wxS( "TSIG" ) );
244 CHAIN_TOPOLOGY topo( board.get(), wxS( "TSIG" ), items );
245
246 BOOST_CHECK( topo.IsValid() );
247 BOOST_REQUIRE_EQUAL( topo.Stubs().size(), 1u );
248
249 const CHAIN_TOPOLOGY::STUB& stub = topo.Stubs().front();
250 BOOST_CHECK_LE( std::abs( stub.branchPoint.x - 25 * MM ), 100 );
251 BOOST_CHECK_LE( std::abs( stub.branchPoint.y ), 100 );
252 BOOST_CHECK_CLOSE( stub.length, 5.0 * MM, 5.0 );
253
254 // Trunk length is 50 mm and the stub does not contribute.
255 BOOST_CHECK_CLOSE( topo.TrunkLength(), 50.0 * MM, 5.0 );
256}
257
258
259// Only one terminal pad set: NO_TERMINAL_PADS.
260BOOST_AUTO_TEST_CASE( TopologyMissingTerminalPad )
261{
262 std::string pcb = std::string( PCB_HEADER ) +
263 R"(
264 (net 0 "")
265 (net 1 "/NET_A")
266 (gr_line (start -5 -5) (end 40 -5) (layer "Edge.Cuts") (width 0.05))
267 (gr_line (start 40 -5) (end 40 5) (layer "Edge.Cuts") (width 0.05))
268 (gr_line (start 40 5) (end -5 5) (layer "Edge.Cuts") (width 0.05))
269 (gr_line (start -5 5) (end -5 -5) (layer "Edge.Cuts") (width 0.05))
270)" +
271 passive( wxS( "FP_START" ), 0.0, 0.0, 0.0,
272 1, wxS( "/NET_A" ), 1, wxS( "/NET_A" ) ) +
273 segment( 0.0, 0.0, 30.0, 0.0, 1 ) +
274 "\n)";
275
276 auto board = loadFromString( pcb, "kicad_drc_topo_missing_term" );
277 // Only set chain — no second terminal-pad anchor footprint.
278 for( NETINFO_ITEM* n : board->GetNetInfo() )
279 {
280 if( n && n->GetNetname().StartsWith( wxS( "/NET_" ) ) )
281 n->SetNetChain( wxS( "MISSING" ) );
282 }
284 PAD* termA = board->Footprints().empty()
285 ? nullptr
286 : board->Footprints().front()->Pads().empty()
287 ? nullptr
288 : board->Footprints().front()->Pads().front();
289
290 if( termA )
291 {
292 for( NETINFO_ITEM* n : board->GetNetInfo() )
293 if( n && n->GetNetChain() == wxS( "MISSING" ) )
294 n->SetTerminalPad( 0, termA );
295 }
296
297 auto items = chainItems( board.get(), wxS( "MISSING" ) );
298 CHAIN_TOPOLOGY topo( board.get(), wxS( "MISSING" ), items );
299
300 BOOST_CHECK_EQUAL( static_cast<int>( topo.GetStatus() ),
301 static_cast<int>( CHAIN_TOPOLOGY::STATUS::NO_TERMINAL_PADS ) );
302}
303
304
305// Both terminals set, no track between them: DISCONNECTED.
306BOOST_AUTO_TEST_CASE( TopologyDisconnected )
307{
308 std::string pcb = std::string( PCB_HEADER ) +
309 R"(
310 (net 0 "")
311 (net 1 "/NET_A")
312 (gr_line (start -5 -5) (end 110 -5) (layer "Edge.Cuts") (width 0.05))
313 (gr_line (start 110 -5) (end 110 5) (layer "Edge.Cuts") (width 0.05))
314 (gr_line (start 110 5) (end -5 5) (layer "Edge.Cuts") (width 0.05))
315 (gr_line (start -5 5) (end -5 -5) (layer "Edge.Cuts") (width 0.05))
316)" +
317 passive( wxS( "FP_START" ), 0.0, 0.0, 0.0,
318 1, wxS( "/NET_A" ), 1, wxS( "/NET_A" ) ) +
319 passive( wxS( "FP_END" ), 100.0, 0.0, 0.0,
320 1, wxS( "/NET_A" ), 1, wxS( "/NET_A" ) ) +
321 "\n)";
322
323 auto board = loadFromString( pcb, "kicad_drc_topo_disconnected" );
324 tagChain( board.get(), wxS( "DISC" ), wxS( "FP_START" ), wxS( "FP_END" ) );
325
326 auto items = chainItems( board.get(), wxS( "DISC" ) );
327 CHAIN_TOPOLOGY topo( board.get(), wxS( "DISC" ), items );
328
329 BOOST_CHECK_EQUAL( static_cast<int>( topo.GetStatus() ),
330 static_cast<int>( CHAIN_TOPOLOGY::STATUS::DISCONNECTED ) );
331}
332
333
334// Two parallel paths between terminals → CYCLE_DETECTED.
335BOOST_AUTO_TEST_CASE( TopologyCycleDetected )
336{
337 std::string pcb = std::string( PCB_HEADER ) +
338 R"(
339 (net 0 "")
340 (net 1 "/NET_A")
341 (gr_line (start -5 -10) (end 60 -10) (layer "Edge.Cuts") (width 0.05))
342 (gr_line (start 60 -10) (end 60 10) (layer "Edge.Cuts") (width 0.05))
343 (gr_line (start 60 10) (end -5 10) (layer "Edge.Cuts") (width 0.05))
344 (gr_line (start -5 10) (end -5 -10) (layer "Edge.Cuts") (width 0.05))
345)" +
346 passive( wxS( "FP_START" ), 0.0, 0.0, 0.0,
347 1, wxS( "/NET_A" ), 1, wxS( "/NET_A" ) ) +
348 passive( wxS( "FP_END" ), 50.0, 0.0, 0.0,
349 1, wxS( "/NET_A" ), 1, wxS( "/NET_A" ) ) +
350 // Two routes start→end forming a loop.
351 segment( 0.0, 0.0, 25.0, 5.0, 1 ) +
352 segment( 25.0, 5.0, 50.0, 0.0, 1 ) +
353 segment( 0.0, 0.0, 25.0, -5.0, 1 ) +
354 segment( 25.0, -5.0, 50.0, 0.0, 1 ) +
355 "\n)";
356
357 auto board = loadFromString( pcb, "kicad_drc_topo_cycle" );
358 tagChain( board.get(), wxS( "LOOP" ), wxS( "FP_START" ), wxS( "FP_END" ) );
359
360 auto items = chainItems( board.get(), wxS( "LOOP" ) );
361 CHAIN_TOPOLOGY topo( board.get(), wxS( "LOOP" ), items );
362
363 BOOST_CHECK_EQUAL( static_cast<int>( topo.GetStatus() ),
364 static_cast<int>( CHAIN_TOPOLOGY::STATUS::CYCLE_DETECTED ) );
365}
366
367
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:323
const NETINFO_LIST & GetNetInfo() const
Definition board.h:1004
const FOOTPRINTS & Footprints() const
Definition board.h:364
const TRACKS & Tracks() const
Definition board.h:362
Build a topological view of a single named net chain's routed copper.
const std::vector< STUB > & Stubs() const
STATUS GetStatus() const
double TrunkLength() const
bool IsValid() const
Handle the data for a net.
Definition netinfo.h:50
Definition pad.h:55
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 ...
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:400
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_AUTO_TEST_CASE(TopologyTreeOnSimpleTrunk)
BOOST_AUTO_TEST_SUITE_END()
static const long long MM
BOOST_CHECK_EQUAL(result, "25.4")