KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_pads_binary_import.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) 2026 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 <memory>
24
33#include <io/pads/pads_common.h>
34#include <layer_ids.h>
35#include <padstack.h>
36#include <board.h>
37#include <pcb_text.h>
38#include <pcb_shape.h>
39#include <pcb_field.h>
40#include <pad.h>
41#include <pcb_track.h>
42#include <pcb_group.h>
43#include <pcb_dimension.h>
44#include <footprint.h>
45#include <netinfo.h>
46#include <zone.h>
49#include <netclass.h>
51#include <algorithm>
52#include <array>
53#include <chrono>
54#include <cstdlib>
55#include <filesystem>
56#include <fstream>
57#include <functional>
58#include <iomanip>
59#include <map>
60#include <optional>
61#include <set>
62#include <sstream>
63#include <tuple>
64#include <utility>
65#include <vector>
66
67#include <wx/filename.h>
68#include <wx/ffile.h>
69
70
72{
73 std::string dir;
74 std::string binaryFile;
75 std::string ascFile;
77};
78
79
81 { "TMS1mmX19", "TMS1mmX19.pcb", "TMS1mmX19.asc", false },
82 { "MC4_PLUS_CSHAPE", "MC4_PLUS_CSHAPE.pcb", "MC4_PLUS_CSHAPE.asc", false },
83 { "MC2_PLUS_REV1", "MC2_PLUS_REV1.pcb", "MC2_PLUS_REV1.asc", true },
84 { "Ems4_Rev2", "Ems4_Rev2.pcb", "Ems4_Rev2.asc", false },
85 { "LCORE_4", "LCORE_4.pcb", "LCORE_4.asc", false },
86 { "LCORE_2", "LCORE_2.pcb", "LCORE_2.asc", false },
87 { "Dexter_MotorCtrl", "Dexter_MotorCtrl.pcb", "Dexter_MotorCtrl.asc", true },
88 { "MAIS_FC", "MAIS_FC.pcb", "MAIS_FC.asc", true },
89};
90
91
92static wxString GetBinaryPath( const PADS_BINARY_BOARD_INFO& aBoard )
93{
94 return KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/" + aBoard.dir + "/" + aBoard.binaryFile;
95}
96
97
98static wxString GetAscPath( const PADS_BINARY_BOARD_INFO& aBoard )
99{
100 return KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/" + aBoard.dir + "/" + aBoard.ascFile;
101}
102
103
108// Boards are loaded by name from a handful of files, and several tests reload the same 37 MB
109// design. Share one parse per file and stage binary inputs because importing saved design rules
110// beside the source file would otherwise modify corpus fixtures.
111static std::shared_ptr<BOARD> CachedLoad( const wxString& aFilename, bool aBinary )
112{
113 static std::map<std::string, std::shared_ptr<BOARD>> cache;
114
115 std::string key = ( aBinary ? "B:" : "A:" ) + aFilename.ToStdString();
116 auto it = cache.find( key );
117
118 if( it != cache.end() )
119 return it->second;
120
121 std::shared_ptr<BOARD> board;
122
123 try
124 {
125 if( aBinary )
126 {
127 static const std::filesystem::path stagedRoot =
128 std::filesystem::temp_directory_path()
129 / ( "kicad_pads_binary_cached_"
130 + std::to_string( std::chrono::steady_clock::now().time_since_epoch().count() ) );
131 const std::filesystem::path source( aFilename.ToStdString() );
132 const std::filesystem::path stagedDirectory =
133 stagedRoot / std::to_string( std::hash<std::string>{}( key ) );
134 const std::filesystem::path stagedPath = stagedDirectory / source.filename();
135
136 std::filesystem::create_directories( stagedDirectory );
137 std::filesystem::copy_file( source, stagedPath, std::filesystem::copy_options::overwrite_existing );
138 std::filesystem::permissions( stagedPath, std::filesystem::perms::owner_write,
139 std::filesystem::perm_options::add );
140
141 PCB_IO_PADS_BINARY plugin;
142 board.reset( plugin.LoadBoard( wxString::FromUTF8( stagedPath.string() ), nullptr ).release() );
143 }
144 else
145 {
146 PCB_IO_PADS plugin;
147 board.reset( plugin.LoadBoard( aFilename, nullptr ).release() );
148 }
149 }
150 catch( const std::exception& )
151 {
152 board.reset();
153 cache[key] = board;
154 throw;
155 }
156
157 cache[key] = board;
158 return board;
159}
160
161
162static std::shared_ptr<BOARD> LoadBinary( const PADS_BINARY_BOARD_INFO& aBoard )
163{
164 PCB_IO_PADS_BINARY plugin;
165 wxString filename = GetBinaryPath( aBoard );
166
167 BOOST_CHECK_MESSAGE( plugin.CanReadBoard( filename ),
168 aBoard.dir << " binary should be readable by PCB_IO_PADS_BINARY" );
169
170 std::shared_ptr<BOARD> board;
171
172 try
173 {
174 board = CachedLoad( filename, true );
175 }
176 catch( const std::exception& e )
177 {
178 BOOST_FAIL( aBoard.dir << " binary threw exception during load: " << e.what() );
179 }
180
181 BOOST_REQUIRE_MESSAGE( board != nullptr, aBoard.dir << " binary failed to load" );
182 return board;
183}
184
185
186static std::shared_ptr<BOARD> LoadBinaryPath( const wxString& aFilename, const std::string& aLabel )
187{
188 PCB_IO_PADS_BINARY plugin;
189
190 BOOST_CHECK_MESSAGE( plugin.CanReadBoard( aFilename ),
191 aLabel << " binary should be readable by PCB_IO_PADS_BINARY" );
192
193 std::shared_ptr<BOARD> board;
194
195 try
196 {
197 board = CachedLoad( aFilename, true );
198 }
199 catch( const std::exception& e )
200 {
201 BOOST_FAIL( aLabel << " binary threw exception during load: " << e.what() );
202 return nullptr;
203 }
204
205 BOOST_REQUIRE_MESSAGE( board != nullptr, aLabel << " binary failed to load" );
206 return board;
207}
208
209
210static std::shared_ptr<BOARD> LoadAsc( const PADS_BINARY_BOARD_INFO& aBoard )
211{
212 PCB_IO_PADS plugin;
213 wxString filename = GetAscPath( aBoard );
214
215 std::shared_ptr<BOARD> board;
216
217 try
218 {
219 board = CachedLoad( filename, false );
220 }
221 catch( const std::exception& e )
222 {
223 BOOST_FAIL( aBoard.dir << " ASC threw exception during load: " << e.what() );
224 return nullptr;
225 }
226
227 BOOST_REQUIRE_MESSAGE( board != nullptr, aBoard.dir << " ASC failed to load" );
228 return board;
229}
230
231
232static std::shared_ptr<BOARD> LoadAscPath( const wxString& aFilename, const std::string& aLabel )
233{
234 std::shared_ptr<BOARD> board;
235
236 try
237 {
238 board = CachedLoad( aFilename, false );
239 }
240 catch( const std::exception& e )
241 {
242 BOOST_FAIL( aLabel << " ASC threw exception during load: " << e.what() );
243 return nullptr;
244 }
245
246 BOOST_REQUIRE_MESSAGE( board != nullptr, aLabel << " ASC failed to load" );
247 return board;
248}
249
250
251static std::pair<size_t, size_t> ExactLayeredTrackMatches( const wxString& aBinaryPath, const wxString& aAsciiPath,
252 const std::string& aLabel )
253{
254 auto binary = LoadBinaryPath( aBinaryPath, aLabel );
255 auto ascii = LoadAscPath( aAsciiPath, aLabel );
256
257 auto viaAnchor = []( const BOARD* aBoard ) -> std::optional<VECTOR2I>
258 {
259 std::optional<VECTOR2I> anchor;
260
261 for( PCB_TRACK* item : aBoard->Tracks() )
262 {
263 if( item->Type() != PCB_VIA_T )
264 continue;
265
266 VECTOR2I position = item->GetPosition();
267
268 if( !anchor || std::tie( position.x, position.y ) < std::tie( anchor->x, anchor->y ) )
269 {
270 anchor = position;
271 }
272 }
273
274 return anchor;
275 };
276
277 std::optional<VECTOR2I> binaryAnchor = viaAnchor( binary.get() );
278 std::optional<VECTOR2I> asciiAnchor = viaAnchor( ascii.get() );
279
280 if( binaryAnchor && asciiAnchor )
281 binary->Move( *asciiAnchor - *binaryAnchor );
282
283 using TRACK_GEOMETRY = std::tuple<int, int, int, int, int, int>;
284
285 auto trackGeometry = []( const BOARD* aBoard )
286 {
287 std::set<TRACK_GEOMETRY> geometry;
288
289 for( PCB_TRACK* track : aBoard->Tracks() )
290 {
291 if( track->Type() != PCB_TRACE_T )
292 continue;
293
294 VECTOR2I start = track->GetStart();
295 VECTOR2I end = track->GetEnd();
296
297 if( std::tie( end.x, end.y ) < std::tie( start.x, start.y ) )
298 std::swap( start, end );
299
300 geometry.emplace( static_cast<int>( track->GetLayer() ), start.x, start.y, end.x, end.y,
301 track->GetWidth() );
302 }
303
304 return geometry;
305 };
306
307 const std::set<TRACK_GEOMETRY> binaryTracks = trackGeometry( binary.get() );
308 const std::set<TRACK_GEOMETRY> asciiTracks = trackGeometry( ascii.get() );
309 std::vector<TRACK_GEOMETRY> matched;
310
311 std::set_intersection( binaryTracks.begin(), binaryTracks.end(), asciiTracks.begin(), asciiTracks.end(),
312 std::back_inserter( matched ) );
313
314 return { matched.size(), asciiTracks.size() };
315}
316
317
318static std::pair<size_t, size_t> ExactNettedTrackMatches( const wxString& aBinaryPath, const wxString& aAsciiPath,
319 const std::string& aLabel )
320{
321 auto binary = LoadBinaryPath( aBinaryPath, aLabel );
322 auto ascii = LoadAscPath( aAsciiPath, aLabel );
323
324 auto viaAnchor = []( const BOARD* aBoard ) -> std::optional<VECTOR2I>
325 {
326 std::optional<VECTOR2I> anchor;
327
328 for( PCB_TRACK* item : aBoard->Tracks() )
329 {
330 if( item->Type() != PCB_VIA_T )
331 continue;
332
333 VECTOR2I position = item->GetPosition();
334
335 if( !anchor || std::tie( position.x, position.y ) < std::tie( anchor->x, anchor->y ) )
336 anchor = position;
337 }
338
339 return anchor;
340 };
341
342 std::optional<VECTOR2I> binaryAnchor = viaAnchor( binary.get() );
343 std::optional<VECTOR2I> asciiAnchor = viaAnchor( ascii.get() );
344
345 if( binaryAnchor && asciiAnchor )
346 binary->Move( *asciiAnchor - *binaryAnchor );
347
348 using NETTED_TRACK = std::tuple<int, int, int, int, int, int, wxString>;
349
350 auto tracks = []( const BOARD* aBoard )
351 {
352 std::set<NETTED_TRACK> rows;
353
354 for( PCB_TRACK* track : aBoard->Tracks() )
355 {
356 if( track->Type() != PCB_TRACE_T )
357 continue;
358
359 VECTOR2I start = track->GetStart();
360 VECTOR2I end = track->GetEnd();
361
362 if( std::tie( end.x, end.y ) < std::tie( start.x, start.y ) )
363 std::swap( start, end );
364
365 rows.emplace( static_cast<int>( track->GetLayer() ), start.x, start.y, end.x, end.y, track->GetWidth(),
366 track->GetNetname() );
367 }
368
369 return rows;
370 };
371
372 const std::set<NETTED_TRACK> binaryTracks = tracks( binary.get() );
373 const std::set<NETTED_TRACK> asciiTracks = tracks( ascii.get() );
374 std::vector<NETTED_TRACK> matched;
375
376 std::set_intersection( binaryTracks.begin(), binaryTracks.end(), asciiTracks.begin(), asciiTracks.end(),
377 std::back_inserter( matched ) );
378
379 return { matched.size(), asciiTracks.size() };
380}
381
382
383static std::pair<size_t, size_t> ExactViaMatches( const wxString& aBinaryPath, const wxString& aAsciiPath,
384 const std::string& aLabel )
385{
386 auto binary = LoadBinaryPath( aBinaryPath, aLabel );
387 auto ascii = LoadAscPath( aAsciiPath, aLabel );
388
389 auto viaAnchor = []( const BOARD* aBoard ) -> std::optional<VECTOR2I>
390 {
391 std::optional<VECTOR2I> anchor;
392
393 for( PCB_TRACK* item : aBoard->Tracks() )
394 {
395 if( item->Type() != PCB_VIA_T )
396 continue;
397
398 VECTOR2I position = item->GetPosition();
399
400 if( !anchor || std::tie( position.x, position.y ) < std::tie( anchor->x, anchor->y ) )
401 anchor = position;
402 }
403
404 return anchor;
405 };
406
407 std::optional<VECTOR2I> binaryAnchor = viaAnchor( binary.get() );
408 std::optional<VECTOR2I> asciiAnchor = viaAnchor( ascii.get() );
409
410 if( binaryAnchor && asciiAnchor )
411 binary->Move( *asciiAnchor - *binaryAnchor );
412
413 using VIA_GEOMETRY = std::tuple<wxString, int, int, int, int, int, int, int>;
414
415 auto vias = []( const BOARD* aBoard )
416 {
417 std::set<VIA_GEOMETRY> rows;
418
419 for( PCB_TRACK* item : aBoard->Tracks() )
420 {
421 PCB_VIA* via = dynamic_cast<PCB_VIA*>( item );
422
423 if( !via )
424 continue;
425
426 rows.emplace( via->GetNetname(), via->GetPosition().x, via->GetPosition().y,
427 via->GetWidth( via->TopLayer() ), via->GetDrillValue(), static_cast<int>( via->GetViaType() ),
428 static_cast<int>( via->TopLayer() ), static_cast<int>( via->BottomLayer() ) );
429 }
430
431 return rows;
432 };
433
434 const std::set<VIA_GEOMETRY> binaryVias = vias( binary.get() );
435 const std::set<VIA_GEOMETRY> asciiVias = vias( ascii.get() );
436 std::vector<VIA_GEOMETRY> matched;
437
438 std::set_intersection( binaryVias.begin(), binaryVias.end(), asciiVias.begin(), asciiVias.end(),
439 std::back_inserter( matched ) );
440
441 return { matched.size(), asciiVias.size() };
442}
443
444
445static int CountEdgeCutsShapes( const BOARD* aBoard )
446{
447 int count = 0;
448
449 for( BOARD_ITEM* item : aBoard->Drawings() )
450 {
451 if( PCB_SHAPE* shape = dynamic_cast<PCB_SHAPE*>( item ) )
452 {
453 if( shape->GetLayer() == Edge_Cuts )
454 count++;
455 }
456 }
457
458 return count;
459}
460
461
462static size_t CountVias( const std::shared_ptr<BOARD>& aBoard )
463{
464 size_t vias = 0;
465
466 for( PCB_TRACK* item : aBoard->Tracks() )
467 {
468 if( item->Type() == PCB_VIA_T )
469 ++vias;
470 }
471
472 return vias;
473}
474
475
476static size_t CountTraces( const std::shared_ptr<BOARD>& aBoard )
477{
478 size_t traces = 0;
479
480 for( PCB_TRACK* item : aBoard->Tracks() )
481 {
482 if( item->Type() == PCB_TRACE_T || item->Type() == PCB_ARC_T )
483 ++traces;
484 }
485
486 return traces;
487}
488
489
490static FOOTPRINT* FindFootprintByReference( const BOARD* aBoard, const wxString& aReference )
491{
492 for( FOOTPRINT* fp : aBoard->Footprints() )
493 {
494 if( fp->GetReference() == aReference )
495 return fp;
496 }
497
498 return nullptr;
499}
500
501
502static std::set<wxString> BoardNetNames( const BOARD* aBoard )
503{
504 std::set<wxString> names;
505
506 for( NETINFO_ITEM* net : aBoard->GetNetInfo() )
507 {
508 if( net && !net->GetNetname().IsEmpty() )
509 names.insert( net->GetNetname() );
510 }
511
512 return names;
513}
514
515
516static wxString JoinNetSet( const std::set<wxString>& aNames )
517{
518 wxString joined;
519
520 for( const wxString& name : aNames )
521 {
522 if( !joined.IsEmpty() )
523 joined += wxString( "," );
524
525 joined += name;
526 }
527
528 return joined;
529}
530
531
532static std::vector<std::string> CanonicalGeometryRows( const BOARD* aBoard )
533{
534 std::vector<std::string> rows;
535
536 for( FOOTPRINT* fp : aBoard->Footprints() )
537 {
538 for( PAD* pad : fp->Pads() )
539 {
540 const VECTOR2I drill = pad->GetDrillSize();
541 std::ostringstream row;
542
543 row << "PAD " << std::quoted( fp->GetReference().ToStdString() ) << " "
544 << std::quoted( pad->GetNumber().ToStdString() ) << " " << pad->GetPosition().x << " "
545 << pad->GetPosition().y << " " << pad->GetOrientation().AsTenthsOfADegree() << " "
546 << static_cast<int>( pad->GetAttribute() ) << " " << drill.x << " " << drill.y << " "
547 << static_cast<int>( pad->GetDrillShape() ) << " " << std::quoted( pad->GetLayerSet().FmtHex() ) << " "
548 << std::quoted( pad->GetNetname().ToStdString() );
549
550 for( PCB_LAYER_ID layer : pad->GetLayerSet().Seq() )
551 {
552 const VECTOR2I size = pad->GetSize( layer );
553 const VECTOR2I offset = pad->GetOffset( layer );
554
555 row << " " << static_cast<int>( layer ) << ":" << static_cast<int>( pad->GetShape( layer ) ) << ":"
556 << size.x << ":" << size.y << ":" << offset.x << ":" << offset.y << ":"
557 << pad->GetRoundRectCornerRadius( layer ) << ":" << pad->GetChamferPositions( layer );
558 }
559
560 rows.push_back( row.str() );
561 }
562 }
563
564 for( PCB_TRACK* track : aBoard->Tracks() )
565 {
566 std::ostringstream row;
567
568 if( PCB_VIA* via = dynamic_cast<PCB_VIA*>( track ) )
569 {
570 row << "VIA " << std::quoted( via->GetNetname().ToStdString() ) << " " << via->GetPosition().x << " "
571 << via->GetPosition().y << " " << via->GetWidth( via->TopLayer() ) << " " << via->GetDrillValue() << " "
572 << static_cast<int>( via->GetViaType() ) << " " << static_cast<int>( via->TopLayer() ) << " "
573 << static_cast<int>( via->BottomLayer() );
574 }
575 else if( PCB_ARC* arc = dynamic_cast<PCB_ARC*>( track ) )
576 {
577 row << "ARC " << std::quoted( arc->GetNetname().ToStdString() ) << " "
578 << static_cast<int>( arc->GetLayer() ) << " " << arc->GetStart().x << " " << arc->GetStart().y << " "
579 << arc->GetMid().x << " " << arc->GetMid().y << " " << arc->GetEnd().x << " " << arc->GetEnd().y << " "
580 << arc->GetWidth();
581 }
582 else
583 {
584 VECTOR2I start = track->GetStart();
585 VECTOR2I end = track->GetEnd();
586
587 if( std::tie( end.x, end.y ) < std::tie( start.x, start.y ) )
588 std::swap( start, end );
589
590 row << "TRACK " << std::quoted( track->GetNetname().ToStdString() ) << " "
591 << static_cast<int>( track->GetLayer() ) << " " << start.x << " " << start.y << " " << end.x << " "
592 << end.y << " " << track->GetWidth();
593 }
594
595 rows.push_back( row.str() );
596 }
597
598 std::sort( rows.begin(), rows.end() );
599 return rows;
600}
601
602
608static void CheckCountWithTolerance( const std::string& aLabel, size_t aBinaryCount, size_t aAscCount,
609 bool aDifferentRevision )
610{
611 if( aDifferentRevision )
612 {
613 BOOST_WARN_MESSAGE( aBinaryCount == aAscCount,
614 aLabel << " binary=" << aBinaryCount << " asc=" << aAscCount << " (different revision)" );
615 return;
616 }
617
618 if( aBinaryCount == aAscCount )
619 {
620 BOOST_CHECK_MESSAGE( true, aLabel << " counts match: " << aBinaryCount );
621 return;
622 }
623
624 size_t maxCount = std::max( aBinaryCount, aAscCount );
625 size_t diff = ( aBinaryCount > aAscCount ) ? aBinaryCount - aAscCount : aAscCount - aBinaryCount;
626
627 bool withinTolerance = ( diff <= 2 ) || ( diff * 100 / maxCount <= 5 );
628
629 BOOST_CHECK_MESSAGE( withinTolerance,
630 aLabel << " counts differ beyond tolerance: binary=" << aBinaryCount << " asc=" << aAscCount );
631
632 BOOST_WARN_MESSAGE( aBinaryCount == aAscCount,
633 aLabel << " exact count mismatch: binary=" << aBinaryCount << " asc=" << aAscCount );
634}
635
636
641static void RunStructuralChecks( const PADS_BINARY_BOARD_INFO& aBoard, const BOARD* aBinaryBoard )
642{
643 BOOST_WARN_MESSAGE( aBinaryBoard->Tracks().size() > 0, aBoard.dir << " binary has no tracks" );
644
645 std::set<std::pair<int, int>> viaPositions;
646 bool hasDuplicate = false;
647
648 for( PCB_TRACK* trk : aBinaryBoard->Tracks() )
649 {
650 PCB_VIA* via = dynamic_cast<PCB_VIA*>( trk );
651
652 if( !via || via->GetViaType() != VIATYPE::THROUGH )
653 continue;
654
655 auto key = std::make_pair( via->GetPosition().x, via->GetPosition().y );
656
657 if( viaPositions.count( key ) )
658 {
659 hasDuplicate = true;
660 break;
661 }
662
663 viaPositions.insert( key );
664 }
665
666 BOOST_CHECK_MESSAGE( !hasDuplicate, aBoard.dir << " binary should have no duplicate through-hole vias" );
667
668 for( PCB_TRACK* trk : aBinaryBoard->Tracks() )
669 {
670 if( trk->Type() == PCB_TRACE_T || trk->Type() == PCB_ARC_T )
671 {
672 BOOST_CHECK_MESSAGE( IsCopperLayer( trk->GetLayer() ),
673 aBoard.dir << " binary track on non-copper layer " << trk->GetLayer() );
674 }
675 }
676
677 for( FOOTPRINT* fp : aBinaryBoard->Footprints() )
678 {
679 for( PAD* pad : fp->Pads() )
680 {
681 BOOST_WARN_MESSAGE( pad->GetSize( PADSTACK::ALL_LAYERS ).x > 0
682 && pad->GetSize( PADSTACK::ALL_LAYERS ).y > 0,
683 aBoard.dir << " " << fp->GetReference() << " pad has zero size" );
684 }
685 }
686}
687
688
689BOOST_AUTO_TEST_SUITE( PadsBinaryImport )
690
691
692BOOST_AUTO_TEST_CASE( CanonicalGeometryRowsCoverPadsTracksAndVias )
693{
694 auto board = LoadAsc( PADS_BINARY_BOARDS[3] );
695
696 BOOST_REQUIRE( board );
697
698 const std::vector<std::string> rows = CanonicalGeometryRows( board.get() );
699 size_t pads = 0;
700 size_t tracks = 0;
701 size_t vias = 0;
702
703 for( const std::string& row : rows )
704 {
705 pads += row.rfind( "PAD ", 0 ) == 0;
706 tracks += row.rfind( "TRACK ", 0 ) == 0 || row.rfind( "ARC ", 0 ) == 0;
707 vias += row.rfind( "VIA ", 0 ) == 0;
708 }
709
710 BOOST_CHECK_GT( pads, 0u );
711 BOOST_CHECK_GT( tracks, 0u );
712 BOOST_CHECK_GT( vias, 0u );
713}
714
715
716BOOST_AUTO_TEST_CASE( RouteObjectClassDoesNotDependOnCachedBounds )
717{
718 const wxString source = GetBinaryPath( PADS_BINARY_BOARDS[4] );
719
720 std::vector<uint8_t> bytes;
721 BOOST_REQUIRE( PADS_IO::ReadFileToBuffer( source, bytes ) );
722
724 sdb.Load( bytes );
725
726 const PADS_IO::SDB_SECTION* objects = sdb.Section( 62 );
727 BOOST_REQUIRE( objects != nullptr );
728 BOOST_REQUIRE_GT( objects->physicalCount, 0u );
729 BOOST_REQUIRE( objects->stride == 36 || objects->stride == 48 );
730
731 auto writeRingU32 = [&]( size_t aLogicalOffset, uint32_t aValue )
732 {
733 for( size_t byte = 0; byte < 4; ++byte )
734 {
735 size_t physical = objects->physicalOffset + aLogicalOffset % objects->physicalBytes;
736 bytes.at( physical ) = static_cast<uint8_t>( aValue >> ( byte * 8 ) );
737 ++aLogicalOffset;
738 }
739 };
740
741 const size_t boundLoOffset = objects->stride == 36 ? 12 : 24;
742 const size_t boundHiOffset = objects->stride == 36 ? 16 : 28;
743
744 for( uint32_t index = 0; index < objects->physicalCount; ++index )
745 {
746 const size_t base = 32 + static_cast<size_t>( index ) * objects->stride;
747 writeRingU32( base + boundLoOffset, 0x12345678 );
748 writeRingU32( base + boundHiOffset, 0x76543210 );
749 }
750
751 wxString tempBase = wxFileName::CreateTempFileName( wxS( "kicad_pads_route_bounds_" ) );
752 wxRemoveFile( tempBase );
753 wxString tempPath = tempBase + wxS( ".pcb" );
754
755 {
756 wxFFile file( tempPath, wxS( "wb" ) );
757 BOOST_REQUIRE( file.IsOpened() );
758 BOOST_REQUIRE_EQUAL( file.Write( bytes.data(), bytes.size() ), bytes.size() );
759 }
760
761 using ROUTE_ROW = std::tuple<std::string, int, double, std::vector<std::pair<double, double>>>;
762 auto routeRows = []( const PADS_IO::BINARY_PARSER& aParser )
763 {
764 std::vector<ROUTE_ROW> rows;
765
766 for( const PADS_IO::ROUTE& route : aParser.GetRoutes() )
767 {
768 for( const PADS_IO::TRACK& track : route.tracks )
769 {
770 std::vector<std::pair<double, double>> points;
771
772 for( const PADS_IO::ARC_POINT& point : track.points )
773 points.emplace_back( point.x, point.y );
774
775 rows.emplace_back( route.net_name, track.layer, track.width, std::move( points ) );
776 }
777 }
778
779 return rows;
780 };
781
782 PADS_IO::BINARY_PARSER baseline;
783 PADS_IO::BINARY_PARSER changedBounds;
784 baseline.Parse( source );
785 changedBounds.Parse( tempPath );
786 wxRemoveFile( tempPath );
787
788 BOOST_CHECK( routeRows( changedBounds ) == routeRows( baseline ) );
789}
790
791
792BOOST_AUTO_TEST_CASE( BinaryFileDetection )
793{
794 PCB_IO_PADS_BINARY binaryPlugin;
795 PCB_IO_PADS ascPlugin;
796
797 for( const auto& board : PADS_BINARY_BOARDS )
798 {
799 wxString binaryPath = GetBinaryPath( board );
800
801 BOOST_CHECK_MESSAGE( binaryPlugin.CanReadBoard( binaryPath ),
802 board.dir << " binary should be recognized by PCB_IO_PADS_BINARY" );
803
804 BOOST_CHECK_MESSAGE( !ascPlugin.CanReadBoard( binaryPath ),
805 board.dir << " binary should NOT be recognized by PCB_IO_PADS" );
806 }
807}
808
809
810BOOST_AUTO_TEST_CASE( AsciiFileRejection )
811{
812 PCB_IO_PADS_BINARY binaryPlugin;
813
814 for( const auto& board : PADS_BINARY_BOARDS )
815 {
816 wxString ascPath = GetAscPath( board );
817
818 BOOST_CHECK_MESSAGE( !binaryPlugin.CanReadBoard( ascPath ),
819 board.dir << " ASCII should NOT be recognized by PCB_IO_PADS_BINARY" );
820 }
821}
822
823
824#define BINARY_LOAD_TEST( name, idx ) \
825 BOOST_AUTO_TEST_CASE( BasicLoad_##name ) \
826 { \
827 auto board = LoadBinary( PADS_BINARY_BOARDS[idx] ); \
828 \
829 BOOST_CHECK( board->Footprints().size() > 0 ); \
830 }
831
832BINARY_LOAD_TEST( TMS1mmX19, 0 )
833BINARY_LOAD_TEST( MC4_PLUS_CSHAPE, 1 )
834BINARY_LOAD_TEST( MC2_PLUS_REV1, 2 )
835BINARY_LOAD_TEST( Ems4_Rev2, 3 )
836BINARY_LOAD_TEST( LCORE_4, 4 )
837BINARY_LOAD_TEST( LCORE_2, 5 )
838BINARY_LOAD_TEST( Dexter_MotorCtrl, 6 )
839BINARY_LOAD_TEST( MAIS_FC, 7 )
840
841
842#define FOOTPRINT_COUNT_TEST( name, idx ) \
843 BOOST_AUTO_TEST_CASE( FootprintCount_##name ) \
844 { \
845 auto bin = LoadBinary( PADS_BINARY_BOARDS[idx] ); \
846 \
847 auto asc = LoadAsc( PADS_BINARY_BOARDS[idx] ); \
848 \
849 CheckCountWithTolerance( #name " footprints", bin->Footprints().size(), asc->Footprints().size(), \
850 PADS_BINARY_BOARDS[idx].differentRevision ); \
851 }
852
853FOOTPRINT_COUNT_TEST( TMS1mmX19, 0 )
854FOOTPRINT_COUNT_TEST( MC4_PLUS_CSHAPE, 1 )
855FOOTPRINT_COUNT_TEST( MC2_PLUS_REV1, 2 )
856FOOTPRINT_COUNT_TEST( Ems4_Rev2, 3 )
857FOOTPRINT_COUNT_TEST( LCORE_4, 4 )
858FOOTPRINT_COUNT_TEST( LCORE_2, 5 )
859FOOTPRINT_COUNT_TEST( Dexter_MotorCtrl, 6 )
860FOOTPRINT_COUNT_TEST( MAIS_FC, 7 )
861
862
863#define NET_COUNT_TEST( name, idx ) \
864 BOOST_AUTO_TEST_CASE( NetCount_##name ) \
865 { \
866 auto bin = LoadBinary( PADS_BINARY_BOARDS[idx] ); \
867 \
868 auto asc = LoadAsc( PADS_BINARY_BOARDS[idx] ); \
869 \
870 CheckCountWithTolerance( #name " nets", bin->GetNetCount(), asc->GetNetCount(), \
871 PADS_BINARY_BOARDS[idx].differentRevision ); \
872 }
873
874NET_COUNT_TEST( TMS1mmX19, 0 )
875NET_COUNT_TEST( MC4_PLUS_CSHAPE, 1 )
876NET_COUNT_TEST( MC2_PLUS_REV1, 2 )
877NET_COUNT_TEST( Ems4_Rev2, 3 )
878NET_COUNT_TEST( LCORE_4, 4 )
879NET_COUNT_TEST( LCORE_2, 5 )
880NET_COUNT_TEST( Dexter_MotorCtrl, 6 )
881NET_COUNT_TEST( MAIS_FC, 7 )
882
883
884#define NET_NAMES_EXACT_TEST( name, idx ) \
885 BOOST_AUTO_TEST_CASE( NetNamesExact_##name ) \
886 { \
887 auto bin = LoadBinary( PADS_BINARY_BOARDS[idx] ); \
888 \
889 auto binNames = BoardNetNames( bin.get() ); \
890 auto asc = LoadAsc( PADS_BINARY_BOARDS[idx] ); \
891 auto ascNames = BoardNetNames( asc.get() ); \
892 \
893 std::set<wxString> missing; \
894 std::set_difference( ascNames.begin(), ascNames.end(), binNames.begin(), binNames.end(), \
895 std::inserter( missing, missing.begin() ) ); \
896 \
897 std::set<wxString> extra; \
898 std::set_difference( binNames.begin(), binNames.end(), ascNames.begin(), ascNames.end(), \
899 std::inserter( extra, extra.begin() ) ); \
900 \
901 BOOST_CHECK_MESSAGE( missing.empty(), #name " missing binary nets: " << JoinNetSet( missing ) ); \
902 BOOST_CHECK_MESSAGE( extra.empty(), #name " extra binary nets: " << JoinNetSet( extra ) ); \
903 }
904
905NET_NAMES_EXACT_TEST( MC4_PLUS_CSHAPE, 1 )
906NET_NAMES_EXACT_TEST( Ems4_Rev2, 3 )
907
908
909// The section-62/64 route stream is present on routed boards and absent on unrouted boards.
910// Exact geometry and layer comparisons above guard against fabricated route reconstruction;
911// these smoke tests keep the older in-tree corpus exercising both structural cases.
912#define ROUTED_TRACKS_TEST( name, idx ) \
913 BOOST_AUTO_TEST_CASE( RoutedTracks_##name ) \
914 { \
915 auto bin = LoadBinary( PADS_BINARY_BOARDS[idx] ); \
916 \
917 BOOST_CHECK_GT( CountTraces( bin ), 0u ); \
918 }
919
920ROUTED_TRACKS_TEST( Ems4_Rev2, 3 )
921ROUTED_TRACKS_TEST( LCORE_4, 4 )
922ROUTED_TRACKS_TEST( LCORE_2, 5 )
923ROUTED_TRACKS_TEST( MAIS_FC, 7 )
924
925
926BOOST_AUTO_TEST_CASE( UnroutedBoardHasNoTracksOrVias_TMS1mmX19 )
927{
928 auto bin = LoadBinary( PADS_BINARY_BOARDS[0] );
929
930 BOOST_CHECK_EQUAL( CountTraces( bin ), 0u );
931 BOOST_CHECK_EQUAL( CountVias( bin ), 0u );
932}
933
934
935BOOST_AUTO_TEST_CASE( UnroutedBoardHasNoTracksOrVias_MC4_PLUS_CSHAPE )
936{
937 auto bin = LoadBinary( PADS_BINARY_BOARDS[1] );
938
939 BOOST_CHECK_EQUAL( CountTraces( bin ), 0u );
940 BOOST_CHECK_EQUAL( CountVias( bin ), 0u );
941}
942
943
944// The per-pin padstack assignment (section-15 (pin, ref) pairs sliced by the section-14
945// descriptor table, indexed into the extended section-4 pool) gives each decal's pads their
946// own geometry. Before it, every pad on the board shared pad stack 0, collapsing the whole
947// board to a single distinct pad geometry. Assert the imported board now carries a variety of
948// pad geometries that approaches the ASCII reference's variety.
949static size_t DistinctPadGeometries( BOARD* aBoard )
950{
951 std::set<std::tuple<int, int, int>> geoms;
952
953 for( FOOTPRINT* fp : aBoard->Footprints() )
954 {
955 for( PAD* pad : fp->Pads() )
956 {
957 VECTOR2I sz = pad->GetSize( F_Cu );
958 geoms.emplace( static_cast<int>( pad->GetShape( F_Cu ) ), sz.x, sz.y );
959 }
960 }
961
962 return geoms.size();
963}
964
965
966BOOST_AUTO_TEST_CASE( PerPinPadStackGeometry_MC4_PLUS_CSHAPE )
967{
968 auto bin = LoadBinary( PADS_BINARY_BOARDS[1] );
969
970 size_t binGeoms = DistinctPadGeometries( bin.get() );
971 BOOST_TEST_MESSAGE( "MC4_PLUS_CSHAPE binary distinct pad geometries: " << binGeoms );
972
973 // A single shared pad stack 0 would yield one geometry; per-pin assignment recovers the
974 // real spread.
975 BOOST_CHECK_GE( binGeoms, 5u );
976
977 auto asc = LoadAsc( PADS_BINARY_BOARDS[1] );
978
979 size_t ascGeoms = DistinctPadGeometries( asc.get() );
980 BOOST_TEST_MESSAGE( "MC4_PLUS_CSHAPE ASCII distinct pad geometries: " << ascGeoms );
981
982 // De-duplicated library passives without a descriptor keep the default geometry, so
983 // allow margin, but the binary should reach a substantial fraction of the reference.
984 BOOST_CHECK_GE( binGeoms, ascGeoms / 2 );
985}
986
987
988// Vias are exact structural anchors decoded from the section 60 via records. On the routed
989// v0x2026 LCORE boards the binary via set matches the ASCII reference exactly.
990BOOST_AUTO_TEST_CASE( StructuralRoutesAndVias_LCORE_4 )
991{
992 auto bin = LoadBinary( PADS_BINARY_BOARDS[4] );
993 auto asc = LoadAsc( PADS_BINARY_BOARDS[4] );
994
995 BOOST_REQUIRE( bin );
996 BOOST_REQUIRE( asc );
997
998 BOOST_CHECK_GT( CountTraces( bin ), 0u );
999 BOOST_CHECK_EQUAL( CountVias( bin ), CountVias( asc ) );
1000}
1001
1002
1003BOOST_AUTO_TEST_CASE( StructuralRoutesAndVias_LCORE_2 )
1004{
1005 auto bin = LoadBinary( PADS_BINARY_BOARDS[5] );
1006 auto asc = LoadAsc( PADS_BINARY_BOARDS[5] );
1007
1008 BOOST_REQUIRE( bin );
1009 BOOST_REQUIRE( asc );
1010
1011 BOOST_CHECK_GT( CountTraces( bin ), 0u );
1012 BOOST_CHECK_EQUAL( CountVias( bin ), CountVias( asc ) );
1013}
1014
1015
1016BOOST_AUTO_TEST_CASE( ViaCountAtLeast_Ems4_Rev2 )
1017{
1018 auto bin = LoadBinary( PADS_BINARY_BOARDS[3] );
1019 auto asc = LoadAsc( PADS_BINARY_BOARDS[3] );
1020
1021 BOOST_REQUIRE( bin );
1022 BOOST_REQUIRE( asc );
1023
1024 BOOST_REQUIRE_GE( CountVias( asc ), 443u );
1025 BOOST_CHECK_GE( CountVias( bin ), 443u );
1026 BOOST_CHECK_LE( CountVias( bin ), CountVias( asc ) );
1027}
1028
1029
1030BOOST_AUTO_TEST_CASE( BoardOutline_LCORE_4 )
1031{
1032 auto board = LoadBinary( PADS_BINARY_BOARDS[4] );
1033
1034 BOOST_CHECK_MESSAGE( CountEdgeCutsShapes( board.get() ) > 0, "LCORE_4 binary should have board outline shapes" );
1035}
1036
1037
1038BOOST_AUTO_TEST_CASE( BoardOutline_LCORE_2 )
1039{
1040 auto board = LoadBinary( PADS_BINARY_BOARDS[5] );
1041
1042 BOOST_CHECK_MESSAGE( CountEdgeCutsShapes( board.get() ) > 0, "LCORE_2 binary should have board outline shapes" );
1043}
1044
1045
1046BOOST_AUTO_TEST_CASE( BoardOutline_OtherVersions )
1047{
1048 int indices[] = { 0, 1, 2, 3, 6, 7 };
1049
1050 for( int i : indices )
1051 {
1052 auto board = LoadBinary( PADS_BINARY_BOARDS[i] );
1053
1054 BOOST_WARN_MESSAGE( CountEdgeCutsShapes( board.get() ) > 0,
1055 PADS_BINARY_BOARDS[i].dir << " binary outline parsing not yet complete" );
1056 }
1057}
1058
1059
1060// Centerline bounding box of all Edge_Cuts drawing shapes, using each shape's own
1061// geometric bounding box (arcs report their true swept extent, not the chord). The
1062// line width does not materially affect the comparison since both importers draw the
1063// outline on the same layer and the tolerance absorbs the half-width.
1064static BOX2I EdgeCutsBBox( const BOARD* aBoard )
1065{
1066 BOX2I bbox;
1067
1068 for( BOARD_ITEM* item : aBoard->Drawings() )
1069 {
1070 if( PCB_SHAPE* shape = dynamic_cast<PCB_SHAPE*>( item ) )
1071 {
1072 if( shape->GetLayer() == Edge_Cuts )
1073 bbox.Merge( shape->GetBoundingBox() );
1074 }
1075 }
1076
1077 return bbox;
1078}
1079
1080
1081static int CountEdgeCutsArcs( const BOARD* aBoard )
1082{
1083 int count = 0;
1084
1085 for( BOARD_ITEM* item : aBoard->Drawings() )
1086 {
1087 if( PCB_SHAPE* shape = dynamic_cast<PCB_SHAPE*>( item ) )
1088 {
1089 if( shape->GetLayer() == Edge_Cuts && shape->GetShape() == SHAPE_T::ARC )
1090 count++;
1091 }
1092 }
1093
1094 return count;
1095}
1096
1097
1098// Arc-laden board outlines (MC4/LCORE) are decoded from the binary vertex run plus
1099// the geometric arc-parameter table. Validate that the binary outline reproduces the
1100// ASCII outline's overall size and arc content (not just "more than zero shapes").
1101static void CheckArcOutlineMatchesAsc( int aIdx )
1102{
1103 auto bin = LoadBinary( PADS_BINARY_BOARDS[aIdx] );
1104 auto asc = LoadAsc( PADS_BINARY_BOARDS[aIdx] );
1105
1106 const std::string& name = PADS_BINARY_BOARDS[aIdx].dir;
1107
1108 int binArcs = CountEdgeCutsArcs( bin.get() );
1109 int ascArcs = CountEdgeCutsArcs( asc.get() );
1110
1111 BOOST_CHECK_MESSAGE( binArcs == ascArcs, name << " edge-cut arc count: binary=" << binArcs << " asc=" << ascArcs );
1112
1113 BOX2I binBox = EdgeCutsBBox( bin.get() );
1114 BOX2I ascBox = EdgeCutsBBox( asc.get() );
1115
1116 BOOST_REQUIRE_MESSAGE( binBox.GetWidth() > 0 && ascBox.GetWidth() > 0, name << " edge-cut bounding box is empty" );
1117
1118 // Size must agree closely. The fixed slack absorbs the outline pen width (the ASC
1119 // carries the piece width while the binary path leaves it at 0) plus ASC 2-decimal
1120 // mils rounding; the 1% term scales with the board. This is far tighter than the
1121 // ~7M nm error a wrong major/minor arc selection produced.
1122 double tolX = std::max( 400000.0, ascBox.GetWidth() * 0.01 );
1123 double tolY = std::max( 400000.0, ascBox.GetHeight() * 0.01 );
1124
1125 BOOST_CHECK_MESSAGE( std::abs( binBox.GetWidth() - ascBox.GetWidth() ) < tolX,
1126 name << " edge-cut width: binary=" << binBox.GetWidth() << " asc=" << ascBox.GetWidth() );
1127 BOOST_CHECK_MESSAGE( std::abs( binBox.GetHeight() - ascBox.GetHeight() ) < tolY,
1128 name << " edge-cut height: binary=" << binBox.GetHeight() << " asc=" << ascBox.GetHeight() );
1129}
1130
1131
1132BOOST_AUTO_TEST_CASE( ArcBoardOutline_MC4_PLUS_CSHAPE )
1133{
1135}
1136
1137
1138BOOST_AUTO_TEST_CASE( ArcBoardOutline_LCORE_4 )
1139{
1141}
1142
1143
1144BOOST_AUTO_TEST_CASE( ArcBoardOutline_LCORE_2 )
1145{
1147}
1148
1149
1150// A wrong board outline is worse than none. For every corpus board assert the binary
1151// importer either ships no Edge_Cuts outline at all, or one whose bounding box matches
1152// the ASCII reference outline within tolerance. This catches the class of bug where a
1153// decal or concatenated piece is mistaken for the board outline (too large) or a small
1154// wrong piece is selected (too small), which a bare "more than zero shapes" check missed.
1155BOOST_AUTO_TEST_CASE( BoardOutlineCorrectOrAbsent )
1156{
1158 {
1159 if( info.differentRevision )
1160 continue;
1161
1162 auto bin = LoadBinary( info );
1163 auto asc = LoadAsc( info );
1164
1165 BOX2I binBox = EdgeCutsBBox( bin.get() );
1166 BOX2I ascBox = EdgeCutsBBox( asc.get() );
1167
1168 // No binary outline shipped is acceptable; only a present-but-wrong one fails.
1169 if( binBox.GetWidth() <= 0 && binBox.GetHeight() <= 0 )
1170 continue;
1171
1172 BOOST_REQUIRE_MESSAGE( ascBox.GetWidth() > 0 && ascBox.GetHeight() > 0,
1173 info.dir << " ships a binary outline but the ASC has none "
1174 "to validate against" );
1175
1176 double tolX = std::max( 400000.0, ascBox.GetWidth() * 0.02 );
1177 double tolY = std::max( 400000.0, ascBox.GetHeight() * 0.02 );
1178
1179 BOOST_CHECK_MESSAGE( std::abs( binBox.GetWidth() - ascBox.GetWidth() ) < tolX,
1180 info.dir << " board outline width: binary=" << binBox.GetWidth()
1181 << " asc=" << ascBox.GetWidth() << " (wrong outline shipped)" );
1182 BOOST_CHECK_MESSAGE( std::abs( binBox.GetHeight() - ascBox.GetHeight() ) < tolY,
1183 info.dir << " board outline height: binary=" << binBox.GetHeight()
1184 << " asc=" << ascBox.GetHeight() << " (wrong outline shipped)" );
1185
1186 // Size alone is translation-invariant, so also pin the binary outline relative
1187 // to the binary footprints (a shared frame, unlike the ASC importer which uses a
1188 // different absolute origin). A correctly placed outline contains the parts; a
1189 // doubly-origin-shifted one keeps the right size but drifts off the parts.
1190 BOX2I fpBox;
1191
1192 for( FOOTPRINT* fp : bin->Footprints() )
1193 fpBox.Merge( fp->GetPosition() );
1194
1195 if( fpBox.GetWidth() > 0 || fpBox.GetHeight() > 0 )
1196 {
1197 BOX2I grown = binBox;
1198 grown.Inflate( std::max( binBox.GetWidth(), binBox.GetHeight() ) );
1199
1200 BOOST_CHECK_MESSAGE( grown.Contains( fpBox.GetCenter() ),
1201 info.dir << " board outline at " << binBox.GetCenter()
1202 << " does not enclose the footprint cloud centered at " << fpBox.GetCenter()
1203 << " (outline mispositioned)" );
1204 }
1205 }
1206}
1207
1208
1209#define STRUCTURAL_INTEGRITY_TEST( name, idx ) \
1210 BOOST_AUTO_TEST_CASE( StructuralIntegrity_##name ) \
1211 { \
1212 auto board = LoadBinary( PADS_BINARY_BOARDS[idx] ); \
1213 \
1214 RunStructuralChecks( PADS_BINARY_BOARDS[idx], board.get() ); \
1215 }
1216
1217#define ZONE_COUNT_TEST( name, idx ) \
1218 BOOST_AUTO_TEST_CASE( ZoneCount_##name ) \
1219 { \
1220 auto bin = LoadBinary( PADS_BINARY_BOARDS[idx] ); \
1221 \
1222 auto asc = LoadAsc( PADS_BINARY_BOARDS[idx] ); \
1223 \
1224 size_t binZones = bin->Zones().size(); \
1225 size_t ascZones = asc->Zones().size(); \
1226 \
1227 if( PADS_BINARY_BOARDS[idx].differentRevision ) \
1228 { \
1229 BOOST_WARN_MESSAGE( binZones > 0, #name " binary zone count: " << binZones << " (different revision)" ); \
1230 } \
1231 else \
1232 { \
1233 BOOST_WARN_MESSAGE( binZones == ascZones, \
1234 #name " zone count: binary=" << binZones << " asc=" << ascZones ); \
1235 } \
1236 }
1237
1238ZONE_COUNT_TEST( MC4_PLUS_CSHAPE, 1 )
1239ZONE_COUNT_TEST( Ems4_Rev2, 3 )
1240ZONE_COUNT_TEST( LCORE_4, 4 )
1241ZONE_COUNT_TEST( LCORE_2, 5 )
1242ZONE_COUNT_TEST( MAIS_FC, 7 )
1243
1244
1245BOOST_AUTO_TEST_CASE( ZoneCountExact_MC4_PLUS_CSHAPE )
1246{
1247 // This board's copper-shape recovery has been verified directly against the ASCII *LINES*
1248 // COPPER entries by name (not just by count): the binary importer decodes all 15 real
1249 // COPPER shapes with zero false positives (up from 2 before this session's classifier and
1250 // circle-closure fixes). asc->Zones() is NOT used as the reference here -- the ASCII pour
1251 // parser has its own unrelated, pre-existing bug (it reads far more "*POUROUT*"-adjacent
1252 // lines than there are real pours, most filtered downstream but not audited closely enough
1253 // to reconstruct a reliable total), so its zone count is not a trustworthy oracle for this
1254 // comparison. Two shapes are on copper layer 4; the other thirteen retain their serialized
1255 // documentation layers as filled graphics. 36 = 2 copper shapes + 9 keepouts + 25 pours.
1256 auto bin = LoadBinary( PADS_BINARY_BOARDS[1] );
1257
1258 BOOST_REQUIRE( bin );
1259 BOOST_CHECK_EQUAL( bin->Zones().size(), 36 );
1260 BOOST_CHECK_EQUAL( std::count_if( bin->Drawings().begin(), bin->Drawings().end(),
1261 []( const BOARD_ITEM* aItem )
1262 {
1263 const PCB_SHAPE* shape = dynamic_cast<const PCB_SHAPE*>( aItem );
1264 return shape && shape->GetShape() == SHAPE_T::POLY && shape->IsSolidFill();
1265 } ),
1266 13 );
1267}
1268
1269
1270BOOST_AUTO_TEST_CASE( ZoneCountExact_Ems4_Rev2 )
1271{
1272 // See ZoneCountExact_MC4_PLUS_CSHAPE for why asc->Zones() is not used as the reference.
1273 // The binary importer recovers all 39 real COPPER owners. Four are on copper layers; the
1274 // other 35 retain their silkscreen/paste/custom layers as filled graphics.
1275 auto bin = LoadBinary( PADS_BINARY_BOARDS[3] );
1276
1277 BOOST_REQUIRE( bin );
1278 BOOST_CHECK_EQUAL( bin->Zones().size(), 32 );
1279 BOOST_CHECK_EQUAL( std::count_if( bin->Drawings().begin(), bin->Drawings().end(),
1280 []( const BOARD_ITEM* aItem )
1281 {
1282 const PCB_SHAPE* shape = dynamic_cast<const PCB_SHAPE*>( aItem );
1283 return shape && shape->GetShape() == SHAPE_T::POLY && shape->IsSolidFill();
1284 } ),
1285 35 );
1286}
1287
1288
1289BOOST_AUTO_TEST_CASE( CopperShapeLevelsUseSerializedSuccessorField )
1290{
1291 struct EXPECTED_LEVEL
1292 {
1293 size_t board;
1294 std::string name;
1295 int level;
1296 };
1297
1298 const std::vector<EXPECTED_LEVEL> expected = {
1299 { 1, "DRW68014421", 4 }, { 1, "DRW62739568", 26 }, { 3, "DRW55434270", 1 },
1300 { 3, "DRW28145516", 22 }, { 3, "DRW81324514", 24 },
1301 };
1302
1303 std::map<size_t, std::shared_ptr<PADS_IO::BINARY_PARSER>> parsers;
1304
1305 for( const EXPECTED_LEVEL& item : expected )
1306 {
1307 auto& parser = parsers[item.board];
1308
1309 if( !parser )
1310 {
1311 parser = std::make_shared<PADS_IO::BINARY_PARSER>();
1312 wxString filename = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/" + PADS_BINARY_BOARDS[item.board].dir
1313 + "/" + PADS_BINARY_BOARDS[item.board].binaryFile;
1314 parser->Parse( filename );
1315 }
1316
1317 auto shape = std::find_if( parser->GetCopperShapes().begin(), parser->GetCopperShapes().end(),
1318 [&]( const PADS_IO::COPPER_SHAPE& aShape )
1319 {
1320 return aShape.name == item.name;
1321 } );
1322
1323 BOOST_REQUIRE( shape != parser->GetCopperShapes().end() );
1324 BOOST_CHECK_EQUAL( shape->layer, item.level );
1325 }
1326}
1327
1328
1329BOOST_AUTO_TEST_CASE( ZoneCountExact_LCORE_4 )
1330{
1331 auto bin = LoadBinary( PADS_BINARY_BOARDS[4] );
1332
1333 BOOST_REQUIRE( bin );
1334 BOOST_CHECK_EQUAL( bin->Zones().size(), 4 ); // 2 KEEPOUT, 2 POUROUT
1335}
1336
1337
1338BOOST_AUTO_TEST_CASE( ZoneCountExact_LCORE_2 )
1339{
1340 auto bin = LoadBinary( PADS_BINARY_BOARDS[5] );
1341
1342 BOOST_REQUIRE( bin );
1343 BOOST_CHECK_EQUAL( bin->Zones().size(), 4 ); // 2 KEEPOUT, 2 POUROUT
1344}
1345
1346
1347static size_t CountFreeTexts( const BOARD* aBoard )
1348{
1349 size_t count = 0;
1350
1351 for( BOARD_ITEM* item : aBoard->Drawings() )
1352 {
1353 if( dynamic_cast<PCB_TEXT*>( item ) )
1354 count++;
1355 }
1356
1357 return count;
1358}
1359
1360
1361static bool HasFreeText( const BOARD* aBoard, const wxString& aText )
1362{
1363 for( BOARD_ITEM* item : aBoard->Drawings() )
1364 {
1365 PCB_TEXT* text = dynamic_cast<PCB_TEXT*>( item );
1366
1367 if( text && text->GetText() == aText )
1368 return true;
1369 }
1370
1371 return false;
1372}
1373
1374
1375#define FREE_TEXT_COUNT_TEST( name, idx ) \
1376 BOOST_AUTO_TEST_CASE( FreeTextCount_##name ) \
1377 { \
1378 auto bin = LoadBinary( PADS_BINARY_BOARDS[idx] ); \
1379 \
1380 auto asc = LoadAsc( PADS_BINARY_BOARDS[idx] ); \
1381 \
1382 size_t binTexts = CountFreeTexts( bin.get() ); \
1383 size_t ascTexts = CountFreeTexts( asc.get() ); \
1384 \
1385 BOOST_WARN_MESSAGE( binTexts == ascTexts, \
1386 #name " free text count: binary=" << binTexts << " asc=" << ascTexts \
1387 << " (text extraction incomplete)" ); \
1388 }
1389
1390FREE_TEXT_COUNT_TEST( TMS1mmX19, 0 )
1391FREE_TEXT_COUNT_TEST( MC4_PLUS_CSHAPE, 1 )
1392FREE_TEXT_COUNT_TEST( MC2_PLUS_REV1, 2 )
1393FREE_TEXT_COUNT_TEST( Ems4_Rev2, 3 )
1394FREE_TEXT_COUNT_TEST( LCORE_4, 4 )
1395FREE_TEXT_COUNT_TEST( LCORE_2, 5 )
1396FREE_TEXT_COUNT_TEST( Dexter_MotorCtrl, 6 )
1397FREE_TEXT_COUNT_TEST( MAIS_FC, 7 )
1398
1399
1406#define FREE_TEXT_EXACT_TEST( name, idx ) \
1407 BOOST_AUTO_TEST_CASE( FreeTextExact_##name ) \
1408 { \
1409 auto bin = LoadBinary( PADS_BINARY_BOARDS[idx] ); \
1410 \
1411 auto asc = LoadAsc( PADS_BINARY_BOARDS[idx] ); \
1412 \
1413 BOOST_CHECK_EQUAL( CountFreeTexts( bin.get() ), CountFreeTexts( asc.get() ) ); \
1414 }
1415
1416FREE_TEXT_EXACT_TEST( TMS1mmX19, 0 )
1417FREE_TEXT_EXACT_TEST( Ems4_Rev2, 3 )
1418FREE_TEXT_EXACT_TEST( LCORE_4, 4 )
1419FREE_TEXT_EXACT_TEST( LCORE_2, 5 )
1420
1421
1422BOOST_AUTO_TEST_CASE( FreeTextRejectsClusterNames_MC4_PLUS_CSHAPE )
1423{
1424 auto bin = LoadBinary( PADS_BINARY_BOARDS[1] );
1425
1426 BOOST_CHECK_MESSAGE( !HasFreeText( bin.get(), "CLU_DCDC5V" ),
1427 "MC4 binary cluster name CLU_DCDC5V should not import as free text" );
1428 BOOST_CHECK_MESSAGE( !HasFreeText( bin.get(), "CLU_DCDC3V3" ),
1429 "MC4 binary cluster name CLU_DCDC3V3 should not import as free text" );
1430}
1431
1432
1433BOOST_AUTO_TEST_CASE( FreeTextUsesDeclaredSection8Ring )
1434{
1435 wxString source = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/LCORE_2/LCORE_2.pcb";
1436
1437 std::vector<uint8_t> bytes;
1438 BOOST_REQUIRE( PADS_IO::ReadFileToBuffer( source, bytes ) );
1439
1441 sdb.Load( bytes );
1442
1443 const PADS_IO::SDB_SECTION* texts = sdb.Section( 8 );
1444 BOOST_REQUIRE( texts != nullptr );
1445 BOOST_REQUIRE( texts->physicalOffset >= 180 );
1446 BOOST_REQUIRE( texts->physicalBytes >= 108 );
1447
1448 const size_t ringBase = texts->physicalOffset - 36;
1449 const size_t decoyBase = texts->physicalOffset - 180;
1450 std::copy_n( bytes.begin() + ringBase, 144, bytes.begin() + decoyBase );
1451
1452 PADS_IO::BINARY_PARSER baseline;
1453 baseline.Parse( source );
1454
1455 wxString tempBase = wxFileName::CreateTempFileName( wxS( "kicad_pads_text_decoy_" ) );
1456 wxRemoveFile( tempBase );
1457 wxString tempPath = tempBase + wxS( ".pcb" );
1458
1459 {
1460 wxFFile file( tempPath, wxS( "wb" ) );
1461 BOOST_REQUIRE( file.IsOpened() );
1462 BOOST_REQUIRE_EQUAL( file.Write( bytes.data(), bytes.size() ), bytes.size() );
1463 }
1464
1465 PADS_IO::BINARY_PARSER withDecoy;
1466 withDecoy.Parse( tempPath );
1467 wxRemoveFile( tempPath );
1468
1469 BOOST_CHECK_EQUAL( withDecoy.GetTexts().size(), baseline.GetTexts().size() );
1470}
1471
1472
1473#define PAD_COUNT_TEST( name, idx ) \
1474 BOOST_AUTO_TEST_CASE( PadCount_##name ) \
1475 { \
1476 auto bin = LoadBinary( PADS_BINARY_BOARDS[idx] ); \
1477 \
1478 auto asc = LoadAsc( PADS_BINARY_BOARDS[idx] ); \
1479 \
1480 size_t binPads = 0; \
1481 size_t ascPads = 0; \
1482 \
1483 for( FOOTPRINT * fp : bin->Footprints() ) \
1484 binPads += fp->Pads().size(); \
1485 \
1486 for( FOOTPRINT * fp : asc->Footprints() ) \
1487 ascPads += fp->Pads().size(); \
1488 \
1489 BOOST_WARN_MESSAGE( binPads == ascPads, \
1490 #name " pad count: binary=" << binPads << " asc=" << ascPads \
1491 << " (part-to-decal linking incomplete)" ); \
1492 }
1493
1505#define PER_FOOTPRINT_CONTENT_TEST( name, idx, maxNameWrong, maxCountWrong ) \
1506 BOOST_AUTO_TEST_CASE( PerFootprintContent_##name ) \
1507 { \
1508 auto bin = LoadBinary( PADS_BINARY_BOARDS[idx] ); \
1509 \
1510 auto asc = LoadAsc( PADS_BINARY_BOARDS[idx] ); \
1511 \
1512 std::map<wxString, FOOTPRINT*> ascByRef; \
1513 \
1514 for( FOOTPRINT * fp : asc->Footprints() ) \
1515 ascByRef[fp->GetReference()] = fp; \
1516 \
1517 int resolved = 0, nameWrong = 0, countWrong = 0; \
1518 wxString wrongRefs; \
1519 \
1520 for( FOOTPRINT * fp : bin->Footprints() ) \
1521 { \
1522 wxString binDecal = fp->GetFPID().GetLibItemName().wx_str(); \
1523 \
1524 if( binDecal == fp->GetReference() ) \
1525 continue; /* unresolved placement (absent from section 22) */ \
1526 \
1527 auto it = ascByRef.find( fp->GetReference() ); \
1528 \
1529 if( it == ascByRef.end() ) \
1530 continue; \
1531 \
1532 resolved++; \
1533 \
1534 if( binDecal != it->second->GetFPID().GetLibItemName().wx_str() ) \
1535 { \
1536 nameWrong++; \
1537 wrongRefs += fp->GetReference() \
1538 + wxString::Format( ":name %s/%s %zu/%zu ", binDecal, \
1539 it->second->GetFPID().GetLibItemName().wx_str(), fp->Pads().size(), \
1540 it->second->Pads().size() ); \
1541 } \
1542 else if( fp->Pads().size() != it->second->Pads().size() ) \
1543 { \
1544 countWrong++; \
1545 wrongRefs += fp->GetReference() \
1546 + wxString::Format( ":%zu/%zu ", fp->Pads().size(), it->second->Pads().size() ); \
1547 } \
1548 } \
1549 \
1550 BOOST_TEST_MESSAGE( #name " per-footprint: resolved=" << resolved << " nameWrong=" << nameWrong \
1551 << " countWrong=" << countWrong \
1552 << " wrongRefs=" << wrongRefs ); \
1553 BOOST_CHECK_LE( nameWrong, maxNameWrong ); \
1554 BOOST_CHECK_LE( countWrong, maxCountWrong ); \
1555 }
1556
1557PER_FOOTPRINT_CONTENT_TEST( TMS1mmX19, 0, 1, 0 )
1558PER_FOOTPRINT_CONTENT_TEST( MC4_PLUS_CSHAPE, 1, 0, 0 )
1559PER_FOOTPRINT_CONTENT_TEST( Ems4_Rev2, 3, 0, 0 )
1560PER_FOOTPRINT_CONTENT_TEST( LCORE_4, 4, 1, 0 )
1561
1562
1563BOOST_AUTO_TEST_CASE( SectionBoundaryPlacement_TMS1mmX19_SM3 )
1564{
1565 auto bin = LoadBinary( PADS_BINARY_BOARDS[0] );
1566
1567 FOOTPRINT* sm3 = FindFootprintByReference( bin.get(), "SM3" );
1568
1569 BOOST_REQUIRE_MESSAGE( sm3 != nullptr, "TMS1mmX19 binary should import section-boundary placement SM3" );
1570 BOOST_CHECK_EQUAL( sm3->GetFPID().GetLibItemName().wx_str(), "MTHOLE-M3-3.2MM" );
1571 BOOST_CHECK_EQUAL( sm3->Pads().size(), 1u );
1572}
1573
1574
1585BOOST_AUTO_TEST_CASE( V2021_PadImport_MAIS_FC )
1586{
1587 auto bin = LoadBinary( PADS_BINARY_BOARDS[7] );
1588
1589 size_t binPads = 0;
1590 int resolved = 0;
1591
1592 for( FOOTPRINT* fp : bin->Footprints() )
1593 {
1594 binPads += fp->Pads().size();
1595
1596 if( fp->GetFPID().GetLibItemName().wx_str() != fp->GetReference() )
1597 resolved++;
1598 }
1599
1600 BOOST_TEST_MESSAGE( "MAIS_FC v0x2021: pads=" << binPads << " resolved=" << resolved );
1601
1602 // MAIS_FC has 5 placed parts (43 pads) and the chain is fully deterministic here.
1603 BOOST_CHECK_EQUAL( binPads, 43u );
1604 BOOST_CHECK_EQUAL( resolved, 5 );
1605
1606 // Verify the actual decal mapping, not just the aggregate count. These connectors
1607 // live in section 19 (section 22 carries no placements on this board), so this also
1608 // exercises the +1-lag leading-block recovery: J7 is the anchor block that has no
1609 // 0xFEFF marker of its own. Decal -> expected terminal count from the .asc PARTDECAL.
1610 std::map<wxString, std::pair<wxString, size_t>> expected = {
1611 { "J7", { "54722-0201", 20 } }, { "J1", { "SOLDERLAND4", 11 } }, { "J2", { "CON3", 3 } },
1612 { "J3", { "CON3", 3 } }, { "J4", { "CON6", 6 } },
1613 };
1614
1615 int verified = 0;
1616
1617 for( FOOTPRINT* fp : bin->Footprints() )
1618 {
1619 auto it = expected.find( fp->GetReference() );
1620
1621 if( it == expected.end() )
1622 continue;
1623
1624 verified++;
1625 BOOST_CHECK_EQUAL( fp->GetFPID().GetLibItemName().wx_str(), it->second.first );
1626 BOOST_CHECK_EQUAL( fp->Pads().size(), it->second.second );
1627 }
1628
1629 BOOST_CHECK_EQUAL( verified, 5 );
1630}
1631
1632
1633BOOST_AUTO_TEST_CASE( V2021_PadImport_Dexter_MotorCtrl )
1634{
1635 auto bin = LoadBinary( PADS_BINARY_BOARDS[6] );
1636
1637 size_t binPads = 0;
1638 int resolved = 0;
1639
1640 for( FOOTPRINT* fp : bin->Footprints() )
1641 {
1642 binPads += fp->Pads().size();
1643
1644 if( fp->GetFPID().GetLibItemName().wx_str() != fp->GetReference() )
1645 resolved++;
1646 }
1647
1648 BOOST_TEST_MESSAGE( "Dexter_MotorCtrl v0x2021: pads=" << binPads << " resolved=" << resolved );
1649
1650 // Dexter_MotorCtrl previously imported zero pads (the chain was gated off for v0x2021).
1651 // The direct decal-index path now resolves the large majority of placements; the binary
1652 // is a different revision than the .asc (GOLD 918) and carries additional placed parts,
1653 // so assert a substantial, content-bearing pad count rather than an exact GOLD match.
1654 BOOST_CHECK_GT( binPads, 800u );
1655 BOOST_CHECK_GT( resolved, 200 );
1656
1657 // Verify specific multi-pin decal mappings so a shifted decal index that still yields
1658 // many pads would be caught. Decal name and terminal count both come from the binary
1659 // JMPVIA table and match the .asc PARTDECAL for these parts.
1660 std::map<wxString, std::pair<wxString, size_t>> expected = {
1661 { "D1", { "DFN-6-9", 9 } },
1662 { "L1", { "IHLP-2525", 2 } },
1663 };
1664
1665 int verified = 0;
1666
1667 for( FOOTPRINT* fp : bin->Footprints() )
1668 {
1669 auto it = expected.find( fp->GetReference() );
1670
1671 if( it == expected.end() )
1672 continue;
1673
1674 verified++;
1675 BOOST_CHECK_EQUAL( fp->GetFPID().GetLibItemName().wx_str(), it->second.first );
1676 BOOST_CHECK_EQUAL( fp->Pads().size(), it->second.second );
1677 }
1678
1679 BOOST_CHECK_EQUAL( verified, 2 );
1680}
1681
1682
1695BOOST_AUTO_TEST_CASE( V2021_PartPlacement_MAIS_FC )
1696{
1697 auto bin = LoadBinary( PADS_BINARY_BOARDS[7] ); // MAIS_FC, v0x2021
1698
1699 struct Oracle
1700 {
1701 wxString ref;
1702 int ascY; // ASC design-space Y, for relative ordering only
1703 double ori; // ASC ORI degrees (top-side parts)
1704 bool flipped; // ASC MIRROR flag
1705 };
1706
1707 const std::vector<Oracle> oracle = {
1708 { "J1", 10800000, 0.0, false }, { "J2", 23663130, 180.0, false }, { "J3", 8785902, 0.0, false },
1709 { "J4", 21216083, 90.0, false }, { "J7", 9000000, 270.0, true },
1710 };
1711
1712 std::map<wxString, FOOTPRINT*> binFps;
1713
1714 for( FOOTPRINT* fp : bin->Footprints() )
1715 binFps[fp->GetReference()] = fp;
1716
1717 // Y must not collapse onto a single value (the unsolved-Y regression emitted 0 for all).
1718 std::set<int> binYs;
1719
1720 for( const Oracle& o : oracle )
1721 {
1722 BOOST_REQUIRE_MESSAGE( binFps.count( o.ref ), "v0x2021 missing footprint " << o.ref );
1723 binYs.insert( binFps[o.ref]->GetPosition().y );
1724 }
1725
1726 BOOST_CHECK_MESSAGE( binYs.size() == oracle.size(), "v0x2021 part Y collapsed to "
1727 << binYs.size() << " of " << oracle.size()
1728 << " distinct values (Y decode regression)" );
1729
1730 // Side must match the MIRROR flag; un-mirrored parts must carry the exact ASC ORI.
1731 for( const Oracle& o : oracle )
1732 {
1733 FOOTPRINT* fp = binFps[o.ref];
1734
1735 BOOST_CHECK_MESSAGE( fp->IsFlipped() == o.flipped,
1736 "v0x2021 " << o.ref << " side " << fp->IsFlipped() << " != " << o.flipped );
1737
1738 if( o.flipped )
1739 continue;
1740
1741 double deg = fp->GetOrientation().Normalize().AsDegrees();
1742 double diff = std::abs( deg - o.ori );
1743
1744 BOOST_CHECK_MESSAGE( diff < 0.1 || std::abs( diff - 360.0 ) < 0.1,
1745 "v0x2021 " << o.ref << " orientation " << deg << " != " << o.ori );
1746 }
1747
1748 // Relative Y ordering must follow the ASC (origin/scale/flip invariant): sorting by ASC
1749 // Y must yield a strictly monotonic placed-Y sequence.
1750 std::vector<Oracle> byAscY( oracle );
1751 std::sort( byAscY.begin(), byAscY.end(),
1752 []( const Oracle& a, const Oracle& b )
1753 {
1754 return a.ascY < b.ascY;
1755 } );
1756
1757 bool incr = true;
1758 bool decr = true;
1759
1760 for( size_t i = 1; i < byAscY.size(); ++i )
1761 {
1762 int prev = binFps[byAscY[i - 1].ref]->GetPosition().y;
1763 int cur = binFps[byAscY[i].ref]->GetPosition().y;
1764
1765 if( cur <= prev )
1766 incr = false;
1767
1768 if( cur >= prev )
1769 decr = false;
1770 }
1771
1772 BOOST_CHECK_MESSAGE( incr || decr, "v0x2021 placed-Y ordering does not match ASC oracle" );
1773}
1774
1775
1776PAD_COUNT_TEST( TMS1mmX19, 0 )
1777PAD_COUNT_TEST( MC4_PLUS_CSHAPE, 1 )
1778PAD_COUNT_TEST( MC2_PLUS_REV1, 2 )
1779PAD_COUNT_TEST( Ems4_Rev2, 3 )
1780PAD_COUNT_TEST( LCORE_4, 4 )
1781PAD_COUNT_TEST( LCORE_2, 5 )
1782PAD_COUNT_TEST( Dexter_MotorCtrl, 6 )
1783PAD_COUNT_TEST( MAIS_FC, 7 )
1784
1785BOOST_AUTO_TEST_CASE( PadCountExact_MC4_PLUS_CSHAPE )
1786{
1787 auto bin = LoadBinary( PADS_BINARY_BOARDS[1] );
1788 auto asc = LoadAsc( PADS_BINARY_BOARDS[1] );
1789
1790 size_t binPads = 0;
1791 size_t ascPads = 0;
1792
1793 for( FOOTPRINT* fp : bin->Footprints() )
1794 binPads += fp->Pads().size();
1795
1796 for( FOOTPRINT* fp : asc->Footprints() )
1797 ascPads += fp->Pads().size();
1798
1799 BOOST_CHECK_EQUAL( binPads, ascPads );
1800}
1801
1802BOOST_AUTO_TEST_CASE( PadCountExact_Ems4_Rev2 )
1803{
1804 auto bin = LoadBinary( PADS_BINARY_BOARDS[3] );
1805 auto asc = LoadAsc( PADS_BINARY_BOARDS[3] );
1806
1807 size_t binPads = 0;
1808 size_t ascPads = 0;
1809
1810 for( FOOTPRINT* fp : bin->Footprints() )
1811 binPads += fp->Pads().size();
1812
1813 for( FOOTPRINT* fp : asc->Footprints() )
1814 ascPads += fp->Pads().size();
1815
1816 BOOST_CHECK_EQUAL( binPads, ascPads );
1817}
1818
1819BOOST_AUTO_TEST_CASE( PadCountExact_LCORE_4 )
1820{
1821 auto bin = LoadBinary( PADS_BINARY_BOARDS[4] );
1822 auto asc = LoadAsc( PADS_BINARY_BOARDS[4] );
1823
1824 size_t binPads = 0;
1825 size_t ascPads = 0;
1826
1827 for( FOOTPRINT* fp : bin->Footprints() )
1828 binPads += fp->Pads().size();
1829
1830 for( FOOTPRINT* fp : asc->Footprints() )
1831 ascPads += fp->Pads().size();
1832
1833 BOOST_CHECK_EQUAL( binPads, ascPads );
1834}
1835
1836
1837STRUCTURAL_INTEGRITY_TEST( TMS1mmX19, 0 )
1838STRUCTURAL_INTEGRITY_TEST( MC4_PLUS_CSHAPE, 1 )
1839STRUCTURAL_INTEGRITY_TEST( MC2_PLUS_REV1, 2 )
1840STRUCTURAL_INTEGRITY_TEST( Ems4_Rev2, 3 )
1841STRUCTURAL_INTEGRITY_TEST( LCORE_4, 4 )
1842STRUCTURAL_INTEGRITY_TEST( LCORE_2, 5 )
1843STRUCTURAL_INTEGRITY_TEST( Dexter_MotorCtrl, 6 )
1844STRUCTURAL_INTEGRITY_TEST( MAIS_FC, 7 )
1845
1846
1852#define WIDTH_VARIETY_TEST( name, idx ) \
1853 BOOST_AUTO_TEST_CASE( TrackWidthVariety_##name ) \
1854 { \
1855 auto bin = LoadBinary( PADS_BINARY_BOARDS[idx] ); \
1856 \
1857 auto asc = LoadAsc( PADS_BINARY_BOARDS[idx] ); \
1858 \
1859 std::set<int> binWidths; \
1860 std::set<int> ascWidths; \
1861 \
1862 for( PCB_TRACK * trk : bin->Tracks() ) \
1863 { \
1864 if( trk->Type() == PCB_TRACE_T ) \
1865 binWidths.insert( trk->GetWidth() ); \
1866 } \
1867 \
1868 for( PCB_TRACK * trk : asc->Tracks() ) \
1869 { \
1870 if( trk->Type() == PCB_TRACE_T ) \
1871 ascWidths.insert( trk->GetWidth() ); \
1872 } \
1873 \
1874 if( ascWidths.size() > 1 ) \
1875 { \
1876 BOOST_WARN_MESSAGE( binWidths.size() > 1, #name " binary should have multiple distinct track widths " \
1877 "(found " \
1878 << binWidths.size() << ", ASCII has " \
1879 << ascWidths.size() << ")" ); \
1880 } \
1881 }
1882
1883WIDTH_VARIETY_TEST( TMS1mmX19, 0 )
1884WIDTH_VARIETY_TEST( Ems4_Rev2, 3 )
1885WIDTH_VARIETY_TEST( LCORE_4, 4 )
1886WIDTH_VARIETY_TEST( LCORE_2, 5 )
1887WIDTH_VARIETY_TEST( Dexter_MotorCtrl, 6 )
1888WIDTH_VARIETY_TEST( MAIS_FC, 7 )
1889
1890
1908BOOST_AUTO_TEST_CASE( StructuralZoneVertices )
1909{
1910 struct EXPECTED_OWNER
1911 {
1912 std::string boardDir;
1913 std::string binaryFile;
1914 std::string owner;
1915 std::vector<std::pair<int, int>> firstVerts; // design coords, sec12 units
1916 };
1917
1918 const std::vector<EXPECTED_OWNER> cases = {
1919 { "MC4_PLUS_CSHAPE",
1920 "MC4_PLUS_CSHAPE.pcb",
1921 "DRW68014421",
1922 { { 0, 0 }, { 0, 5486400 }, { 9982200, 5486400 }, { 10210800, 5257800 } } },
1923 { "MC4_PLUS_CSHAPE",
1924 "MC4_PLUS_CSHAPE.pcb",
1925 "DRW16024650",
1926 { { 0, 0 }, { 0, 9784905 }, { -452970, 8882805 }, { -1905015, -2943150 } } },
1927 { "Ems4_Rev2",
1928 "Ems4_Rev2.pcb",
1929 "DRW55434270",
1930 { { 0, -571500 }, { 0, 1333500 }, { 1714500, 1333500 }, { 1714500, 6096000 } } },
1931 { "Ems4_Rev2",
1932 "Ems4_Rev2.pcb",
1933 "DRW1329638",
1934 { { 0, 0 }, { 0, -2095500 }, { 2857500, -2095500 }, { 3048000, -2286000 } } },
1935 { "LCORE_4",
1936 "LCORE_4.pcb",
1937 "DRW47981509",
1938 { { 803078, -1462546 }, { -1259549, -2233786 }, { -295519, -4678038 }, { 1767106, -3906796 } } },
1939 };
1940
1941 std::map<std::string, std::shared_ptr<PADS_IO::BINARY_PARSER>> parsers;
1942
1943 for( const EXPECTED_OWNER& ec : cases )
1944 {
1945 BOOST_TEST_CONTEXT( ec.boardDir << " " << ec.owner )
1946 {
1947 std::shared_ptr<PADS_IO::BINARY_PARSER>& parser = parsers[ec.boardDir];
1948
1949 if( !parser )
1950 {
1951 parser = std::make_shared<PADS_IO::BINARY_PARSER>();
1952 wxString filename =
1953 KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/" + ec.boardDir + "/" + ec.binaryFile;
1954 parser->Parse( filename );
1955 }
1956
1957 std::vector<VECTOR2I> loop;
1958
1959 BOOST_REQUIRE_MESSAGE( parser->GetOwnerLoopForTest( ec.owner, loop ),
1960 ec.owner << " has no structural sec12 loop" );
1961
1962 BOOST_REQUIRE_GE( loop.size(), ec.firstVerts.size() );
1963
1964 for( size_t i = 0; i < ec.firstVerts.size(); ++i )
1965 {
1966 BOOST_CHECK_EQUAL( loop[i].x, ec.firstVerts[i].first );
1967 BOOST_CHECK_EQUAL( loop[i].y, ec.firstVerts[i].second );
1968 }
1969 }
1970 }
1971}
1972
1973
1974// PADS name fields use a legacy 8-bit code page, so a high byte must survive the fixed-string read
1975// and decode through ConvertText. Rejecting it blanks the whole field, and the same board then
1976// imports with names from .asc and without them from .pcb.
1977BOOST_AUTO_TEST_CASE( FixedStringKeepsHighBytes )
1978{
1979 // "Res" with a CP1252 e-acute in the middle
1980 std::vector<uint8_t> field = { 'R', 0xE9, 's', 0 };
1981
1982 std::string raw = PADS_IO::readFixedString( field, 0, 4 );
1983
1984 BOOST_REQUIRE_EQUAL( raw.size(), 3u );
1985 BOOST_CHECK_EQUAL( static_cast<unsigned>( static_cast<uint8_t>( raw[1] ) ), 0xE9u );
1986 BOOST_CHECK_EQUAL( PADS_COMMON::ConvertText( raw ), wxString::FromUTF8( "R\xC3\xA9s" ) );
1987
1988 // Net names reach the board through the inverted-name wrapper, so it has to decode as well
1989 BOOST_CHECK_EQUAL( PADS_COMMON::ConvertInvertedNetName( raw ), wxString::FromUTF8( "R\xC3\xA9s" ) );
1991 wxString::FromUTF8( "~{R\xC3\xA9s}" ) );
1992}
1993
1994
1995// A control byte still invalidates the field, and only trailing spaces are trimmed. StrPurge also
1996// stripped leading whitespace, which the documented contract does not allow.
1997BOOST_AUTO_TEST_CASE( FixedStringRejectsControlAndKeepsLeadingSpace )
1998{
1999 std::vector<uint8_t> control = { 'A', 0x01, 'B', 0 };
2000
2001 BOOST_CHECK( PADS_IO::readFixedString( control, 0, 4 ).empty() );
2002
2003 std::vector<uint8_t> padded = { ' ', ' ', 'N', 'E', 'T', ' ', ' ', 0 };
2004
2005 BOOST_CHECK_EQUAL( PADS_IO::readFixedString( padded, 0, 8 ), std::string( " NET" ) );
2006}
2007
2008
2009// A section-14 record count that overruns the file must be rejected before it sizes the decal-name
2010// table. Without the extent guard the reserve raises std::bad_alloc, which is not an IO_ERROR and
2011// so escapes the plugin's catch.
2012BOOST_AUTO_TEST_CASE( DecalNameTableRejectsOversizedCount )
2013{
2014 // The directory entry count is a u32 at HEADER_SIZE + DecalHeader * DIR_ENTRY_SIZE
2015 const size_t sec14CountOffset = 10 + 14 * 16;
2016
2017 std::ifstream in( GetBinaryPath( PADS_BINARY_BOARDS[4] ).ToStdString(), std::ios::binary );
2018
2019 BOOST_REQUIRE( in.good() );
2020
2021 std::vector<char> bytes( ( std::istreambuf_iterator<char>( in ) ), std::istreambuf_iterator<char>() );
2022
2023 in.close();
2024
2025 BOOST_REQUIRE_GT( bytes.size(), sec14CountOffset + 4 );
2026
2027 bytes[sec14CountOffset + 0] = static_cast<char>( 0xFF );
2028 bytes[sec14CountOffset + 1] = static_cast<char>( 0xFF );
2029 bytes[sec14CountOffset + 2] = static_cast<char>( 0xFF );
2030 bytes[sec14CountOffset + 3] = static_cast<char>( 0x0F );
2031
2032 std::filesystem::path mutated = std::filesystem::temp_directory_path()
2033 / "kicad_pads_sec14_oversized_count.pcb";
2034
2035 {
2036 std::ofstream out( mutated, std::ios::binary );
2037
2038 out.write( bytes.data(), static_cast<std::streamsize>( bytes.size() ) );
2039 }
2040
2042
2043 BOOST_CHECK_EXCEPTION( parser.Parse( wxString::FromUTF8( mutated.string() ) ), IO_ERROR,
2044 []( const IO_ERROR& aError )
2045 {
2046 return aError.What().Contains( wxT( "decal-name table extent" ) );
2047 } );
2048
2049 std::filesystem::remove( mutated );
2050}
2051
2052
2062BOOST_AUTO_TEST_CASE( PadStackShapeEnum_OblongDecoded )
2063{
2064 const std::vector<std::string> boardsWithOblong = { "TMS1mmX19", "MC4_PLUS_CSHAPE" };
2065
2066 for( const std::string& dir : boardsWithOblong )
2067 {
2068 BOOST_TEST_CONTEXT( dir )
2069 {
2071 wxString filename = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/" + dir + "/" + dir + ".pcb";
2072
2073 parser.Parse( filename );
2074
2075 std::set<std::string> shapes = parser.GetPadStackShapesForTest();
2076
2077 BOOST_CHECK_MESSAGE( shapes.count( "OF" ),
2078 dir << " decoded no OF padstack (shape-enum off-by-one regression)" );
2079
2080 // The same boards carry round and rectangular-finger padstacks, so a decode
2081 // that collapsed everything to one shape would also be caught.
2082 BOOST_CHECK_MESSAGE( shapes.count( "R" ), dir << " decoded no round padstack" );
2083 BOOST_CHECK_MESSAGE( shapes.count( "RF" ), dir << " decoded no RF padstack" );
2084 }
2085 }
2086}
2087
2088
2099BOOST_AUTO_TEST_CASE( DedupePoolTerminalPositions_MC4 )
2100{
2102 wxString filename = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/MC4_PLUS_CSHAPE/MC4_PLUS_CSHAPE.pcb";
2103
2104 parser.Parse( filename );
2105
2106 const std::map<std::string, PADS_IO::PART_DECAL>& decals = parser.GetPartDecals();
2107
2108 struct Expected
2109 {
2110 std::string name;
2111 std::vector<std::pair<double, double>> terms;
2112 };
2113
2114 const std::vector<Expected> cases = {
2115 { "0402", { { -750000, 0 }, { 750000, 0 } } }, { "0603", { { -945000, 0 }, { 945000, 0 } } },
2116 { "0805", { { -1200000, 0 }, { 1200000, 0 } } }, { "1206", { { -1800000, 0 }, { 1800000, 0 } } },
2117 { "CC3", { { 0, -571500 }, { 0, 0 }, { 0, 571500 } } },
2118 };
2119
2120 for( const Expected& e : cases )
2121 {
2122 BOOST_TEST_CONTEXT( e.name )
2123 {
2124 auto it = decals.find( e.name );
2125 BOOST_REQUIRE_MESSAGE( it != decals.end(), "decal " << e.name << " missing" );
2126
2127 const std::vector<PADS_IO::TERMINAL>& terms = it->second.terminals;
2128 BOOST_REQUIRE_EQUAL( terms.size(), e.terms.size() );
2129
2130 for( size_t t = 0; t < e.terms.size(); ++t )
2131 {
2132 BOOST_CHECK_EQUAL( terms[t].x, e.terms[t].first );
2133 BOOST_CHECK_EQUAL( terms[t].y, e.terms[t].second );
2134 }
2135 }
2136 }
2137}
2138
2139
2140//---------------------------------------------------------------------------------------
2141// WAVE 3 round-trip oracle tests: part clusters (groups), dimensions, stackup, diff pairs.
2142//---------------------------------------------------------------------------------------
2143
2152BOOST_AUTO_TEST_CASE( ClusterGroups_MC4_PLUS_CSHAPE )
2153{
2154 const PADS_BINARY_BOARD_INFO board{ "MC4_PLUS_CSHAPE", "MC4_PLUS_CSHAPE.pcb", "MC4_PLUS_CSHAPE.asc", false };
2155
2156 std::shared_ptr<BOARD> brd = LoadBinary( board );
2157 BOOST_REQUIRE( brd != nullptr );
2158
2159 std::map<wxString, std::set<wxString>> groupMembers;
2160
2161 for( PCB_GROUP* group : brd->Groups() )
2162 {
2163 std::set<wxString>& refs = groupMembers[group->GetName()];
2164
2165 for( EDA_ITEM* item : group->GetItems() )
2166 {
2167 if( item->Type() == PCB_FOOTPRINT_T )
2168 refs.insert( static_cast<FOOTPRINT*>( item )->GetReference() );
2169 }
2170 }
2171
2172 BOOST_REQUIRE_MESSAGE( groupMembers.count( "CLU_DCDC5V" ), "cluster CLU_DCDC5V missing from PCB_GROUPs" );
2173 BOOST_REQUIRE_MESSAGE( groupMembers.count( "CLU_DCDC3V3" ), "cluster CLU_DCDC3V3 missing from PCB_GROUPs" );
2174
2175 const std::set<wxString> clu5v = { "C79", "C80", "C81", "C82", "C83", "C84", "C85", "C86",
2176 "D4", "L2", "R84", "R85", "R86", "R87", "R89", "U10" };
2177 const std::set<wxString> clu3v3 = { "C72", "C73", "C74", "C75", "C76", "C77", "C78", "D3",
2178 "L1", "R77", "R78", "R79", "R80", "R81", "R83", "U9" };
2179
2180 BOOST_CHECK( groupMembers["CLU_DCDC5V"] == clu5v );
2181 BOOST_CHECK( groupMembers["CLU_DCDC3V3"] == clu3v3 );
2182}
2183
2184
2185BOOST_AUTO_TEST_CASE( ModernConnectionsUseDeclaredSection24Ring )
2186{
2187 const wxString source = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/LCORE_2/LCORE_2.pcb";
2188
2189 std::vector<uint8_t> bytes;
2190 BOOST_REQUIRE( PADS_IO::ReadFileToBuffer( source, bytes ) );
2191
2193 sdb.Load( bytes );
2194
2195 const PADS_IO::SDB_SECTION* text = sdb.Section( 8 );
2196 const PADS_IO::SDB_SECTION* connections = sdb.Section( 24 );
2197 BOOST_REQUIRE( text != nullptr );
2198 BOOST_REQUIRE( connections != nullptr );
2199 BOOST_REQUIRE_GT( connections->count, 2u );
2200 BOOST_REQUIRE_GE( text->physicalBytes, 68u );
2201 BOOST_REQUIRE_GE( connections->physicalOffset, 36u );
2202
2203 const size_t declaredRecord = connections->physicalOffset - 36 + 2 * 68;
2204 BOOST_REQUIRE_LE( declaredRecord + 68, bytes.size() );
2205
2206 std::copy_n( bytes.begin() + declaredRecord, 68, bytes.begin() + text->physicalOffset );
2207 bytes[declaredRecord + 23] = 0x42;
2208
2209 wxString tempBase = wxFileName::CreateTempFileName( wxS( "kicad_pads_connection_decoy_" ) );
2210 wxRemoveFile( tempBase );
2211 wxString tempPath = tempBase + wxS( ".pcb" );
2212
2213 {
2214 wxFFile file( tempPath, wxS( "wb" ) );
2215 BOOST_REQUIRE( file.IsOpened() );
2216 BOOST_REQUIRE_EQUAL( file.Write( bytes.data(), bytes.size() ), bytes.size() );
2217 }
2218
2219 bool rejectedDeclaredRing = false;
2220
2221 try
2222 {
2224 parser.Parse( tempPath );
2225 }
2226 catch( const std::exception& e )
2227 {
2228 rejectedDeclaredRing = std::string( e.what() ).find( "net-connection ring framing" ) != std::string::npos;
2229 }
2230
2231 wxRemoveFile( tempPath );
2232 BOOST_CHECK( rejectedDeclaredRing );
2233}
2234
2235
2236BOOST_AUTO_TEST_CASE( ModernConnectionsRequireSerializedFinalEdgeHead )
2237{
2238 const wxString source = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/LCORE_2/LCORE_2.pcb";
2239
2240 std::vector<uint8_t> bytes;
2241 BOOST_REQUIRE( PADS_IO::ReadFileToBuffer( source, bytes ) );
2242
2244 sdb.Load( bytes );
2245
2246 const PADS_IO::SDB_SECTION* connections = sdb.Section( 24 );
2247 BOOST_REQUIRE( connections != nullptr );
2248 BOOST_REQUIRE_GE( connections->physicalOffset, 36u );
2249
2250 const size_t finalHead = connections->physicalOffset - 36 + static_cast<size_t>( connections->count ) * 68;
2251 BOOST_REQUIRE_LE( finalHead + 36, bytes.size() );
2252
2253 bytes[finalHead + 23] = 0x42;
2254
2255 wxString tempBase = wxFileName::CreateTempFileName( wxS( "kicad_pads_connection_final_" ) );
2256 wxRemoveFile( tempBase );
2257 wxString tempPath = tempBase + wxS( ".pcb" );
2258
2259 {
2260 wxFFile file( tempPath, wxS( "wb" ) );
2261 BOOST_REQUIRE( file.IsOpened() );
2262 BOOST_REQUIRE_EQUAL( file.Write( bytes.data(), bytes.size() ), bytes.size() );
2263 }
2264
2265 bool rejectedFinalHead = false;
2266
2267 try
2268 {
2270 parser.Parse( tempPath );
2271 }
2272 catch( const std::exception& e )
2273 {
2274 rejectedFinalHead = std::string( e.what() ).find( "net-connection ring framing" ) != std::string::npos;
2275 }
2276
2277 wxRemoveFile( tempPath );
2278 BOOST_CHECK( rejectedFinalHead );
2279}
2280
2281
2282BOOST_AUTO_TEST_CASE( LegacyConnectionsRequireDeclaredRingMarker )
2283{
2284 const wxString source = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/Dexter_MotorCtrl/Dexter_MotorCtrl.pcb";
2285
2286 std::vector<uint8_t> bytes;
2287 BOOST_REQUIRE( PADS_IO::ReadFileToBuffer( source, bytes ) );
2288
2290 sdb.Load( bytes );
2291
2292 const PADS_IO::SDB_SECTION* connections = sdb.Section( 24 );
2293 BOOST_REQUIRE( connections != nullptr );
2294 BOOST_REQUIRE_GT( connections->count, 0u );
2295
2296 const size_t firstRecord = static_cast<size_t>( connections->physicalOffset ) + 16;
2297 BOOST_REQUIRE_LE( firstRecord + 68, bytes.size() );
2298 bytes[firstRecord + 39] = 0x42;
2299
2300 wxString tempBase = wxFileName::CreateTempFileName( wxS( "kicad_pads_connection_legacy_" ) );
2301 wxRemoveFile( tempBase );
2302 wxString tempPath = tempBase + wxS( ".pcb" );
2303
2304 {
2305 wxFFile file( tempPath, wxS( "wb" ) );
2306 BOOST_REQUIRE( file.IsOpened() );
2307 BOOST_REQUIRE_EQUAL( file.Write( bytes.data(), bytes.size() ), bytes.size() );
2308 }
2309
2310 bool rejectedLegacyRing = false;
2311
2312 try
2313 {
2315 parser.Parse( tempPath );
2316 }
2317 catch( const std::exception& e )
2318 {
2319 rejectedLegacyRing = std::string( e.what() ).find( "legacy net-connection ring framing" ) != std::string::npos;
2320 }
2321
2322 wxRemoveFile( tempPath );
2323 BOOST_CHECK( rejectedLegacyRing );
2324}
2325
2326
2332BOOST_AUTO_TEST_CASE( SdbContainerDecode )
2333{
2334 wxString path = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/LCORE_2/LCORE_2.pcb";
2335
2336 std::vector<uint8_t> bytes;
2337 BOOST_REQUIRE_MESSAGE( PADS_IO::ReadFileToBuffer( path, bytes ), "LCORE_2.pcb test data should be readable" );
2338
2340 BOOST_REQUIRE_NO_THROW( sdb.Load( std::move( bytes ) ) );
2341
2342 // LCORE_2 is a v0x2026 board: the modern 75-entry directory and a per-axis origin.
2343 BOOST_CHECK_EQUAL( sdb.Version(), 0x2026 );
2344 BOOST_CHECK( !sdb.IsOldFormat() );
2345 BOOST_CHECK_EQUAL( sdb.SectionCount(), 75u );
2346
2347 BOOST_REQUIRE( sdb.Coords().Found() );
2348 BOOST_CHECK_EQUAL( sdb.Coords().OriginX(), -2290000 );
2349 BOOST_CHECK_EQUAL( sdb.Coords().OriginY(), -213230500 );
2350
2351 // design = raw - origin, so the origin itself maps to design (0, 0).
2352 BOOST_CHECK_EQUAL( sdb.Coords().DesignX( sdb.Coords().OriginX() ), 0 );
2353 BOOST_CHECK_EQUAL( sdb.Coords().DesignY( sdb.Coords().OriginY() ), 0 );
2354
2355 // The physical net controller is present after the rotated board-setup view.
2356 const PADS_IO::SDB_SECTION* nets = sdb.Section( 23 );
2357 BOOST_REQUIRE( nets != nullptr );
2358 BOOST_CHECK( sdb.Coords().HeaderBase() > 0 );
2359 BOOST_CHECK( nets->physicalOffset > sdb.Coords().HeaderBase() );
2361
2362 // Out-of-range section indices return null rather than indexing past the directory.
2363 BOOST_CHECK( sdb.Section( -1 ) == nullptr );
2364 BOOST_CHECK( sdb.Section( 10000 ) == nullptr );
2365}
2366
2367
2368BOOST_AUTO_TEST_CASE( SdbPagedControllersUseSerializedPageCounts )
2369{
2370 wxString path = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/TMS1mmX19/TMS1mmX19.pcb";
2371
2372 std::vector<uint8_t> bytes;
2373 BOOST_REQUIRE_MESSAGE( PADS_IO::ReadFileToBuffer( path, bytes ), "TMS1mmX19.pcb test data should be readable" );
2374
2376 BOOST_REQUIRE_NO_THROW( sdb.Load( std::move( bytes ) ) );
2377
2378 BOOST_CHECK_EQUAL( sdb.SectionCount(), 75u );
2379
2380 const PADS_IO::SDB_SECTION* pageDirectory = sdb.Section( 26 );
2381 const PADS_IO::SDB_SECTION* clearance = sdb.Section( 41 );
2382 const PADS_IO::SDB_SECTION* highSpeed = sdb.Section( 42 );
2383 const PADS_IO::SDB_SECTION* route = sdb.Section( 46 );
2384 const PADS_IO::SDB_SECTION* diffPair = sdb.Section( 48 );
2385 const PADS_IO::SDB_SECTION* netRelationships = sdb.Section( 49 );
2386 const PADS_IO::SDB_SECTION* stringPool = sdb.Section( 57 );
2387 const PADS_IO::SDB_SECTION* layers = sdb.Section( 69 );
2388 const PADS_IO::SDB_SECTION* layerState = sdb.Section( 70 );
2389 const PADS_IO::SDB_SECTION* displayPreferences = sdb.Section( 71 );
2390 const PADS_IO::SDB_SECTION* fonts = sdb.Section( 73 );
2391
2392 BOOST_REQUIRE( pageDirectory != nullptr );
2393 BOOST_REQUIRE( clearance != nullptr );
2394 BOOST_REQUIRE( highSpeed != nullptr );
2395 BOOST_REQUIRE( route != nullptr );
2396 BOOST_REQUIRE( diffPair != nullptr );
2397 BOOST_REQUIRE( netRelationships != nullptr );
2398 BOOST_REQUIRE( stringPool != nullptr );
2399 BOOST_REQUIRE( layers != nullptr );
2400 BOOST_REQUIRE( layerState != nullptr );
2401 BOOST_REQUIRE( displayPreferences != nullptr );
2402 BOOST_REQUIRE( fonts != nullptr );
2403
2404 BOOST_CHECK_EQUAL( pageDirectory->physicalOffset, 474496u );
2405 BOOST_CHECK_EQUAL( pageDirectory->physicalBytes, 120u );
2406
2407 BOOST_CHECK_EQUAL( clearance->physicalOffset, 474660u );
2408 BOOST_CHECK_EQUAL( clearance->physicalCount, 1u );
2409 BOOST_CHECK_EQUAL( clearance->physicalBytes, 188u );
2410
2411 BOOST_CHECK_EQUAL( highSpeed->physicalOffset, 474848u );
2412 BOOST_CHECK_EQUAL( highSpeed->physicalCount, 1u );
2413 BOOST_CHECK_EQUAL( highSpeed->physicalBytes, 80u );
2414
2415 BOOST_CHECK_EQUAL( route->physicalOffset, 474928u );
2416 BOOST_CHECK_EQUAL( route->physicalCount, 1u );
2418 BOOST_CHECK_EQUAL( route->physicalBytes, 40u );
2419
2420 BOOST_CHECK_EQUAL( diffPair->physicalOffset, 474968u );
2421 BOOST_CHECK_EQUAL( diffPair->physicalCount, 10u );
2422 BOOST_CHECK_EQUAL( diffPair->physicalBytes, 8640u );
2423
2424 BOOST_CHECK_EQUAL( netRelationships->physicalOffset, 483608u );
2425 BOOST_CHECK_EQUAL( netRelationships->physicalBytes, 44000u );
2426
2427 BOOST_CHECK_EQUAL( stringPool->physicalOffset, 549792u );
2428 BOOST_CHECK_EQUAL( stringPool->physicalBytes, 36703u );
2429
2430 BOOST_CHECK_EQUAL( layers->physicalOffset, 738695u );
2431 BOOST_CHECK_EQUAL( layers->physicalBytes, 4724u );
2432 BOOST_CHECK_EQUAL( layerState->physicalOffset, 743419u );
2433 BOOST_CHECK_EQUAL( layerState->physicalBytes, 4u );
2434 BOOST_CHECK_EQUAL( displayPreferences->physicalOffset, 743423u );
2435 BOOST_CHECK_EQUAL( displayPreferences->physicalBytes, 1144u );
2436 BOOST_CHECK_EQUAL( fonts->physicalOffset, 744567u );
2437 BOOST_CHECK_EQUAL( fonts->physicalBytes, 52u );
2438}
2439
2440
2441BOOST_AUTO_TEST_CASE( SdbRouteRuleSlotLivenessUsesSerializedSlotState )
2442{
2443 wxString path = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/TMS1mmX19/TMS1mmX19.pcb";
2444
2445 std::vector<uint8_t> bytes;
2447
2448 PADS_IO::PADS_SDB baseline;
2449 baseline.Load( bytes );
2450
2451 const PADS_IO::SDB_SECTION* route = baseline.Section( 46 );
2452 const PADS_IO::SDB_SECTION* relationships = baseline.Section( 49 );
2453 const PADS_IO::SDB_SECTION* stringPool = baseline.Section( 57 );
2454 const PADS_IO::SDB_SECTION* layers = baseline.Section( 69 );
2455
2456 BOOST_REQUIRE( route != nullptr );
2457 BOOST_REQUIRE( relationships != nullptr );
2458 BOOST_REQUIRE( stringPool != nullptr );
2459 BOOST_REQUIRE( layers != nullptr );
2460 BOOST_REQUIRE_EQUAL( route->physicalLiveCount, 1u );
2461
2462 const size_t stateOffset = relationships->physicalOffset + relationships->physicalBytes;
2463 BOOST_REQUIRE( stateOffset + 4 <= bytes.size() - 42 );
2464
2465 auto removeState = [stateOffset]( std::vector<uint8_t>& aBytes )
2466 {
2467 aBytes.erase( aBytes.begin() + stateOffset, aBytes.begin() + stateOffset + 4 );
2468
2469 uint32_t containerOffset = static_cast<uint32_t>( aBytes[aBytes.size() - 4] )
2470 | static_cast<uint32_t>( aBytes[aBytes.size() - 3] ) << 8
2471 | static_cast<uint32_t>( aBytes[aBytes.size() - 2] ) << 16
2472 | static_cast<uint32_t>( aBytes[aBytes.size() - 1] ) << 24;
2473 containerOffset -= 4;
2474 aBytes[aBytes.size() - 4] = static_cast<uint8_t>( containerOffset );
2475 aBytes[aBytes.size() - 3] = static_cast<uint8_t>( containerOffset >> 8 );
2476 aBytes[aBytes.size() - 2] = static_cast<uint8_t>( containerOffset >> 16 );
2477 aBytes[aBytes.size() - 1] = static_cast<uint8_t>( containerOffset >> 24 );
2478 };
2479
2480 std::vector<uint8_t> zeroHandle = bytes;
2481 std::fill_n( zeroHandle.begin() + route->physicalOffset + 4, 4, uint8_t{ 0 } );
2482 removeState( zeroHandle );
2483
2484 PADS_IO::PADS_SDB zeroHandleSlot;
2485 BOOST_REQUIRE_NO_THROW( zeroHandleSlot.Load( std::move( zeroHandle ) ) );
2486 BOOST_CHECK_EQUAL( zeroHandleSlot.Section( 46 )->physicalLiveCount, 0u );
2487 BOOST_CHECK_EQUAL( zeroHandleSlot.Section( 57 )->physicalOffset, stringPool->physicalOffset - 4 );
2488 BOOST_CHECK_EQUAL( zeroHandleSlot.Section( 69 )->physicalOffset, layers->physicalOffset - 4 );
2489
2490 std::vector<uint8_t> negativeHandle = bytes;
2491 negativeHandle[route->physicalOffset + 3] |= 0x80;
2492 removeState( negativeHandle );
2493
2494 PADS_IO::PADS_SDB negativeHandleSlot;
2495 BOOST_REQUIRE_NO_THROW( negativeHandleSlot.Load( std::move( negativeHandle ) ) );
2496 BOOST_CHECK_EQUAL( negativeHandleSlot.Section( 46 )->physicalLiveCount, 0u );
2497 BOOST_CHECK_EQUAL( negativeHandleSlot.Section( 57 )->physicalOffset, stringPool->physicalOffset - 4 );
2498 BOOST_CHECK_EQUAL( negativeHandleSlot.Section( 69 )->physicalOffset, layers->physicalOffset - 4 );
2499}
2500
2501
2502BOOST_AUTO_TEST_CASE( SdbStringPoolUsesDeclaredSection57Extent )
2503{
2504 wxString path = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/TMS1mmX19/TMS1mmX19.pcb";
2505
2506 std::vector<uint8_t> bytes;
2508
2509 PADS_IO::PADS_SDB baseline;
2510 baseline.Load( bytes );
2511
2512 const PADS_IO::SDB_SECTION* text = baseline.Section( 8 );
2513 const PADS_IO::SDB_SECTION* strings = baseline.Section( 57 );
2514 BOOST_REQUIRE( text != nullptr );
2515 BOOST_REQUIRE( strings != nullptr );
2516 BOOST_REQUIRE_GE( text->physicalBytes, 64u );
2517 BOOST_REQUIRE_GE( strings->physicalBytes, 64u );
2518
2519 std::vector<uint8_t> decoy = bytes;
2520 std::copy_n( decoy.begin() + strings->physicalOffset, 64, decoy.begin() + text->physicalOffset );
2521
2522 PADS_IO::PADS_SDB withDecoy;
2523 BOOST_REQUIRE_NO_THROW( withDecoy.Load( std::move( decoy ) ) );
2524 BOOST_CHECK_EQUAL( withDecoy.Section( 57 )->physicalOffset, strings->physicalOffset );
2525 BOOST_CHECK_EQUAL( withDecoy.Section( 57 )->physicalBytes, strings->physicalBytes );
2526
2527 std::vector<uint8_t> corrupt = bytes;
2528 const size_t extentOffset = 10 + 57 * 16 + 4;
2529 uint32_t extent = strings->totalBytes + 1;
2530 corrupt[extentOffset] = static_cast<uint8_t>( extent );
2531 corrupt[extentOffset + 1] = static_cast<uint8_t>( extent >> 8 );
2532 corrupt[extentOffset + 2] = static_cast<uint8_t>( extent >> 16 );
2533 corrupt[extentOffset + 3] = static_cast<uint8_t>( extent >> 24 );
2534
2535 wxString tempBase = wxFileName::CreateTempFileName( wxS( "kicad_pads_string_extent_corrupt_" ) );
2536 wxRemoveFile( tempBase );
2537 wxString tempPath = tempBase + wxS( ".pcb" );
2538
2539 {
2540 wxFFile file( tempPath, wxS( "wb" ) );
2541 BOOST_REQUIRE( file.IsOpened() );
2542 BOOST_REQUIRE_EQUAL( file.Write( corrupt.data(), corrupt.size() ), corrupt.size() );
2543 }
2544
2545 PADS_IO::BINARY_PARSER invalid;
2546 BOOST_CHECK_THROW( invalid.Parse( tempPath ), std::exception );
2547 wxRemoveFile( tempPath );
2548}
2549
2550
2551BOOST_AUTO_TEST_CASE( SdbGraphicControllersUseDeclaredCircularExtents )
2552{
2553 wxString path = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/LCORE_2/LCORE_2.pcb";
2554
2555 std::vector<uint8_t> bytes;
2557
2558 PADS_IO::PADS_SDB baseline;
2559 baseline.Load( bytes );
2560
2561 const PADS_IO::SDB_SECTION* text = baseline.Section( 8 );
2562 const PADS_IO::SDB_SECTION* owners = baseline.Section( 10 );
2563 const PADS_IO::SDB_SECTION* pieces = baseline.Section( 11 );
2564 const PADS_IO::SDB_SECTION* vertices = baseline.Section( 12 );
2565 BOOST_REQUIRE( text != nullptr );
2566 BOOST_REQUIRE( owners != nullptr );
2567 BOOST_REQUIRE( pieces != nullptr );
2568 BOOST_REQUIRE( vertices != nullptr );
2569 BOOST_REQUIRE( text->physicalBytes >= 112 );
2570
2571 BOOST_CHECK_EQUAL( owners->physicalBytes, owners->count * 112u );
2572 BOOST_CHECK_EQUAL( pieces->physicalBytes, pieces->count * 20u );
2573 BOOST_CHECK_EQUAL( vertices->physicalBytes, vertices->count * 12u );
2574 BOOST_CHECK_EQUAL( pieces->physicalOffset, owners->physicalOffset + owners->physicalBytes );
2575 BOOST_CHECK_EQUAL( vertices->physicalOffset, pieces->physicalOffset + pieces->physicalBytes );
2576
2577 std::vector<uint8_t> decoy = bytes;
2578 std::copy_n( decoy.begin() + owners->physicalOffset, 112, decoy.begin() + text->physicalOffset );
2579
2580 PADS_IO::PADS_SDB withDecoy;
2581 BOOST_REQUIRE_NO_THROW( withDecoy.Load( std::move( decoy ) ) );
2582 BOOST_CHECK_EQUAL( withDecoy.Section( 10 )->physicalOffset, owners->physicalOffset );
2583 BOOST_CHECK_EQUAL( withDecoy.Section( 11 )->physicalOffset, pieces->physicalOffset );
2584 BOOST_CHECK_EQUAL( withDecoy.Section( 12 )->physicalOffset, vertices->physicalOffset );
2585
2586 std::vector<uint8_t> corrupt = bytes;
2587 const size_t countOffset = 10 + 10 * 16;
2588 uint32_t count = owners->count + 1;
2589 corrupt[countOffset] = static_cast<uint8_t>( count );
2590 corrupt[countOffset + 1] = static_cast<uint8_t>( count >> 8 );
2591 corrupt[countOffset + 2] = static_cast<uint8_t>( count >> 16 );
2592 corrupt[countOffset + 3] = static_cast<uint8_t>( count >> 24 );
2593
2594 PADS_IO::PADS_SDB invalid;
2595 BOOST_CHECK_THROW( invalid.Load( std::move( corrupt ) ), std::exception );
2596}
2597
2598
2599BOOST_AUTO_TEST_CASE( PourOwnersUseDeclaredSection52Array )
2600{
2601 wxString source = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/LCORE_4/LCORE_4.pcb";
2602
2603 std::vector<uint8_t> bytes;
2604 BOOST_REQUIRE( PADS_IO::ReadFileToBuffer( source, bytes ) );
2605
2607 sdb.Load( bytes );
2608
2609 const PADS_IO::SDB_SECTION* text = sdb.Section( 8 );
2610 const PADS_IO::SDB_SECTION* owners = sdb.Section( 52 );
2611 BOOST_REQUIRE( text != nullptr );
2612 BOOST_REQUIRE( owners != nullptr );
2613 BOOST_REQUIRE( text->physicalBytes >= 88 );
2614 BOOST_REQUIRE_EQUAL( owners->physicalBytes, owners->count * 88u );
2615
2616 std::copy_n( bytes.begin() + owners->physicalOffset, 88, bytes.begin() + text->physicalOffset );
2617
2618 wxString tempBase = wxFileName::CreateTempFileName( wxS( "kicad_pads_pour_decoy_" ) );
2619 wxRemoveFile( tempBase );
2620 wxString tempPath = tempBase + wxS( ".pcb" );
2621
2622 {
2623 wxFFile file( tempPath, wxS( "wb" ) );
2624 BOOST_REQUIRE( file.IsOpened() );
2625 BOOST_REQUIRE_EQUAL( file.Write( bytes.data(), bytes.size() ), bytes.size() );
2626 }
2627
2628 auto board = LoadBinaryPath( tempPath, "LCORE_4 pour decoy" );
2629 wxRemoveFile( tempPath );
2630
2631 BOOST_REQUIRE( board );
2632 BOOST_CHECK_EQUAL( board->Zones().size(), 4 );
2633}
2634
2635
2636BOOST_AUTO_TEST_CASE( SdbCorpusVersionsAreRecognized )
2637{
2638 BOOST_CHECK( PADS_IO::PADS_SDB::IsSupportedVersion( 0x2017 ) );
2639 BOOST_CHECK( PADS_IO::PADS_SDB::IsSupportedVersion( 0x2019 ) );
2640}
2641
2642
2643BOOST_AUTO_TEST_CASE( SdbRejectsUnclaimedBodyByte )
2644{
2645 wxString source = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/LCORE_2/LCORE_2.pcb";
2646
2647 std::vector<uint8_t> bytes;
2648 BOOST_REQUIRE( PADS_IO::ReadFileToBuffer( source, bytes ) );
2649
2650 const size_t totalBytesOffset = 10 + 69 * 16 + 4;
2651 BOOST_REQUIRE( totalBytesOffset + sizeof( uint32_t ) <= bytes.size() );
2652
2653 uint32_t totalBytes = static_cast<uint32_t>( bytes[totalBytesOffset] )
2654 | static_cast<uint32_t>( bytes[totalBytesOffset + 1] ) << 8
2655 | static_cast<uint32_t>( bytes[totalBytesOffset + 2] ) << 16
2656 | static_cast<uint32_t>( bytes[totalBytesOffset + 3] ) << 24;
2657 BOOST_REQUIRE_GT( totalBytes, 0u );
2658 --totalBytes;
2659
2660 for( size_t byte = 0; byte < sizeof( totalBytes ); ++byte )
2661 bytes[totalBytesOffset + byte] = static_cast<uint8_t>( totalBytes >> ( byte * 8 ) );
2662
2664 BOOST_CHECK_THROW( sdb.Load( std::move( bytes ) ), std::exception );
2665}
2666
2667
2668BOOST_AUTO_TEST_CASE( RouteObjectsRejectZeroCountWithSerializedBytes )
2669{
2670 const wxString source = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/LCORE_2/LCORE_2.pcb";
2671
2672 std::vector<uint8_t> bytes;
2673 BOOST_REQUIRE( PADS_IO::ReadFileToBuffer( source, bytes ) );
2674
2675 const size_t countOffset = 10 + 62 * 16;
2676 BOOST_REQUIRE( countOffset + sizeof( uint32_t ) <= bytes.size() );
2677 std::fill_n( bytes.begin() + countOffset, sizeof( uint32_t ), 0 );
2678
2679 wxString tempBase = wxFileName::CreateTempFileName( wxS( "kicad_pads_route_count_" ) );
2680 wxRemoveFile( tempBase );
2681 wxString tempPath = tempBase + wxS( ".pcb" );
2682
2683 {
2684 wxFFile file( tempPath, wxS( "wb" ) );
2685 BOOST_REQUIRE( file.IsOpened() );
2686 BOOST_REQUIRE_EQUAL( file.Write( bytes.data(), bytes.size() ), bytes.size() );
2687 }
2688
2690 BOOST_CHECK_THROW( parser.Parse( tempPath ), std::exception );
2691 wxRemoveFile( tempPath );
2692}
2693
2694
2695BOOST_AUTO_TEST_CASE( VersionedLayerTablesUseSerializedNamesAndCounts )
2696{
2697 struct CASE
2698 {
2699 const char* dir;
2700 const char* file;
2701 std::vector<std::string> copperNames;
2702 };
2703
2704 const std::vector<CASE> cases = {
2705 { "Dexter_MotorCtrl",
2706 "Dexter_MotorCtrl.pcb",
2707 { "Top", "Signal Routing 1", "Power P3_3v", "Signal Routing 2", "Signal Routing 3", "Power P33v",
2708 "Signal Routing 4", "Bottom" } },
2709 { "LCORE_2", "LCORE_2.pcb", { "Top", "Bottom" } },
2710 { "MC2_PLUS_REV1",
2711 "MC2_PLUS_REV1.pcb",
2712 { "Top", "Inner Layer 1 GND", "Inner Layer 2", "Inner Layer 3", "Inner Layer 4 VCC", "Bottom" } },
2713 };
2714
2715 for( const CASE& test : cases )
2716 {
2718 parser.Parse( KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/" + test.dir + "/" + test.file );
2719
2720 std::vector<std::string> actual;
2721
2722 for( const PADS_IO::LAYER_INFO& layer : parser.GetLayerInfos() )
2723 {
2724 if( layer.is_copper )
2725 actual.push_back( layer.name );
2726 }
2727
2728 BOOST_CHECK_EQUAL_COLLECTIONS( actual.begin(), actual.end(), test.copperNames.begin(), test.copperNames.end() );
2729 }
2730}
2731
2732
2733BOOST_AUTO_TEST_CASE( DirectPlacementArraysUseSerializedCountsAndOrder )
2734{
2735 struct CASE
2736 {
2737 const char* dir;
2738 const char* file;
2739 size_t count;
2740 const char* first;
2741 const char* last;
2742 };
2743
2744 const std::vector<CASE> cases = {
2745 { "Dexter_MotorCtrl", "Dexter_MotorCtrl.pcb", 284, "J1", "D5" },
2746 { "LCORE_2", "LCORE_2.pcb", 30, "C4", "FL1" },
2747 { "MC2_PLUS_REV1", "MC2_PLUS_REV1.pcb", 402, "C1", "UC128" },
2748 };
2749
2750 for( const CASE& test : cases )
2751 {
2753 parser.Parse( KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/" + test.dir + "/" + test.file );
2754
2755 const std::vector<PADS_IO::PART>& parts = parser.GetParts();
2756 BOOST_REQUIRE_EQUAL( parts.size(), test.count );
2757 BOOST_CHECK_EQUAL( parts.front().name, test.first );
2758 BOOST_CHECK_EQUAL( parts.back().name, test.last );
2759 }
2760}
2761
2762
2763BOOST_AUTO_TEST_CASE( SdbFooterStoresContainerItemBackPointer )
2764{
2765 static constexpr char GUID[] = "{2FE18320-6448-11d1-A412-000000000000}";
2766 static constexpr size_t GUID_LENGTH = sizeof( GUID ) - 1;
2767
2768 std::vector<uint8_t> bytes( 128, 0 );
2769 const size_t footerStart = bytes.size() - GUID_LENGTH - sizeof( uint32_t );
2770 const uint32_t containerItemsOffset = 24;
2771
2772 std::copy_n( reinterpret_cast<const uint8_t*>( GUID ), GUID_LENGTH,
2773 bytes.begin() + static_cast<std::ptrdiff_t>( footerStart ) );
2774
2775 for( size_t i = 0; i < sizeof( containerItemsOffset ); ++i )
2776 bytes[footerStart + GUID_LENGTH + i] = static_cast<uint8_t>( containerItemsOffset >> ( i * 8 ) );
2777
2778 BOOST_REQUIRE_NO_THROW( PADS_IO::ValidateSdbFooter( bytes, footerStart, GUID, GUID_LENGTH ) );
2779
2780 // The offset is a size_t on a public entry point, so a near-SIZE_MAX value must be rejected
2781 // rather than wrap the length sum back inside the buffer and reach the memcmp
2782 BOOST_CHECK_THROW( PADS_IO::ValidateSdbFooter( bytes, SIZE_MAX - 8, GUID, GUID_LENGTH ),
2783 IO_ERROR );
2784 BOOST_CHECK_THROW( PADS_IO::ValidateSdbFooter( bytes, SIZE_MAX, GUID, GUID_LENGTH ), IO_ERROR );
2785
2786 // A buffer shorter than the footer itself must not underflow the remaining-bytes arithmetic
2787 std::vector<uint8_t> tiny( 2, 0 );
2788
2789 BOOST_CHECK_THROW( PADS_IO::ValidateSdbFooter( tiny, 0, GUID, GUID_LENGTH ), IO_ERROR );
2790}
2791
2792
2793BOOST_AUTO_TEST_CASE( SdbRejectsContainerBackPointerInsideFooter )
2794{
2795 const wxString source = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/LCORE_2/LCORE_2.pcb";
2796
2797 std::vector<uint8_t> bytes;
2798 BOOST_REQUIRE( PADS_IO::ReadFileToBuffer( source, bytes ) );
2799 BOOST_REQUIRE_GE( bytes.size(), 42u );
2800
2801 const uint32_t invalidOffset = static_cast<uint32_t>( bytes.size() - 41 );
2802
2803 for( size_t byte = 0; byte < sizeof( invalidOffset ); ++byte )
2804 bytes[bytes.size() - sizeof( invalidOffset ) + byte] = static_cast<uint8_t>( invalidOffset >> ( byte * 8 ) );
2805
2807 BOOST_CHECK_THROW( sdb.Load( std::move( bytes ) ), std::exception );
2808}
2809
2810
2820BOOST_AUTO_TEST_CASE( OriginBaseIsDeclaredNotSearched )
2821{
2822 const std::vector<std::string> boards = {
2823 "Dexter_MotorCtrl/Dexter_MotorCtrl.pcb",
2824 "Ems4_Rev2/Ems4_Rev2.pcb",
2825 "LCORE_2/LCORE_2.pcb",
2826 "LCORE_4/LCORE_4.pcb",
2827 "MAIS_FC/MAIS_FC.pcb",
2828 "MC2_PLUS_REV1/MC2_PLUS_REV1.pcb",
2829 "TMS1mmX19/TMS1mmX19.pcb",
2830 };
2831
2832 for( const std::string& board : boards )
2833 {
2834 wxString path = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/" + board;
2835 std::vector<uint8_t> bytes;
2836
2837 BOOST_REQUIRE_MESSAGE( PADS_IO::ReadFileToBuffer( path, bytes ), board << " should be readable" );
2838
2839 auto u32At = [&bytes]( size_t aOffset )
2840 {
2841 return static_cast<uint32_t>( bytes[aOffset] ) | ( static_cast<uint32_t>( bytes[aOffset + 1] ) << 8 )
2842 | ( static_cast<uint32_t>( bytes[aOffset + 2] ) << 16 )
2843 | ( static_cast<uint32_t>( bytes[aOffset + 3] ) << 24 );
2844 };
2845
2846 const uint32_t slots = u32At( 10 + 16 ); // directory[1].count
2847 const uint32_t dirBytes = u32At( 10 + 16 + 4 ); // directory[1].total_bytes
2848 const uint32_t viewStates = u32At( 10 + 2 * 16 ); // directory[2].count
2849
2850 BOOST_TEST_CONTEXT( board )
2851 {
2852 // The directory declares its own byte size, which is what makes the slot count
2853 // trustworthy without a version branch.
2854 BOOST_CHECK_EQUAL( dirBytes, slots * 16 );
2855
2856 const uint32_t expected = 10 + ( slots - 1 ) * 16 + viewStates * 48;
2857
2859 BOOST_REQUIRE_NO_THROW( sdb.Load( std::move( bytes ) ) );
2860
2861 BOOST_REQUIRE( sdb.Coords().Found() );
2863 }
2864 }
2865}
2866
2867
2868BOOST_AUTO_TEST_CASE( CorruptDeclaredOriginDoesNotRelocateToDftMarker )
2869{
2870 wxString source = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/LCORE_2/LCORE_2.pcb";
2871
2872 std::vector<uint8_t> bytes;
2873 BOOST_REQUIRE( PADS_IO::ReadFileToBuffer( source, bytes ) );
2874
2875 PADS_IO::PADS_SDB baseline;
2876 baseline.Load( bytes );
2877 BOOST_REQUIRE( baseline.Coords().Found() );
2878
2879 const size_t scaleOffset = baseline.Coords().HeaderBase() + 56;
2880 BOOST_REQUIRE( scaleOffset + sizeof( float ) <= bytes.size() );
2881 std::fill_n( bytes.begin() + scaleOffset, sizeof( float ), uint8_t{ 0 } );
2882
2883 const std::string marker = "DFT_CONFIGURATION";
2884 auto markerIt = std::search( bytes.begin(), bytes.end(), marker.begin(), marker.end() );
2885 BOOST_REQUIRE( markerIt != bytes.end() );
2886
2887 const char config[] = "X\0"
2888 "12345\0"
2889 "Y\0"
2890 "67890\0\0";
2891 auto configIt = markerIt + marker.size() + 1;
2892 BOOST_REQUIRE( configIt + sizeof( config ) <= bytes.end() );
2893 std::copy_n( reinterpret_cast<const uint8_t*>( config ), sizeof( config ), configIt );
2894
2895 wxString tempBase = wxFileName::CreateTempFileName( wxS( "kicad_pads_origin_corrupt_" ) );
2896 wxRemoveFile( tempBase );
2897 wxString tempPath = tempBase + wxS( ".pcb" );
2898
2899 {
2900 wxFFile file( tempPath, wxS( "wb" ) );
2901 BOOST_REQUIRE( file.IsOpened() );
2902 BOOST_REQUIRE_EQUAL( file.Write( bytes.data(), bytes.size() ), bytes.size() );
2903 }
2904
2906 BOOST_CHECK_THROW( parser.Parse( tempPath ), std::exception );
2907 wxRemoveFile( tempPath );
2908}
2909
2910
2911// Matches the "__"-joined, space-to-underscore flattening used for the ground-truth
2912// ASCII exports, so dumps and exports pair up by filename.
2913static std::string FlattenPath( const std::filesystem::path& aRoot, const std::filesystem::path& aFile )
2914{
2915 std::filesystem::path relPath = std::filesystem::relative( aFile, aRoot );
2916 std::string out;
2917 bool first = true;
2918
2919 for( const auto& part : relPath )
2920 {
2921 if( !first )
2922 out += "__";
2923
2924 std::string p = part.string();
2925
2926 for( char& c : p )
2927 {
2928 if( c == ' ' )
2929 c = '_';
2930 }
2931
2932 out += p;
2933 first = false;
2934 }
2935
2936 return out;
2937}
2938
2939
2940// The compared tuple carries the via type and both span layers, so a via whose decoded span is
2941// overwritten by SanitizeLayers stops matching its ASCII reference. ExactViaMatches was written
2942// with this commit but never called, which is why the span regression was invisible.
2943BOOST_AUTO_TEST_CASE( ExactViaGeometryMatchesAscii )
2944{
2945 size_t nonThroughSeen = 0;
2946
2947 for( const PADS_BINARY_BOARD_INFO& board : PADS_BINARY_BOARDS )
2948 {
2949 if( board.differentRevision )
2950 continue;
2951
2952 BOOST_TEST_CONTEXT( board.dir )
2953 {
2954 std::pair<size_t, size_t> counts =
2955 ExactViaMatches( GetBinaryPath( board ), GetAscPath( board ), board.dir );
2956
2957 BOOST_CHECK_EQUAL( counts.first, counts.second );
2958
2959 std::shared_ptr<BOARD> ascii = LoadAscPath( GetAscPath( board ), board.dir );
2960
2961 for( PCB_TRACK* item : ascii->Tracks() )
2962 {
2963 PCB_VIA* via = dynamic_cast<PCB_VIA*>( item );
2964
2965 if( via && via->GetViaType() != VIATYPE::THROUGH )
2966 nonThroughSeen++;
2967 }
2968 }
2969 }
2970
2971 // Every via in this corpus is full-stack, so the check above guards width, drill, net and
2972 // position but cannot expose the span ordering. ViaSpanSurvivesOnlyWhenTypeSetFirst covers
2973 // that; raise this to a real assertion once a blind-via board joins the corpus
2974 BOOST_TEST_MESSAGE( "non-through vias in the ASCII reference: " << nonThroughSeen );
2975}
2976
2977
2978// PCB_VIA is constructed THROUGH and SetLayerPair sanitizes the span against the current type, so
2979// the loader has to set the type first. Nothing in the corpus has a blind or buried via, so this
2980// pins the API trap the loader depends on
2981BOOST_AUTO_TEST_CASE( ViaSpanSurvivesOnlyWhenTypeSetFirst )
2982{
2983 BOARD board;
2984
2985 board.SetCopperLayerCount( 4 );
2986
2987 PCB_VIA typeFirst( &board );
2988
2989 typeFirst.SetViaType( VIATYPE::BURIED );
2990 typeFirst.SetLayerPair( In1_Cu, In2_Cu );
2991
2992 BOOST_CHECK_EQUAL( static_cast<int>( typeFirst.TopLayer() ), static_cast<int>( In1_Cu ) );
2993 BOOST_CHECK_EQUAL( static_cast<int>( typeFirst.BottomLayer() ), static_cast<int>( In2_Cu ) );
2994
2995 // The order the loader used before the fix, kept here so the trap stays documented
2996 PCB_VIA spanFirst( &board );
2997
2998 spanFirst.SetLayerPair( In1_Cu, In2_Cu );
2999 spanFirst.SetViaType( VIATYPE::BURIED );
3000
3001 BOOST_CHECK_EQUAL( static_cast<int>( spanFirst.TopLayer() ), static_cast<int>( F_Cu ) );
3002 BOOST_CHECK_EQUAL( static_cast<int>( spanFirst.BottomLayer() ), static_cast<int>( B_Cu ) );
3003}
3004
3005
3007
3008
3009
3017BOOST_AUTO_TEST_CASE( BinaryPadConnectionMatchesAsc )
3018{
3019 auto thermalPadKeys = []( const std::shared_ptr<BOARD>& aBoard )
3020 {
3021 std::set<std::string> keys;
3022
3023 for( FOOTPRINT* fp : aBoard->Footprints() )
3024 {
3025 for( PAD* pad : fp->Pads() )
3026 {
3027 if( pad->GetLocalZoneConnection() == ZONE_CONNECTION::THERMAL )
3028 {
3029 keys.insert( fp->GetReference().ToStdString() + "." + pad->GetNumber().ToStdString() );
3030 }
3031 }
3032 }
3033
3034 return keys;
3035 };
3036
3037 auto everyPourIsSolid = []( const std::shared_ptr<BOARD>& aBoard )
3038 {
3039 for( ZONE* zone : aBoard->Zones() )
3040 {
3041 if( !zone->GetIsRuleArea() && zone->GetNetCode() > 0 && zone->GetPadConnection() != ZONE_CONNECTION::FULL )
3042 {
3043 return false;
3044 }
3045 }
3046
3047 return true;
3048 };
3049
3050 for( const PADS_BINARY_BOARD_INFO& board : PADS_BINARY_BOARDS )
3051 {
3052 BOOST_TEST_CONTEXT( board.dir )
3053 {
3054 std::shared_ptr<BOARD> binary = LoadBinary( board );
3055 std::shared_ptr<BOARD> ascii = LoadAsc( board );
3056
3057 BOOST_REQUIRE( binary != nullptr );
3058 BOOST_REQUIRE( ascii != nullptr );
3059
3060 std::set<std::string> binKeys = thermalPadKeys( binary );
3061 std::set<std::string> ascKeys = thermalPadKeys( ascii );
3062 std::vector<std::string> extra;
3063
3064 // The binary reader must not invent relief the design does not ask for. It can
3065 // still miss some, because the RT/ST decode does not yet cover every way a v0x2027
3066 // file names a plane-relief padstack.
3067 std::set_difference( binKeys.begin(), binKeys.end(), ascKeys.begin(), ascKeys.end(),
3068 std::back_inserter( extra ) );
3069
3070 BOOST_CHECK_MESSAGE( extra.empty(), "binary import invented a thermal override on "
3071 << extra.size() << " pad(s), first "
3072 << ( extra.empty() ? std::string( "-" ) : extra.front() ) );
3073
3074 BOOST_CHECK_MESSAGE( everyPourIsSolid( binary ),
3075 "binary netted pours must connect solid, leaving relief to the "
3076 "per-pad overrides" );
3077 BOOST_CHECK_MESSAGE( everyPourIsSolid( ascii ),
3078 "ASCII netted pours must connect solid, leaving relief to the "
3079 "per-pad overrides" );
3080 }
3081 }
3082}
3083
3084
3085BOOST_AUTO_TEST_CASE( PadstackRowsMatchAsciiAcross2022And2027 )
3086{
3087 const wxString root = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/padstack_rows/";
3088 const wxString asciiPath = root + "padstack-v2022.asc";
3089 const wxString v2022Path = root + "padstack-v2022.pcb";
3090 const wxString v2027Path = root + "padstack-v2027.pcb";
3091
3092 PADS_IO::PARSER ascii;
3095 ascii.Parse( asciiPath );
3096 v2022.Parse( v2022Path );
3097 v2027.Parse( v2027Path );
3098
3099 BOOST_REQUIRE_EQUAL( v2022.GetVersion(), 0x2022 );
3100 BOOST_REQUIRE_EQUAL( v2027.GetVersion(), 0x2027 );
3101
3102 auto tuples = []( const auto& aParser, const std::set<std::string>& aDecalNames )
3103 {
3104 std::vector<std::string> result;
3105
3106 for( const auto& [decalName, decal] : aParser.GetPartDecals() )
3107 {
3108 if( !aDecalNames.contains( decalName ) )
3109 continue;
3110
3111 for( const auto& [pin, stack] : decal.pad_stacks )
3112 {
3113 for( const PADS_IO::PAD_STACK_LAYER& layer : stack )
3114 {
3115 std::ostringstream tuple;
3116 tuple << decalName << ' ' << pin << ' ' << layer.layer << ' ' << layer.shape << ' ' << std::fixed
3117 << std::setprecision( 0 ) << layer.sizeA << ' ' << layer.sizeB << ' '
3118 << layer.thermal_outer_diameter;
3119 result.push_back( tuple.str() );
3120 }
3121 }
3122 }
3123
3124 return result;
3125 };
3126
3127 std::set<std::string> decalNames;
3128
3129 for( const auto& [name, unused] : ascii.GetPartDecals() )
3130 decalNames.insert( name );
3131
3132 const std::vector<std::string> asciiTuples = tuples( ascii, decalNames );
3133 const std::vector<std::string> v2022Tuples = tuples( v2022, decalNames );
3134 const std::vector<std::string> v2027Tuples = tuples( v2027, decalNames );
3135
3136 for( const auto& [label, binaryTuples] : std::array<std::pair<const char*, const std::vector<std::string>*>, 2>{
3137 { { "0x2022", &v2022Tuples }, { "0x2027", &v2027Tuples } } } )
3138 {
3139 BOOST_TEST_CONTEXT( label )
3140 {
3141 BOOST_REQUIRE_EQUAL( binaryTuples->size(), asciiTuples.size() );
3142 BOOST_CHECK_EQUAL_COLLECTIONS( binaryTuples->begin(), binaryTuples->end(), asciiTuples.begin(),
3143 asciiTuples.end() );
3144 }
3145 }
3146
3147 const wxString thermalAsciiPath = root + "padstack-thermal-v2027.asc";
3148 const wxString thermalBinaryPath = root + "padstack-thermal-v2027.pcb";
3149
3150 PADS_IO::PARSER thermalAscii;
3151 PADS_IO::BINARY_PARSER thermalBinary;
3152 thermalAscii.Parse( thermalAsciiPath );
3153 thermalBinary.Parse( thermalBinaryPath );
3154 BOOST_REQUIRE_EQUAL( thermalBinary.GetVersion(), 0x2027 );
3155 decalNames.clear();
3156
3157 for( const auto& [name, unused] : thermalAscii.GetPartDecals() )
3158 decalNames.insert( name );
3159
3160 const std::vector<std::string> thermalAsciiTuples = tuples( thermalAscii, decalNames );
3161 const std::vector<std::string> thermalBinaryTuples = tuples( thermalBinary, decalNames );
3162
3163 BOOST_REQUIRE_EQUAL( thermalBinaryTuples.size(), thermalAsciiTuples.size() );
3164 BOOST_CHECK_EQUAL_COLLECTIONS( thermalBinaryTuples.begin(), thermalBinaryTuples.end(), thermalAsciiTuples.begin(),
3165 thermalAsciiTuples.end() );
3166}
3167
3168
3174BOOST_AUTO_TEST_CASE( ThroughHolePadsAreNotTented )
3175{
3176 size_t plated = 0;
3177 size_t unplated = 0;
3178
3179 auto check = [&]( const std::shared_ptr<BOARD>& aBoard, const std::string& aLabel )
3180 {
3181 for( FOOTPRINT* footprint : aBoard->Footprints() )
3182 {
3183 for( PAD* pad : footprint->Pads() )
3184 {
3185 const std::string key =
3186 aLabel + " " + footprint->GetReference().ToStdString() + "." + pad->GetNumber().ToStdString();
3187
3188 if( pad->GetAttribute() == PAD_ATTRIB::PTH )
3189 {
3190 ++plated;
3191 BOOST_CHECK_MESSAGE( pad->GetLayerSet().test( F_Mask ) && pad->GetLayerSet().test( B_Mask ),
3192 "plated hole imported tented: " << key );
3193 }
3194 else if( pad->GetAttribute() == PAD_ATTRIB::NPTH )
3195 {
3196 ++unplated;
3197 BOOST_CHECK_MESSAGE( ( pad->GetLayerSet() & LSET::InternalCuMask() ).none(),
3198 "unplated hole given inner copper: " << key );
3199 BOOST_CHECK_MESSAGE( pad->GetNetCode() == 0, "unplated hole joined a net: " << key );
3200 }
3201 }
3202 }
3203 };
3204
3205 for( const PADS_BINARY_BOARD_INFO& board : PADS_BINARY_BOARDS )
3206 {
3207 BOOST_TEST_CONTEXT( board.dir )
3208 {
3209 check( LoadBinary( board ), board.dir + " binary" );
3210 check( LoadAsc( board ), board.dir + " ASC" );
3211 }
3212 }
3213
3214 BOOST_CHECK_GT( plated, 0u );
3215 BOOST_CHECK_GT( unplated, 0u );
3216}
3217
3218
3224BOOST_AUTO_TEST_CASE( ThermalReliefRowsReachBinaryPads )
3225{
3226 const wxString root = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/padstack_rows/";
3227
3228 std::shared_ptr<BOARD> binary = LoadBinaryPath( root + "padstack-thermal-v2027.pcb", "padstack-thermal-v2027" );
3229 std::shared_ptr<BOARD> ascii = LoadAscPath( root + "padstack-thermal-v2027.asc", "padstack-thermal-v2027" );
3230
3231 BOOST_REQUIRE( binary != nullptr );
3232 BOOST_REQUIRE( ascii != nullptr );
3233
3234 auto thermalPadKeys = []( const std::shared_ptr<BOARD>& aBoard )
3235 {
3236 std::set<std::string> keys;
3237
3238 for( FOOTPRINT* footprint : aBoard->Footprints() )
3239 {
3240 for( PAD* pad : footprint->Pads() )
3241 {
3242 if( pad->GetLocalZoneConnection() == ZONE_CONNECTION::THERMAL )
3243 keys.insert( footprint->GetReference().ToStdString() + "." + pad->GetNumber().ToStdString() );
3244 }
3245 }
3246
3247 return keys;
3248 };
3249
3250 const std::set<std::string> binKeys = thermalPadKeys( binary );
3251 const std::set<std::string> ascKeys = thermalPadKeys( ascii );
3252
3253 BOOST_REQUIRE_GT( ascKeys.size(), 0u );
3254
3255 std::vector<std::string> missing;
3256 std::set_difference( ascKeys.begin(), ascKeys.end(), binKeys.begin(), binKeys.end(),
3257 std::back_inserter( missing ) );
3258
3259 BOOST_CHECK_MESSAGE( missing.empty(), "binary import lost the plane relief on "
3260 << missing.size() << " pad(s), first "
3261 << ( missing.empty() ? std::string( "-" ) : missing.front() ) );
3262}
3263
3264
3270BOOST_AUTO_TEST_CASE( DimensionLeaderWidthIsUnitModeIndependent )
3271{
3272 PADS_IO::DIMENSION dimension;
3273 dimension.is_horizontal = true;
3274 dimension.points = { { 0.0, 0.0 }, { 1000.0, 0.0 } };
3275 dimension.crossbar_pos = 500.0;
3276
3277 for( bool basicUnits : { false, true } )
3278 {
3279 BOOST_TEST_CONTEXT( std::string( basicUnits ? "BASIC units" : "mils" ) )
3280 {
3281 BOARD board;
3282 PADS_PCB_CONVERTER converter( &board, nullptr );
3283
3284 converter.UnitConverter().SetBasicUnitsMode( basicUnits );
3285 converter.SetScaleFactor( basicUnits ? PADS_UNIT_CONVERTER::BASIC_TO_NM
3287 converter.LoadDimensions( { dimension } );
3288
3289 PCB_DIMENSION_BASE* built = nullptr;
3290
3291 for( BOARD_ITEM* item : board.Drawings() )
3292 {
3293 if( auto* candidate = dynamic_cast<PCB_DIMENSION_BASE*>( item ) )
3294 built = candidate;
3295 }
3296
3297 BOOST_REQUIRE( built );
3298 BOOST_CHECK_EQUAL( built->GetLineThickness(), pcbIUScale.mmToIU( 0.127 ) );
3299 }
3300 }
3301}
3302
3303
3308BOOST_AUTO_TEST_CASE( AsciiCodePageNetclassNamesSurvive )
3309{
3310 const wxString path = KI_TEST::GetPcbnewTestDataDir() + "plugins/pads/synthetic_constraints_cp1252.asc";
3311
3312 std::shared_ptr<BOARD> board = LoadAscPath( path, "synthetic_constraints_cp1252" );
3313
3314 BOOST_REQUIRE( board != nullptr );
3315
3316 std::shared_ptr<NET_SETTINGS> netSettings = board->GetDesignSettings().m_NetSettings;
3317
3318 BOOST_REQUIRE( netSettings );
3319
3320 for( const auto& [name, netclass] : netSettings->GetNetclasses() )
3321 BOOST_CHECK_MESSAGE( !name.IsEmpty(), "a net class imported under a blank name" );
3322
3323 BOOST_CHECK_MESSAGE( netSettings->HasNetclass( wxString::FromUTF8( "HAUTE_VITESSÉ" ) ),
3324 "the CP1252 net class name was lost" );
3325 BOOST_CHECK_MESSAGE( netSettings->HasNetclass( wxString::FromUTF8( "DiffPair_PAIRE_DIFFÉ" ) ),
3326 "the CP1252 differential pair name was lost" );
3327}
int index
const char * name
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:121
General utilities for PCB file IO for QA programs.
BOX2< VECTOR2I > BOX2I
Definition box2.h:927
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:84
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:409
const NETINFO_LIST & GetNetInfo() const
Definition board.h:1207
const FOOTPRINTS & Footprints() const
Definition board.h:463
const TRACKS & Tracks() const
Definition board.h:461
void SetCopperLayerCount(int aCount)
Definition board.cpp:1137
const DRAWINGS & Drawings() const
Definition board.h:465
constexpr BOX2< Vec > & Inflate(coord_type dx, coord_type dy)
Inflates the rectangle horizontally by dx and vertically by dy.
Definition box2.h:553
constexpr size_type GetWidth() const
Definition box2.h:211
constexpr BOX2< Vec > & Merge(const BOX2< Vec > &aRect)
Modify the position and size of the rectangle in order to contain aRect.
Definition box2.h:653
constexpr const Vec GetCenter() const
Definition box2.h:227
constexpr size_type GetHeight() const
Definition box2.h:212
constexpr bool Contains(const Vec &aPoint) const
Definition box2.h:165
EDA_ANGLE Normalize()
Definition eda_angle.h:229
double AsDegrees() const
Definition eda_angle.h:116
A base class for most all the KiCad significant classes used in schematics and boards.
Definition eda_item.h:98
EDA_ANGLE GetOrientation() const
Definition footprint.h:438
std::deque< PAD * > & Pads()
Definition footprint.h:404
bool IsFlipped() const
Definition footprint.h:660
const LIB_ID & GetFPID() const
Definition footprint.h:473
Hold an error message and may be used when throwing exceptions containing meaningful error messages.
const UTF8 & GetLibItemName() const
Definition lib_id.h:98
static const LSET & InternalCuMask()
Return a complete set of internal copper layers which is all Cu layers except F_Cu and B_Cu.
Definition lset.cpp:573
Handle the data for a net.
Definition netinfo.h:50
static constexpr PCB_LAYER_ID ALL_LAYERS
! The layer identifier to use for the single defintion on normal padstacks
Definition padstack.h:179
Reader for the PADS PowerPCB binary .pcb format.
const std::map< std::string, PART_DECAL > & GetPartDecals() const
std::set< std::string > GetPadStackShapesForTest() const
const std::vector< TEXT > & GetTexts() const
void Parse(const wxString &aFileName)
const std::vector< PART > & GetParts() const
std::vector< LAYER_INFO > GetLayerInfos() const
The parsed container of a PADS .pcb file: header, the section directory, and the coordinate system.
Definition pads_sdb.h:111
const SDB_COORDS & Coords() const
Definition pads_sdb.h:133
const SDB_SECTION * Section(int aIndex) const
Definition pads_sdb.cpp:436
uint16_t Version() const
Definition pads_sdb.h:129
size_t SectionCount() const
Definition pads_sdb.h:135
bool IsOldFormat() const
Definition pads_sdb.h:130
void Load(std::vector< uint8_t > aBytes)
Validate the header and footer, parse the section directory, and derive the coordinate origin.
Definition pads_sdb.cpp:60
static bool IsSupportedVersion(uint16_t aVersion)
Definition pads_sdb.cpp:43
const std::map< std::string, PART_DECAL > & GetPartDecals() const
void Parse(const wxString &aFileName)
int32_t DesignY(int32_t aRaw) const
Definition pads_sdb.h:86
bool Found() const
Definition pads_sdb.h:83
uint32_t HeaderBase() const
Absolute file offset of the PCB board-setup parameter block read by PADS_SDB (the origin sits at Head...
Definition pads_sdb.h:92
int32_t OriginY() const
Definition pads_sdb.h:82
int32_t OriginX() const
Definition pads_sdb.h:81
int32_t DesignX(int32_t aRaw) const
Definition pads_sdb.h:85
Builds KiCad board objects from the PADS_IO model structs.
PADS_UNIT_CONVERTER & UnitConverter()
void LoadDimensions(const std::vector< PADS_IO::DIMENSION > &aDimensions)
void SetScaleFactor(double aScaleFactor)
Set the nanometers-per-file-unit factor used by the coordinate transform.
static constexpr double MILS_TO_NM
static constexpr double BASIC_TO_NM
void SetBasicUnitsMode(bool aEnabled)
Enable or disable BASIC units mode.
Definition pad.h:61
Abstract dimension API.
int GetLineThickness() const
A set of BOARD_ITEMs (i.e., without duplicates).
Definition pcb_group.h:51
PCB I/O plugin for importing the native PADS Layout binary .pcb format, distinct from the ASCII forma...
bool CanReadBoard(const wxString &aFileName) const override
Checks if this PCB_IO can read the specified board file.
bool CanReadBoard(const wxString &aFileName) const override
Checks if this PCB_IO can read the specified board file.
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
PCB_LAYER_ID BottomLayer() const
void SetLayerPair(PCB_LAYER_ID aTopLayer, PCB_LAYER_ID aBottomLayer)
For a via m_layer contains the top layer, the other layer is in m_bottomLayer/.
void SetViaType(VIATYPE aViaType)
Definition pcb_track.h:411
PCB_LAYER_ID TopLayer() const
wxString wx_str() const
Definition utf8.cpp:41
Handle a list of polygons defining a copper zone.
Definition zone.h:70
static bool empty(const wxTextEntryBase *aCtrl)
static std::string ToStdString(const wxString &aStr)
bool IsCopperLayer(int aLayerId)
Test whether a layer is a copper layer.
Definition layer_ids.h:703
PCB_LAYER_ID
A quick note on layer IDs:
Definition layer_ids.h:56
@ Edge_Cuts
Definition layer_ids.h:108
@ B_Mask
Definition layer_ids.h:94
@ B_Cu
Definition layer_ids.h:61
@ F_Mask
Definition layer_ids.h:93
@ In2_Cu
Definition layer_ids.h:63
@ In1_Cu
Definition layer_ids.h:62
@ 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.
wxString ConvertText(const std::string &aText)
Decode text from a PADS file, which uses an 8-bit codepage rather than UTF-8.
wxString ConvertInvertedNetName(const std::string &aNetName)
Convert a PADS net name to KiCad notation.
bool ReadFileToBuffer(const wxString &aFileName, std::vector< uint8_t > &aOut)
Read an entire file into aOut.
void ValidateSdbFooter(const std::vector< uint8_t > &aData, size_t aFooterStart, const char *aGuid, size_t aGuidLen)
As CheckSdbFooter, throwing a bare IO_ERROR on failure.
std::string readFixedString(const std::vector< uint8_t > &aData, size_t aOffset, size_t aMaxLen)
As readFixedStringRaw, then trim trailing spaces.
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:411
@ NPTH
like PAD_PTH, but not plated mechanical use only, no connection allowed
Definition padstack.h:102
@ PTH
Plated through hole pad.
Definition padstack.h:97
Class to handle a set of BOARD_ITEMs.
A polyline point that may instead be an arc segment.
Definition pads_parser.h:64
Standalone copper area from the LINES section (type=COPPER), not part of a pour.
double crossbar_pos
Y for horizontal, X for vertical.
std::vector< POINT > points
Measurement endpoints.
The PADS PowerPCB .pcb file is a serialized snapshot of PADS' in-memory SDB (System DataBase) object ...
Definition pads_sdb.h:58
uint32_t physicalLiveCount
Definition pads_sdb.h:67
uint32_t physicalCount
Definition pads_sdb.h:66
uint32_t physicalOffset
Definition pads_sdb.h:64
uint32_t physicalBytes
Definition pads_sdb.h:65
uint32_t totalBytes
Definition pads_sdb.h:61
std::vector< ARC_POINT > points
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_CHECK_EQUAL_COLLECTIONS(mixed.begin(), mixed.end(), expMixed.begin(), expMixed.end())
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
std::string path
KIBIS_PIN * pin
VECTOR3I expected(15, 30, 45)
static int CountEdgeCutsShapes(const BOARD *aBoard)
static size_t CountVias(const std::shared_ptr< BOARD > &aBoard)
#define BINARY_LOAD_TEST(name, idx)
#define NET_NAMES_EXACT_TEST(name, idx)
static std::shared_ptr< BOARD > LoadAsc(const PADS_BINARY_BOARD_INFO &aBoard)
#define FOOTPRINT_COUNT_TEST(name, idx)
#define STRUCTURAL_INTEGRITY_TEST(name, idx)
static std::string FlattenPath(const std::filesystem::path &aRoot, const std::filesystem::path &aFile)
static bool HasFreeText(const BOARD *aBoard, const wxString &aText)
static wxString JoinNetSet(const std::set< wxString > &aNames)
#define ROUTED_TRACKS_TEST(name, idx)
static int CountEdgeCutsArcs(const BOARD *aBoard)
static std::shared_ptr< BOARD > LoadAscPath(const wxString &aFilename, const std::string &aLabel)
BOOST_AUTO_TEST_CASE(CanonicalGeometryRowsCoverPadsTracksAndVias)
static wxString GetAscPath(const PADS_BINARY_BOARD_INFO &aBoard)
static std::shared_ptr< BOARD > LoadBinary(const PADS_BINARY_BOARD_INFO &aBoard)
static std::pair< size_t, size_t > ExactNettedTrackMatches(const wxString &aBinaryPath, const wxString &aAsciiPath, const std::string &aLabel)
static std::shared_ptr< BOARD > CachedLoad(const wxString &aFilename, bool aBinary)
Load a binary .pcb file.
static size_t DistinctPadGeometries(BOARD *aBoard)
#define FREE_TEXT_EXACT_TEST(name, idx)
Free-text extraction is solved for these boards (sec5/sec8 text-header stream with metadata lagging g...
static std::vector< std::string > CanonicalGeometryRows(const BOARD *aBoard)
#define FREE_TEXT_COUNT_TEST(name, idx)
#define NET_COUNT_TEST(name, idx)
#define PAD_COUNT_TEST(name, idx)
#define PER_FOOTPRINT_CONTENT_TEST(name, idx, maxNameWrong, maxCountWrong)
Per-footprint content correctness over the RESOLVED set.
static FOOTPRINT * FindFootprintByReference(const BOARD *aBoard, const wxString &aReference)
static std::shared_ptr< BOARD > LoadBinaryPath(const wxString &aFilename, const std::string &aLabel)
static std::pair< size_t, size_t > ExactViaMatches(const wxString &aBinaryPath, const wxString &aAsciiPath, const std::string &aLabel)
static std::pair< size_t, size_t > ExactLayeredTrackMatches(const wxString &aBinaryPath, const wxString &aAsciiPath, const std::string &aLabel)
static void CheckArcOutlineMatchesAsc(int aIdx)
static std::set< wxString > BoardNetNames(const BOARD *aBoard)
static size_t CountTraces(const std::shared_ptr< BOARD > &aBoard)
static void CheckCountWithTolerance(const std::string &aLabel, size_t aBinaryCount, size_t aAscCount, bool aDifferentRevision)
Compare counts with tolerance for binary/ASC differences.
static const PADS_BINARY_BOARD_INFO PADS_BINARY_BOARDS[]
static wxString GetBinaryPath(const PADS_BINARY_BOARD_INFO &aBoard)
static size_t CountFreeTexts(const BOARD *aBoard)
static BOX2I EdgeCutsBBox(const BOARD *aBoard)
#define ZONE_COUNT_TEST(name, idx)
static void RunStructuralChecks(const PADS_BINARY_BOARD_INFO &aBoard, const BOARD *aBinaryBoard)
Structural integrity checks.
#define WIDTH_VARIETY_TEST(name, idx)
Verify that the binary import produces multiple distinct track widths when the ASCII reference file h...
VECTOR2I end
BOOST_TEST_CONTEXT("Test Clearance")
int clearance
BOOST_TEST_MESSAGE("Polyline has "<< chain.PointCount()<< " points")
int actual
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")
@ PCB_VIA_T
class PCB_VIA, a via (like a track segment on a copper layer)
Definition typeinfo.h:89
@ PCB_FOOTPRINT_T
class FOOTPRINT, a footprint
Definition typeinfo.h:78
@ PCB_ARC_T
class PCB_ARC, an arc track segment on a copper layer
Definition typeinfo.h:90
@ PCB_TRACE_T
class PCB_TRACK, a track segment (segment on a copper layer)
Definition typeinfo.h:88
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683
@ THERMAL
Use thermal relief for pads.
Definition zones.h:46
@ FULL
pads are covered by copper
Definition zones.h:47