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