KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_allegro_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 The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, you may find one here:
18 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
19 * or you may search the http://www.gnu.org website for the version 2 license,
20 * or you may write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
22 */
23
28
29#include "allegro_test_utils.h"
33
35
36#include <board.h>
38#include <footprint.h>
39#include <pad.h>
40#include <pcb_shape.h>
41#include <pcb_text.h>
42#include <pcb_track.h>
43#include <zone.h>
44#include <netinfo.h>
45#include <netclass.h>
48#include <reporter.h>
49
50#include <filesystem>
51#include <fstream>
52#include <map>
53#include <set>
54
55using namespace KI_TEST;
56
57
59{
61
62 std::unique_ptr<BOARD> LoadAllegroBoard( const std::string& aFileName )
63 {
64 std::string dataPath = KI_TEST::AllegroBoardFile( aFileName );
65
66 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
67 m_allegroPlugin.LoadBoard( dataPath, board.get(), nullptr, nullptr );
68
69 return board;
70 }
71
73};
74
75
76BOOST_FIXTURE_TEST_SUITE( AllegroImport, ALLEGRO_IMPORT_FIXTURE )
77
78
79
82BOOST_AUTO_TEST_CASE( FootprintRefDes )
83{
84 std::unique_ptr<BOARD> board = LoadAllegroBoard( "TRS80_POWER/TRS80_POWER.brd" );
85
86 BOOST_REQUIRE( board != nullptr );
87
88 int emptyRefDesCount = 0;
89 int validRefDesCount = 0;
90
91 for( FOOTPRINT* fp : board->Footprints() )
92 {
93 wxString refdes = fp->GetReference();
94
95 if( refdes.IsEmpty() )
96 emptyRefDesCount++;
97 else
98 validRefDesCount++;
99 }
100
101 BOOST_TEST_MESSAGE( "Valid RefDes: " << validRefDesCount << ", Empty: " << emptyRefDesCount );
102
103 // Most footprints should have valid reference designators
104 BOOST_CHECK_GT( validRefDesCount, emptyRefDesCount );
105}
106
107
112{
113 std::unique_ptr<BOARD> board = LoadAllegroBoard( "TRS80_POWER/TRS80_POWER.brd" );
114
115 BOOST_REQUIRE( board != nullptr );
116
117 int validPadCount = 0;
118 int zeroPadCount = 0;
119 int hugePadCount = 0;
120
121 for( FOOTPRINT* fp : board->Footprints() )
122 {
123 for( PAD* pad : fp->Pads() )
124 {
125 VECTOR2I size = pad->GetSize( F_Cu );
126
127 if( size.x == 0 || size.y == 0 )
128 {
129 zeroPadCount++;
130 BOOST_TEST_MESSAGE( "Zero-size pad in " << fp->GetReference() << " pad "
131 << pad->GetNumber() );
132 }
133 else if( size.x > 50000000 || size.y > 50000000 ) // > 50mm is suspicious
134 {
135 hugePadCount++;
136 BOOST_TEST_MESSAGE( "Huge pad in " << fp->GetReference() << " pad " << pad->GetNumber()
137 << ": " << size.x / 1000000.0 << "mm x "
138 << size.y / 1000000.0 << "mm" );
139 }
140 else
141 {
142 validPadCount++;
143 }
144 }
145 }
146
147 BOOST_TEST_MESSAGE( "Valid pads: " << validPadCount << ", Zero: " << zeroPadCount
148 << ", Huge: " << hugePadCount );
149
150 // No pads should be zero-size (this catches hardcoded fallbacks)
151 BOOST_CHECK_EQUAL( zeroPadCount, 0 );
152}
153
154
159{
160 std::unique_ptr<BOARD> board = LoadAllegroBoard( "TRS80_POWER/TRS80_POWER.brd" );
161
162 BOOST_REQUIRE( board != nullptr );
163
164 int validViaCount = 0;
165 int suspiciousViaCount = 0;
166
167 // Count vias with the hardcoded size (1000000 = 1mm exactly)
168 const int HARDCODED_SIZE = 1000000;
169
170 for( PCB_TRACK* track : board->Tracks() )
171 {
172 if( track->Type() == PCB_VIA_T )
173 {
174 PCB_VIA* via = static_cast<PCB_VIA*>( track );
175 int width = via->GetWidth( F_Cu );
176
177 if( width == HARDCODED_SIZE )
178 {
179 suspiciousViaCount++;
180 }
181 else if( width > 0 && width < 10000000 ) // 0 < size < 10mm is reasonable
182 {
183 validViaCount++;
184 }
185 }
186 }
187
188 BOOST_TEST_MESSAGE( "Valid vias: " << validViaCount << ", Hardcoded-size vias: " << suspiciousViaCount );
189
190 // This test will fail until via size is properly extracted from padstack
191 if( suspiciousViaCount > 0 )
192 {
193 BOOST_WARN_MESSAGE( false, "Found " << suspiciousViaCount << " vias with hardcoded 1mm size" );
194 }
195}
196
197
202{
203 std::unique_ptr<BOARD> board = LoadAllegroBoard( "TRS80_POWER/TRS80_POWER.brd" );
204
205 BOOST_REQUIRE( board != nullptr );
206
207 int validTrackCount = 0;
208 int zeroTrackCount = 0;
209
210 for( PCB_TRACK* track : board->Tracks() )
211 {
212 if( track->Type() == PCB_TRACE_T )
213 {
214 int width = track->GetWidth();
215
216 if( width == 0 )
217 zeroTrackCount++;
218 else if( width > 0 && width < 10000000 ) // 0 < width < 10mm
219 validTrackCount++;
220 }
221 }
222
223 BOOST_TEST_MESSAGE( "Valid tracks: " << validTrackCount << ", Zero-width: " << zeroTrackCount );
224
225 BOOST_CHECK_EQUAL( zeroTrackCount, 0 );
226}
227
228
233{
234 std::unique_ptr<BOARD> board = LoadAllegroBoard( "TRS80_POWER/TRS80_POWER.brd" );
235
236 BOOST_REQUIRE( board != nullptr );
237
238 int numberedPads = 0;
239 int unnumberedPads = 0;
240
241 for( FOOTPRINT* fp : board->Footprints() )
242 {
243 for( PAD* pad : fp->Pads() )
244 {
245 if( pad->GetNumber().IsEmpty() )
246 unnumberedPads++;
247 else
248 numberedPads++;
249 }
250 }
251
252 BOOST_TEST_MESSAGE( "Numbered pads: " << numberedPads << ", Unnumbered: " << unnumberedPads );
253
254 // All pads should have numbers for proper netlist generation
255 // This test will fail until pad numbers are properly set
256 if( unnumberedPads > 0 )
257 {
258 BOOST_WARN_MESSAGE( false, "Found " << unnumberedPads << " pads without numbers" );
259 }
260}
261
262
263static unsigned CountOutlineElements( const BOARD& board )
264{
265 unsigned count = 0;
266 for( const BOARD_ITEM* item : board.Drawings() )
267 {
268 if( item->Type() == PCB_SHAPE_T && item->GetLayer() == Edge_Cuts )
269 {
270 count++;
271 }
272 }
273 return count;
274}
275
276
277static void AssertOutlineValid( const BOARD& aBoard )
278{
279 // Verify outline forms a closed contour by checking that all segments connect
280 std::vector<SEG> outlineSegs;
281
282 for( BOARD_ITEM* item : aBoard.Drawings() )
283 {
284 if( item->Type() == PCB_SHAPE_T && item->GetLayer() == Edge_Cuts )
285 {
286 PCB_SHAPE* shape = static_cast<PCB_SHAPE*>( item );
287 switch( shape->GetShape() )
288 {
289 case SHAPE_T::SEGMENT:
290 case SHAPE_T::ARC:
291 case SHAPE_T::BEZIER:
292 {
293 outlineSegs.push_back( SEG( shape->GetStart(), shape->GetEnd() ) );
294 break;
295 }
297 {
298 // Rectangles are stored as a single item but represent 4 segments
299 VECTOR2I start = shape->GetStart();
300 VECTOR2I end = shape->GetEnd();
301
302 for( const auto& seg : KIGEOM::BoxToSegs( BOX2I( start, end ) ) )
303 {
304 outlineSegs.push_back( seg );
305 }
306 break;
307 }
308 case SHAPE_T::POLY:
309 {
310 std::vector<VECTOR2I> polyPoints = shape->GetPolyPoints();
311
312 for( size_t i = 0; i < polyPoints.size() - 1; i++ )
313 {
314 VECTOR2I start = polyPoints[i];
315 VECTOR2I end = polyPoints[( i + 1 ) % polyPoints.size()];
316 outlineSegs.emplace_back( start, end );
317 }
318 break;
319 }
320 case SHAPE_T::CIRCLE:
321 // Not really sure what we can do here? Zero-length seg?
322 outlineSegs.push_back( SEG( shape->GetStart(), shape->GetStart() ) );
323 break;
324 default:
325 BOOST_WARN_MESSAGE(
326 false, "Unexpected shape type in board outline: " << static_cast<int>( shape->GetShape() ) );
327 }
328 }
329 }
330
331 if( !outlineSegs.empty() )
332 {
333 // For a valid closed outline, the sum of all segment lengths should equal the perimeter
334 // and each endpoint should connect to another endpoint
335 int connectedCount = 0;
336
337 for( const SEG& seg : outlineSegs )
338 {
339 for( const SEG& other : outlineSegs )
340 {
341 if( &other == &seg )
342 continue;
343
344 // Check if this shape's start connects to another shape's start or end
345 if( seg.A == other.A || seg.A == other.B )
346 connectedCount++;
347
348 // Check if this shape's end connects to another shape's start or end
349 if( seg.B == other.A || seg.B == other.B )
350 connectedCount++;
351 }
352 }
353
354 // Each segment should connect at both ends for a closed outline
355 // For 4 segments, we expect 8 connections (2 per segment)
356 BOOST_TEST_MESSAGE( "Connected endpoints: " << connectedCount );
357 BOOST_CHECK_GE( connectedCount, outlineSegs.size() * 2 );
358 }
359}
360
361
365BOOST_AUTO_TEST_CASE( BoardOutline )
366{
367 std::unique_ptr<BOARD> board = LoadAllegroBoard( "TRS80_POWER/TRS80_POWER.brd" );
368
369 BOOST_REQUIRE( board != nullptr );
370
371 // Count shapes on Edge_Cuts layer
372 int outlineSegmentCount = CountOutlineElements( *board );
373
374 BOOST_TEST_MESSAGE( "Board outline elements: " << outlineSegmentCount );
375
376 // Board should have an outline - TRS80_POWER.brd has a rectangular outline (4 segments)
377 BOOST_CHECK_GE( outlineSegmentCount, 1 );
378
379 AssertOutlineValid( *board );
380}
381
382
386BOOST_AUTO_TEST_CASE( PadsInsideOutline )
387{
388 std::unique_ptr<BOARD> board = LoadAllegroBoard( "TRS80_POWER/TRS80_POWER.brd" );
389
390 BOOST_REQUIRE( board != nullptr );
391
392 // Get board bounding box from outline
393 BOX2I boardBbox;
394 bool hasBbox = false;
395
396 for( BOARD_ITEM* item : board->Drawings() )
397 {
398 if( item->Type() == PCB_SHAPE_T && item->GetLayer() == Edge_Cuts )
399 {
400 if( !hasBbox )
401 {
402 boardBbox = item->GetBoundingBox();
403 hasBbox = true;
404 }
405 else
406 {
407 boardBbox.Merge( item->GetBoundingBox() );
408 }
409 }
410 }
411
412 BOOST_REQUIRE_MESSAGE( hasBbox, "Board should have an outline" );
413
414 int padsInside = 0;
415 int padsOutside = 0;
416
417 // Inflate bbox slightly to account for edge cases
418 BOX2I testBbox = boardBbox;
419 testBbox.Inflate( 1000 ); // 1mm tolerance
420
421 for( FOOTPRINT* fp : board->Footprints() )
422 {
423 for( PAD* pad : fp->Pads() )
424 {
425 VECTOR2I padCenter = pad->GetPosition();
426
427 if( testBbox.Contains( padCenter ) )
428 {
429 padsInside++;
430 }
431 else
432 {
433 padsOutside++;
434 BOOST_TEST_MESSAGE( "Pad outside outline: " << fp->GetReference() << " pad "
435 << pad->GetNumber() << " at ("
436 << padCenter.x / 1000000.0 << ", "
437 << padCenter.y / 1000000.0 << ") mm" );
438 }
439 }
440 }
441
442 BOOST_TEST_MESSAGE( "Pads inside outline: " << padsInside << ", outside: " << padsOutside );
443
444 // Most pads should be inside the board outline
445 // Some boards may have off-board test points or fiducials
446 if( padsOutside > 0 )
447 {
448 BOOST_WARN_MESSAGE( false, "Found " << padsOutside << " pads outside board outline" );
449 }
450
451 // At minimum, most pads should be inside
452 BOOST_CHECK_GT( padsInside, padsOutside );
453}
454
455
460BOOST_AUTO_TEST_CASE( PreV16FileRejection )
461{
462 BOOST_CHECK_EXCEPTION(
463 LoadAllegroBoard( "v13_header/v13_header.brd" ), IO_ERROR,
464 []( const IO_ERROR& e )
465 {
466 wxString msg = e.What();
467
468 return msg.Contains( wxS( "predates Allegro 16.0" ) )
469 && msg.Contains( wxS( "Allegro PCB Design" ) );
470 } );
471}
472
473
479BOOST_AUTO_TEST_CASE( RectsZoneVsCopperPolygon )
480{
481 std::unique_ptr<BOARD> board = LoadAllegroBoard( "rects/rects.brd" );
482 BOOST_REQUIRE( board );
483
484 // Should have exactly one zone (the left rectangle as a zone fill)
485 BOOST_CHECK_EQUAL( board->Zones().size(), 1 );
486
487 ZONE* zone = board->Zones().front();
488 BOOST_CHECK( zone->GetNetCode() > 0 );
489 BOOST_CHECK( IsCopperLayer( zone->GetFirstLayer() ) );
490 BOOST_CHECK( zone->IsFilled() );
491
492 // Should have exactly one standalone copper polygon (the right rectangle)
493 int copperPolyCount = 0;
494 int copperPolyWithNet = 0;
495
496 for( BOARD_ITEM* item : board->Drawings() )
497 {
498 if( item->Type() == PCB_SHAPE_T )
499 {
500 PCB_SHAPE* shape = static_cast<PCB_SHAPE*>( item );
501
502 if( IsCopperLayer( shape->GetLayer() ) && shape->GetShape() == SHAPE_T::POLY )
503 {
504 copperPolyCount++;
505
506 if( shape->GetNetCode() > 0 )
507 copperPolyWithNet++;
508 }
509 }
510 }
511
512 BOOST_CHECK_EQUAL( copperPolyCount, 1 );
513 BOOST_CHECK_EQUAL( copperPolyWithNet, 1 );
514}
515
516
521{
522 std::unique_ptr<BOARD> board = LoadAllegroBoard( "copper_text/copper_text.brd" );
523 BOOST_REQUIRE( board );
524
525 int copperTextCount = 0;
526 bool foundTestingText = false;
527
528 for( BOARD_ITEM* item : board->Drawings() )
529 {
530 if( item->Type() == PCB_TEXT_T )
531 {
532 PCB_TEXT* text = static_cast<PCB_TEXT*>( item );
533
534 if( IsCopperLayer( text->GetLayer() ) )
535 {
536 copperTextCount++;
537
538 if( text->GetText() == wxS( "TESTING" ) )
539 {
540 foundTestingText = true;
541 BOOST_CHECK_EQUAL( text->GetLayer(), F_Cu );
542 }
543 }
544 }
545 }
546
547 BOOST_CHECK_MESSAGE( foundTestingText, "Board should contain 'TESTING' text on F.Cu" );
548 BOOST_CHECK_EQUAL( copperTextCount, 1 );
549}
550
551
553
554
555
559{
560 std::string filename;
561 bool expected_to_load; // Set false for known-broken boards
562};
563
564
569{
571
577 BOARD* GetCachedBoard( const std::string& aFilePath )
578 {
580 }
581
585 static std::vector<std::string> GetAllBoardFiles()
586 {
587 std::vector<std::string> boards;
588 std::string dataPath = KI_TEST::AllegroBoardDataDir( "" );
589
590 // For each board dir, look for .brd files and add them to the list of test cases
591 try
592 {
593 for( const auto& boardDir : std::filesystem::directory_iterator( dataPath ) )
594 {
595 if( !boardDir.is_directory() )
596 continue;
597
598 for( const auto& entry : std::filesystem::directory_iterator( boardDir ) )
599 {
600 if( entry.is_regular_file() && entry.path().extension() == ".brd" && entry.file_size() > 0 )
601 {
602 std::string name = entry.path().filename().string();
603
604 // v13_header.brd is intentionally pre-v16 and tested separately
605 if( name != "v13_header.brd" )
606 {
607 boards.push_back( boardDir.path().string() + "/" + name );
608 }
609 }
610 }
611 }
612 }
613 catch( const std::filesystem::filesystem_error& e )
614 {
615 BOOST_TEST_MESSAGE( "Failed to enumerate board files: " << e.what() );
616 }
617
618 std::sort( boards.begin(), boards.end() );
619 return boards;
620 }
621
623};
624
625BOOST_FIXTURE_TEST_SUITE( AllegroComprehensive, ALLEGRO_COMPREHENSIVE_FIXTURE )
626
627
628
633BOOST_AUTO_TEST_CASE( BeagleBone_OutermostZoneNets )
634{
635 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
636
637 BOARD* board = GetCachedBoard( dataPath );
638 BOOST_REQUIRE( board );
639
640 const std::vector<wxString> expectedLayers = { wxS( "TOP" ), wxS( "LYR2_GND" ),
641 wxS( "LYR5_PWR" ), wxS( "BOTTOM" ) };
642
643 for( const wxString& layerName : expectedLayers )
644 {
645 BOOST_TEST_CONTEXT( "Outermost zone on " << layerName )
646 {
647 PCB_LAYER_ID layerId = board->GetLayerID( layerName );
648
649 BOOST_REQUIRE_MESSAGE( layerId != UNDEFINED_LAYER,
650 "Layer " << layerName << " should exist" );
651
652 const ZONE* largest = nullptr;
653 double largestArea = 0;
654
655 for( const ZONE* zone : board->Zones() )
656 {
657 if( zone->GetIsRuleArea() )
658 continue;
659
660 if( zone->GetNetCode() == 0 )
661 continue;
662
663 if( !zone->GetLayerSet().Contains( layerId ) )
664 continue;
665
666 BOX2I bbox = zone->GetBoundingBox();
667 double area = static_cast<double>( bbox.GetWidth() )
668 * static_cast<double>( bbox.GetHeight() );
669
670 if( area > largestArea )
671 {
672 largestArea = area;
673 largest = zone;
674 }
675 }
676
677 BOOST_REQUIRE_MESSAGE( largest != nullptr,
678 "Should find a netted copper zone on " << layerName );
679 BOOST_CHECK_EQUAL( largest->GetNetname(), wxString( wxS( "GND_EARTH" ) ) );
680 }
681 }
682}
683
684
688BOOST_AUTO_TEST_CASE( PadSizesPositive )
689{
690 std::vector<std::string> boards = GetAllBoardFiles();
691
692 for( const std::string& boardPath : boards )
693 {
694 std::string boardName = std::filesystem::path( boardPath ).filename().string();
695 BOARD* board = GetCachedBoard( boardPath );
696
697 if( !board )
698 continue;
699
700 BOOST_TEST_CONTEXT( "Testing board: " << boardName )
701 {
702 int negativePadCount = 0;
703
704 for( FOOTPRINT* fp : board->Footprints() )
705 {
706 for( PAD* pad : fp->Pads() )
707 {
708 VECTOR2I size = pad->GetSize( F_Cu );
709
710 if( size.x < 0 || size.y < 0 )
711 {
712 negativePadCount++;
713 BOOST_TEST_MESSAGE( boardName << ": Negative pad size in " << fp->GetReference()
714 << " pad " << pad->GetNumber() << ": " << size.x << " x "
715 << size.y );
716 }
717 }
718 }
719
720 BOOST_CHECK_EQUAL( negativePadCount, 0 );
721 }
722 }
723}
724
725
730BOOST_AUTO_TEST_CASE( ViaDrillNotLargerThanSize )
731{
732 std::vector<std::string> boards = GetAllBoardFiles();
733
734 for( const std::string& boardPath : boards )
735 {
736 std::string boardName = std::filesystem::path( boardPath ).filename().string();
737 BOARD* board = GetCachedBoard( boardPath );
738
739 if( !board )
740 continue;
741
742 BOOST_TEST_CONTEXT( "Testing board: " << boardName )
743 {
744 int invalidViaCount = 0;
745
746 for( PCB_TRACK* track : board->Tracks() )
747 {
748 if( track->Type() == PCB_VIA_T )
749 {
750 PCB_VIA* via = static_cast<PCB_VIA*>( track );
751 int drill = via->GetDrill();
752 int width = via->GetWidth( F_Cu );
753
754 if( drill > width )
755 {
756 invalidViaCount++;
757 BOOST_TEST_MESSAGE( boardName << ": Via at ("
758 << via->GetPosition().x / 1000000.0 << ", "
759 << via->GetPosition().y / 1000000.0
760 << ") has drill " << drill / 1000000.0
761 << "mm > width " << width / 1000000.0 << "mm" );
762 }
763 }
764 }
765
766 BOOST_CHECK_EQUAL( invalidViaCount, 0 );
767 }
768 }
769}
770
771
776BOOST_AUTO_TEST_CASE( SmdPadDetection )
777{
778 std::vector<std::string> boards = GetAllBoardFiles();
779
780 for( const std::string& boardPath : boards )
781 {
782 std::string boardName = std::filesystem::path( boardPath ).filename().string();
783 BOARD* board = GetCachedBoard( boardPath );
784
785 if( !board )
786 continue;
787
788 BOOST_TEST_CONTEXT( "Testing board: " << boardName )
789 {
790 int misclassifiedSmdCount = 0;
791 int correctSmdCount = 0;
792 int correctThCount = 0;
793
794 for( FOOTPRINT* fp : board->Footprints() )
795 {
796 for( PAD* pad : fp->Pads() )
797 {
798 bool hasDrill = pad->GetDrillSizeX() > 0 && pad->GetDrillSizeY() > 0;
799 PAD_ATTRIB attr = pad->GetAttribute();
800
801 if( !hasDrill && attr == PAD_ATTRIB::PTH )
802 {
803 misclassifiedSmdCount++;
804 BOOST_TEST_MESSAGE( boardName << ": Pad " << fp->GetReference()
805 << "." << pad->GetNumber()
806 << " has no drill but is marked as PTH (should be SMD)" );
807 }
808 else if( !hasDrill && attr == PAD_ATTRIB::SMD )
809 {
810 correctSmdCount++;
811 }
812 else if( hasDrill && ( attr == PAD_ATTRIB::PTH || attr == PAD_ATTRIB::NPTH ) )
813 {
814 correctThCount++;
815 }
816 }
817 }
818
819 BOOST_TEST_MESSAGE( boardName << ": Correct SMD=" << correctSmdCount
820 << ", Correct TH=" << correctThCount
821 << ", Misclassified=" << misclassifiedSmdCount );
822
823 BOOST_CHECK_EQUAL( misclassifiedSmdCount, 0 );
824 }
825 }
826}
827
828
833BOOST_AUTO_TEST_CASE( QuadPackagePadRotation )
834{
835 std::vector<std::string> boards = GetAllBoardFiles();
836
837 for( const std::string& boardPath : boards )
838 {
839 std::string boardName = std::filesystem::path( boardPath ).filename().string();
840 BOARD* board = GetCachedBoard( boardPath );
841
842 if( !board )
843 continue;
844
845 BOOST_TEST_CONTEXT( "Testing board: " << boardName )
846 {
847 int quadPackageCount = 0;
848 int packagesWithRotatedPads = 0;
849 int packagesWithUnrotatedPads = 0;
850
851 for( FOOTPRINT* fp : board->Footprints() )
852 {
853 wxString refdes = fp->GetReference().Upper();
854
855 // Look for ICs (typically U* prefix) that might be quad packages
856 if( !refdes.StartsWith( "U" ) )
857 continue;
858
859 // Must have at least 16 pads to be a quad package
860 if( fp->Pads().size() < 16 )
861 continue;
862
863 // Find bounding box of all pad centers to estimate package shape
864 BOX2I padBounds;
865 bool first = true;
866
867 for( PAD* pad : fp->Pads() )
868 {
869 VECTOR2I pos = pad->GetPosition();
870
871 if( first )
872 {
873 padBounds = BOX2I( pos, VECTOR2I( 0, 0 ) );
874 first = false;
875 }
876 else
877 {
878 padBounds.Merge( pos );
879 }
880 }
881
882 // Must be roughly square to be a quad package
883 int width = padBounds.GetWidth();
884 int height = padBounds.GetHeight();
885
886 if( width == 0 || height == 0 )
887 continue;
888
889 double aspectRatio = static_cast<double>( std::max( width, height ) ) /
890 static_cast<double>( std::min( width, height ) );
891
892 if( aspectRatio > 2.0 )
893 continue;
894
895 quadPackageCount++;
896
897 // Check if pads have varying orientations
898 std::set<int> uniqueAngles;
899
900 for( PAD* pad : fp->Pads() )
901 {
902 EDA_ANGLE angle = pad->GetOrientation();
903 angle.Normalize();
904 int degrees = static_cast<int>( angle.AsDegrees() + 0.5 ) % 360;
905 uniqueAngles.insert( degrees );
906 }
907
908 // A properly imported quad package should have at least 2 different pad orientations
909 // (for 2-sided packages) or 4 (for 4-sided packages like QFP)
910 if( uniqueAngles.size() >= 2 )
911 {
912 packagesWithRotatedPads++;
913 BOOST_TEST_MESSAGE( boardName << ": " << fp->GetReference()
914 << " has " << uniqueAngles.size() << " unique pad orientations" );
915 }
916 else
917 {
918 packagesWithUnrotatedPads++;
919 BOOST_TEST_MESSAGE( boardName << ": " << fp->GetReference()
920 << " has only " << uniqueAngles.size()
921 << " unique pad orientation (may be missing rotation)" );
922 }
923 }
924
925 if( quadPackageCount > 0 )
926 {
927 BOOST_TEST_MESSAGE( boardName << ": Found " << quadPackageCount
928 << " potential quad packages, "
929 << packagesWithRotatedPads << " with rotated pads, "
930 << packagesWithUnrotatedPads << " without" );
931
932 // At least some packages should have rotated pads to confirm rotation parsing works.
933 // Many packages may legitimately have all pads at the same orientation (BGAs, single-row).
934 if( packagesWithRotatedPads == 0 && quadPackageCount > 0 )
935 {
936 BOOST_WARN_MESSAGE( false, boardName << " has no packages with rotated pads" );
937 }
938 }
939 }
940 }
941}
942
943
949BOOST_AUTO_TEST_CASE( FootprintLayerPlacement )
950{
951 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
952
953 BOARD* board = GetCachedBoard( dataPath );
954 BOOST_REQUIRE_MESSAGE( board != nullptr, "BeagleBone_Black_RevC.brd should load successfully" );
955
956 // Look for C78 which should be on the bottom layer
957 FOOTPRINT* c78 = nullptr;
958
959 for( FOOTPRINT* fp : board->Footprints() )
960 {
961 if( fp->GetReference() == "C78" )
962 {
963 c78 = fp;
964 break;
965 }
966 }
967
968 BOOST_REQUIRE_MESSAGE( c78 != nullptr, "Footprint C78 should exist in BeagleBone Black" );
969
970 PCB_LAYER_ID fpLayer = c78->GetLayer();
971
972 BOOST_TEST_MESSAGE( "C78 layer: " << board->GetLayerName( fpLayer ) << " (ID: " << fpLayer << ")" );
973 BOOST_TEST_MESSAGE( "C78 is flipped: " << ( c78->IsFlipped() ? "yes" : "no" ) );
974
975 BOOST_CHECK_MESSAGE( fpLayer == B_Cu, "C78 should be on the bottom copper layer (B_Cu), got "
976 << board->GetLayerName( fpLayer ) );
977 BOOST_CHECK_MESSAGE( c78->IsFlipped(), "C78 should be flipped (IsFlipped() == true)" );
978
979 // Count footprints on top vs bottom to ensure we're parsing layer correctly
980 int topCount = 0;
981 int bottomCount = 0;
982
983 for( FOOTPRINT* fp : board->Footprints() )
984 {
985 if( fp->GetLayer() == F_Cu )
986 topCount++;
987 else if( fp->GetLayer() == B_Cu )
988 bottomCount++;
989 }
990
991 BOOST_TEST_MESSAGE( "Footprints on top: " << topCount << ", on bottom: " << bottomCount );
992
993 // BeagleBone should have components on both sides
994 BOOST_CHECK_GT( topCount, 0 );
995 BOOST_CHECK_GT( bottomCount, 0 );
996}
997
998
1003BOOST_AUTO_TEST_CASE( ArcConnectivity )
1004{
1005 std::vector<std::string> boards = GetAllBoardFiles();
1006
1007 for( const std::string& boardPath : boards )
1008 {
1009 std::string boardName = std::filesystem::path( boardPath ).filename().string();
1010 BOARD* board = GetCachedBoard( boardPath );
1011
1012 if( !board )
1013 continue;
1014
1015 BOOST_TEST_CONTEXT( "Testing board: " << boardName )
1016 {
1017 int arcCount = 0;
1018 int disconnectedArcs = 0;
1019
1020 // Build a map of track endpoints per net for quick lookup
1021 std::map<int, std::vector<VECTOR2I>> netEndpoints;
1022
1023 for( PCB_TRACK* track : board->Tracks() )
1024 {
1025 int netCode = track->GetNetCode();
1026
1027 if( track->Type() == PCB_TRACE_T )
1028 {
1029 netEndpoints[netCode].push_back( track->GetStart() );
1030 netEndpoints[netCode].push_back( track->GetEnd() );
1031 }
1032 else if( track->Type() == PCB_VIA_T )
1033 {
1034 netEndpoints[netCode].push_back( track->GetPosition() );
1035 }
1036 }
1037
1038 // Also include pad positions
1039 for( FOOTPRINT* fp : board->Footprints() )
1040 {
1041 for( PAD* pad : fp->Pads() )
1042 {
1043 int netCode = pad->GetNetCode();
1044
1045 if( netCode > 0 )
1046 netEndpoints[netCode].push_back( pad->GetPosition() );
1047 }
1048 }
1049
1050 // Now check each arc
1051 for( PCB_TRACK* track : board->Tracks() )
1052 {
1053 if( track->Type() != PCB_ARC_T )
1054 continue;
1055
1056 arcCount++;
1057 PCB_ARC* arc = static_cast<PCB_ARC*>( track );
1058 int netCode = arc->GetNetCode();
1059
1060 VECTOR2I arcStart = arc->GetStart();
1061 VECTOR2I arcEnd = arc->GetEnd();
1062
1063 // Check if arc endpoints connect to something
1064 bool startConnected = false;
1065 bool endConnected = false;
1066 const int tolerance = 1000; // 1um tolerance
1067
1068 for( const VECTOR2I& pt : netEndpoints[netCode] )
1069 {
1070 if( ( pt - arcStart ).EuclideanNorm() < tolerance )
1071 startConnected = true;
1072
1073 if( ( pt - arcEnd ).EuclideanNorm() < tolerance )
1074 endConnected = true;
1075 }
1076
1077 // Arc should connect to at least one other track/pad at each end
1078 // (unless it's an isolated arc, which is unusual but possible)
1079 if( !startConnected && !endConnected && netEndpoints[netCode].size() > 2 )
1080 {
1081 disconnectedArcs++;
1082 BOOST_TEST_MESSAGE( boardName << ": Arc at ("
1083 << arcStart.x / 1000000.0 << ", "
1084 << arcStart.y / 1000000.0 << ") to ("
1085 << arcEnd.x / 1000000.0 << ", "
1086 << arcEnd.y / 1000000.0
1087 << ") appears disconnected from net " << netCode );
1088 }
1089 }
1090
1091 if( arcCount > 0 )
1092 {
1093 BOOST_TEST_MESSAGE( boardName << ": Found " << arcCount << " arcs, "
1094 << disconnectedArcs << " disconnected" );
1095 }
1096
1097 // Allow some disconnected arcs as they may be legitimate isolated features
1098 // but flag if more than 20% are disconnected
1099 if( arcCount > 5 )
1100 {
1101 BOOST_CHECK_LE( disconnectedArcs, arcCount / 5 );
1102 }
1103 }
1104 }
1105}
1106
1107
1124{
1125 wxString layer;
1126 int recordId = 0;
1127 wxString netName;
1128 double minX = 1e18, minY = 1e18, maxX = -1e18, maxY = -1e18;
1130
1131 void AddPoint( double aX, double aY )
1132 {
1133 minX = std::min( minX, aX );
1134 minY = std::min( minY, aY );
1135 maxX = std::max( maxX, aX );
1136 maxY = std::max( maxY, aY );
1137 segmentCount++;
1138 }
1139};
1140
1141
1143{
1144 std::set<wxString> netNames;
1145 std::set<wxString> refDes;
1146 std::map<wxString, wxString> refDesToSymName;
1147 std::map<wxString, std::set<wxString>> netToRefDes;
1148
1149 std::vector<ALG_ZONE_POLYGON> zonePolygons;
1150
1151 static std::vector<wxString> SplitAlgLine( const wxString& aLine )
1152 {
1153 std::vector<wxString> fields;
1154 wxString current;
1155
1156 for( size_t i = 0; i < aLine.size(); ++i )
1157 {
1158 if( aLine[i] == '!' )
1159 {
1160 fields.push_back( current );
1161 current.clear();
1162 }
1163 else
1164 {
1165 current += aLine[i];
1166 }
1167 }
1168
1169 if( !current.empty() )
1170 fields.push_back( current );
1171
1172 return fields;
1173 }
1174
1179 static int ParseRecordId( const wxString& aTag )
1180 {
1181 long val = -1;
1182 wxString tag = aTag.BeforeFirst( ' ' );
1183 tag.ToLong( &val );
1184 return static_cast<int>( val );
1185 }
1186
1187 static ALG_REFERENCE_DATA ParseAlgFile( const std::string& aPath )
1188 {
1189 ALG_REFERENCE_DATA data;
1190 std::ifstream file( aPath );
1191
1192 if( !file.is_open() )
1193 return data;
1194
1195 enum class SECTION
1196 {
1197 UNKNOWN,
1198 NET_NODES,
1199 SYM_PLACEMENT,
1200 GRAPHICS,
1201 };
1202
1203 SECTION currentSection = SECTION::UNKNOWN;
1204 std::string line;
1205
1206 // Accumulate zone segments grouped by (layer, recordId)
1207 std::map<std::pair<wxString, int>, ALG_ZONE_POLYGON> zoneMap;
1208
1209 while( std::getline( file, line ) )
1210 {
1211 if( line.empty() || line[0] == 'J' )
1212 continue;
1213
1214 if( line[0] == 'A' )
1215 {
1216 if( line.find( "NET_NAME_SORT!NODE_SORT!NET_NAME!REFDES!" ) != std::string::npos )
1217 currentSection = SECTION::NET_NODES;
1218 else if( line.find( "SYM_TYPE!SYM_NAME!REFDES!SYM_MIRROR!" ) != std::string::npos )
1219 currentSection = SECTION::SYM_PLACEMENT;
1220 else if( line.find( "CLASS!SUBCLASS!RECORD_TAG!GRAPHIC_DATA_NAME!" ) != std::string::npos )
1221 currentSection = SECTION::GRAPHICS;
1222 else
1223 currentSection = SECTION::UNKNOWN;
1224
1225 continue;
1226 }
1227
1228 if( line[0] != 'S' )
1229 continue;
1230
1231 auto fields = SplitAlgLine( wxString::FromUTF8( line ) );
1232
1233 switch( currentSection )
1234 {
1235 case SECTION::NET_NODES:
1236 {
1237 // S!sort!nodeSort!NET_NAME!REFDES!PIN!PIN_NAME!SUBCLASS!
1238 if( fields.size() >= 5 )
1239 {
1240 wxString netName = fields[3];
1241 wxString refdes = fields[4];
1242
1243 if( !netName.empty() )
1244 {
1245 data.netNames.insert( netName );
1246
1247 if( !refdes.empty() )
1248 data.netToRefDes[netName].insert( refdes );
1249 }
1250 }
1251
1252 break;
1253 }
1254 case SECTION::SYM_PLACEMENT:
1255 {
1256 // S!SYM_TYPE!SYM_NAME!REFDES!MIRROR!ROTATE!X!Y!CX!CY!LIB_PATH!
1257 if( fields.size() >= 4 )
1258 {
1259 wxString symType = fields[1];
1260 wxString symName = fields[2];
1261 wxString refdes = fields[3];
1262
1263 if( symType == wxT( "PACKAGE" ) && !refdes.empty() )
1264 {
1265 data.refDes.insert( refdes );
1266 data.refDesToSymName[refdes] = symName;
1267 }
1268 }
1269
1270 break;
1271 }
1272 case SECTION::GRAPHICS:
1273 {
1274 // Field layout (0-indexed after splitting on '!'):
1275 // 0=S, 1=CLASS, 2=SUBCLASS, 3=RECORD_TAG, 4=GRAPHIC_DATA_NAME,
1276 // 5=GRAPHIC_DATA_NUMBER, 6..15=GRAPHIC_DATA_1..10,
1277 // 16=PIN_NUMBER, ..., 23=NET_NAME
1278 if( fields.size() < 16 || fields[1] != wxT( "BOUNDARY" ) )
1279 break;
1280
1281 wxString closureType = fields[15];
1282
1283 if( closureType != wxT( "SHAPE" ) )
1284 break;
1285
1286 wxString layer = fields[2];
1287 int recordId = ParseRecordId( fields[3] );
1288
1289 if( recordId < 0 )
1290 break;
1291
1292 wxString netName;
1293
1294 if( fields.size() > 23 )
1295 netName = fields[23];
1296
1297 auto key = std::make_pair( layer, recordId );
1298 auto& zone = zoneMap[key];
1299 zone.layer = layer;
1300 zone.recordId = recordId;
1301
1302 if( !netName.empty() )
1303 zone.netName = netName;
1304
1305 double x1 = 0, y1 = 0, x2 = 0, y2 = 0;
1306
1307 if( fields.size() > 9 )
1308 {
1309 fields[6].ToDouble( &x1 );
1310 fields[7].ToDouble( &y1 );
1311 fields[8].ToDouble( &x2 );
1312 fields[9].ToDouble( &y2 );
1313 zone.AddPoint( x1, y1 );
1314 zone.AddPoint( x2, y2 );
1315 }
1316
1317 break;
1318 }
1319 default:
1320 break;
1321 }
1322 }
1323
1324 for( auto& [key, zone] : zoneMap )
1325 data.zonePolygons.push_back( std::move( zone ) );
1326
1327 return data;
1328 }
1329};
1330
1331
1333{
1334 std::string brdFile;
1335 std::string algFile;
1336};
1337
1338
1343static std::vector<BRD_ALG_PAIR> getBoardsWithAlg()
1344{
1345 std::string dataPath = KI_TEST::AllegroBoardDataDir( "" );
1346 std::vector<BRD_ALG_PAIR> boardsWithAlg;
1347
1348 for( const auto& boardDir : std::filesystem::directory_iterator( dataPath ) )
1349 {
1350 if( !boardDir.is_directory() )
1351 continue;
1352
1353 std::filesystem::path boardPath;
1354 std::filesystem::path algPath;
1355
1356 for( const auto& entry : std::filesystem::directory_iterator( boardDir ) )
1357 {
1358 if( !entry.is_regular_file() )
1359 continue;
1360
1361 if( entry.path().extension() == ".brd" )
1362 boardPath = entry.path();
1363 else if( entry.path().extension() == ".alg" )
1364 algPath = entry.path();
1365
1366 if( !boardPath.empty() && !algPath.empty() )
1367 {
1368 boardsWithAlg.push_back( { boardPath.string(), algPath.string() } );
1369 break;
1370 }
1371 }
1372 }
1373
1374 return boardsWithAlg;
1375}
1376
1377
1381BOOST_AUTO_TEST_CASE( AlgReferenceNetNames )
1382{
1383 std::vector<BRD_ALG_PAIR> boardsWithAlg = getBoardsWithAlg();
1384
1385 BOOST_REQUIRE_GT( boardsWithAlg.size(), 0u );
1386
1387 for( const auto& [brdFile, algFile] : boardsWithAlg )
1388 {
1389 BOOST_TEST_MESSAGE( "Validating net names: " << brdFile );
1390
1392 BOOST_REQUIRE_GT( algData.netNames.size(), 0u );
1393
1394 BOARD* board = GetCachedBoard( brdFile );
1395 BOOST_REQUIRE( board );
1396
1397 std::set<wxString> boardNets;
1398
1399 for( const NETINFO_ITEM* net : board->GetNetInfo() )
1400 {
1401 if( net->GetNetCode() > 0 )
1402 boardNets.insert( net->GetNetname() );
1403 }
1404
1405 int missingNets = 0;
1406
1407 for( const wxString& algNet : algData.netNames )
1408 {
1409 if( boardNets.find( algNet ) == boardNets.end() )
1410 {
1411 missingNets++;
1412
1413 if( missingNets <= 10 )
1414 BOOST_TEST_MESSAGE( " Missing net: " << algNet );
1415 }
1416 }
1417
1418 BOOST_TEST_MESSAGE( brdFile << ": .alg has " << algData.netNames.size() << " nets, board has "
1419 << boardNets.size() << ", missing " << missingNets );
1420
1421 BOOST_CHECK_EQUAL( missingNets, 0 );
1422 }
1423}
1424
1425
1429BOOST_AUTO_TEST_CASE( AlgReferenceComponentPlacement )
1430{
1431 std::vector<BRD_ALG_PAIR> boardsWithAlg = getBoardsWithAlg();
1432
1433 BOOST_REQUIRE_GT( boardsWithAlg.size(), 0u );
1434
1435 for( const auto& [brdFile, algFile] : boardsWithAlg )
1436 {
1437 BOOST_TEST_MESSAGE( "Validating components: " << brdFile );
1438
1440 BOOST_REQUIRE_GT( algData.refDes.size(), 0u );
1441
1442 BOARD* board = GetCachedBoard( brdFile );
1443 BOOST_REQUIRE( board );
1444
1445 std::set<wxString> boardRefDes;
1446
1447 for( const FOOTPRINT* fp : board->Footprints() )
1448 boardRefDes.insert( fp->GetReference() );
1449
1450 int missingRefDes = 0;
1451 int extraRefDes = 0;
1452
1453 for( const wxString& algRef : algData.refDes )
1454 {
1455 if( boardRefDes.find( algRef ) == boardRefDes.end() )
1456 {
1457 missingRefDes++;
1458
1459 if( missingRefDes <= 10 )
1460 BOOST_TEST_MESSAGE( " Missing refdes: " << algRef );
1461 }
1462 }
1463
1464 for( const wxString& boardRef : boardRefDes )
1465 {
1466 if( algData.refDes.find( boardRef ) == algData.refDes.end() )
1467 {
1468 extraRefDes++;
1469
1470 if( extraRefDes <= 10 )
1471 BOOST_TEST_MESSAGE( " Extra refdes in board: " << boardRef );
1472 }
1473 }
1474
1475 BOOST_TEST_MESSAGE( brdFile << ": .alg has " << algData.refDes.size()
1476 << " components, board has " << boardRefDes.size()
1477 << ", missing " << missingRefDes
1478 << ", extra " << extraRefDes );
1479
1480 BOOST_CHECK_EQUAL( missingRefDes, 0 );
1481 }
1482}
1483
1484
1488BOOST_AUTO_TEST_CASE( AllTracksPositiveWidth )
1489{
1490 std::vector<std::string> boards = GetAllBoardFiles();
1491
1492 for( const std::string& boardPath : boards )
1493 {
1494 std::string boardName = std::filesystem::path( boardPath ).filename().string();
1495 BOARD* board = GetCachedBoard( boardPath );
1496
1497 if( !board )
1498 continue;
1499
1500 BOOST_TEST_CONTEXT( "Testing board: " << boardName )
1501 {
1502 int zeroWidthCount = 0;
1503 int totalCount = 0;
1504
1505 for( PCB_TRACK* track : board->Tracks() )
1506 {
1507 if( track->Type() == PCB_TRACE_T || track->Type() == PCB_ARC_T )
1508 {
1509 totalCount++;
1510
1511 if( track->GetWidth() <= 0 )
1512 {
1513 zeroWidthCount++;
1514
1515 if( zeroWidthCount <= 5 )
1516 {
1517 BOOST_TEST_MESSAGE( boardName << ": Zero-width track at ("
1518 << track->GetStart().x / 1000000.0 << ", "
1519 << track->GetStart().y / 1000000.0 << ")" );
1520 }
1521 }
1522 }
1523 }
1524
1525 BOOST_CHECK_EQUAL( zeroWidthCount, 0 );
1526 }
1527 }
1528}
1529
1530
1538{
1539 enum class SEGMENT_TYPE
1540 {
1544 };
1545
1547 {
1549 double x1, y1, x2, y2;
1552 };
1553
1556 std::vector<OUTLINE_SEGMENT> designOutlineSegments;
1557 std::vector<OUTLINE_SEGMENT> outlineSegments;
1558
1559 double minX = std::numeric_limits<double>::max();
1560 double minY = std::numeric_limits<double>::max();
1561 double maxX = std::numeric_limits<double>::lowest();
1562 double maxY = std::numeric_limits<double>::lowest();
1563
1564 void updateBounds( double aX, double aY )
1565 {
1566 minX = std::min( minX, aX );
1567 minY = std::min( minY, aY );
1568 maxX = std::max( maxX, aX );
1569 maxY = std::max( maxY, aY );
1570 }
1571
1572 static ALG_OUTLINE_DATA ParseAlgOutlines( const std::string& aPath )
1573 {
1574 ALG_OUTLINE_DATA data;
1575 std::ifstream file( aPath );
1576
1577 if( !file.is_open() )
1578 return data;
1579
1580 std::string line;
1581
1582 while( std::getline( file, line ) )
1583 {
1584 if( line.empty() || line[0] != 'S' )
1585 continue;
1586
1587 auto fields = ALG_REFERENCE_DATA::SplitAlgLine( wxString::FromUTF8( line ) );
1588
1589 if( fields.size() < 10 )
1590 continue;
1591
1592 bool isDesignOutline = ( fields[1] == wxT( "BOARD GEOMETRY" )
1593 && fields[2] == wxT( "DESIGN_OUTLINE" ) );
1594
1595 bool isOutline = ( fields[1] == wxT( "BOARD GEOMETRY" )
1596 && fields[2] == wxT( "OUTLINE" ) );
1597
1598 if( !isDesignOutline && !isOutline )
1599 continue;
1600
1601 wxString shapeType = fields[4];
1602 OUTLINE_SEGMENT seg = {};
1603
1604 if( shapeType == wxT( "LINE" ) && fields.size() >= 10 )
1605 {
1607 fields[6].ToCDouble( &seg.x1 );
1608 fields[7].ToCDouble( &seg.y1 );
1609 fields[8].ToCDouble( &seg.x2 );
1610 fields[9].ToCDouble( &seg.y2 );
1611
1612 data.updateBounds( seg.x1, seg.y1 );
1613 data.updateBounds( seg.x2, seg.y2 );
1614 }
1615 else if( shapeType == wxT( "ARC" ) && fields.size() >= 15 )
1616 {
1617 seg.type = SEGMENT_TYPE::ARC;
1618 fields[6].ToCDouble( &seg.x1 );
1619 fields[7].ToCDouble( &seg.y1 );
1620 fields[8].ToCDouble( &seg.x2 );
1621 fields[9].ToCDouble( &seg.y2 );
1622 fields[10].ToCDouble( &seg.centerX );
1623 fields[11].ToCDouble( &seg.centerY );
1624 fields[12].ToCDouble( &seg.radius );
1625 seg.clockwise = ( fields[14] == wxT( "CLOCKWISE" ) );
1626
1627 data.updateBounds( seg.x1, seg.y1 );
1628 data.updateBounds( seg.x2, seg.y2 );
1629 }
1630 else if( shapeType == wxT( "RECTANGLE" ) && fields.size() >= 10 )
1631 {
1633 fields[6].ToCDouble( &seg.x1 );
1634 fields[7].ToCDouble( &seg.y1 );
1635 fields[8].ToCDouble( &seg.x2 );
1636 fields[9].ToCDouble( &seg.y2 );
1637
1638 data.updateBounds( seg.x1, seg.y1 );
1639 data.updateBounds( seg.x2, seg.y2 );
1640 }
1641 else
1642 {
1643 continue;
1644 }
1645
1646 if( isDesignOutline )
1647 {
1648 data.designOutlineCount++;
1649 data.designOutlineSegments.push_back( seg );
1650 }
1651 else
1652 {
1653 data.outlineCount++;
1654 data.outlineSegments.push_back( seg );
1655 }
1656 }
1657
1658 return data;
1659 }
1660
1666 {
1667 int count = 0;
1668
1669 for( const auto& seg : designOutlineSegments )
1670 {
1671 if( seg.type == SEGMENT_TYPE::RECTANGLE )
1672 count += 4;
1673 else
1674 count += 1;
1675 }
1676
1677 return count;
1678 }
1679};
1680
1681
1686BOOST_AUTO_TEST_CASE( OutlineSegmentCount )
1687{
1688 std::vector<BRD_ALG_PAIR> testBoards = getBoardsWithAlg();
1689
1690 BOOST_REQUIRE_GT( testBoards.size(), 0u );
1691
1692 for( const auto& [brdFile, algFile] : testBoards )
1693 {
1694 BOOST_TEST_CONTEXT( "Board: " << brdFile )
1695 {
1697
1698 if( algOutlines.designOutlineCount == 0 && algOutlines.outlineCount == 0 )
1699 {
1700 BOOST_TEST_MESSAGE( " No outline records in .alg, skipping" );
1701 continue;
1702 }
1703
1704 BOARD* board = GetCachedBoard( brdFile );
1705 BOOST_REQUIRE( board );
1706
1707 int edgeCutsCount = 0;
1708
1709 for( BOARD_ITEM* item : board->Drawings() )
1710 {
1711 if( item->Type() == PCB_SHAPE_T && item->GetLayer() == Edge_Cuts )
1712 edgeCutsCount++;
1713 }
1714
1715 int expectedCount = algOutlines.expectedEdgeCutsSegments();
1716
1717 BOOST_TEST_MESSAGE( " .alg DESIGN_OUTLINE records: " << algOutlines.designOutlineCount
1718 << " -> expected Edge_Cuts segments: " << expectedCount );
1719 BOOST_TEST_MESSAGE( " .alg OUTLINE records: " << algOutlines.outlineCount );
1720 BOOST_TEST_MESSAGE( " Binary import Edge_Cuts segments: " << edgeCutsCount );
1721
1722 BOOST_CHECK_EQUAL( edgeCutsCount, expectedCount );
1723 }
1724 }
1725}
1726
1727
1732BOOST_AUTO_TEST_CASE( OutlineBoundingBox )
1733{
1734 std::vector<BRD_ALG_PAIR> testBoards = getBoardsWithAlg();
1735
1736 BOOST_REQUIRE_GT( testBoards.size(), 0u );
1737
1738 // 1 mil = 25400 nm
1739 const double milToNm = 25400.0;
1740
1741 // Allow 2 mil tolerance for coordinate rounding across formats
1742 const int toleranceNm = static_cast<int>( 2.0 * milToNm );
1743
1744 for( const auto& [brdFile, algFile] : testBoards )
1745 {
1746 BOOST_TEST_CONTEXT( "Board: " << brdFile )
1747 {
1749
1750 if( algOutlines.designOutlineCount == 0 )
1751 {
1752 BOOST_TEST_MESSAGE( " No DESIGN_OUTLINE records in .alg, skipping" );
1753 continue;
1754 }
1755
1756 BOARD* board = GetCachedBoard( brdFile );
1757 BOOST_REQUIRE( board );
1758
1759 BOX2I boardBbox;
1760 bool hasBbox = false;
1761
1762 for( BOARD_ITEM* item : board->Drawings() )
1763 {
1764 if( item->Type() == PCB_SHAPE_T && item->GetLayer() == Edge_Cuts )
1765 {
1766 if( !hasBbox )
1767 {
1768 boardBbox = item->GetBoundingBox();
1769 hasBbox = true;
1770 }
1771 else
1772 {
1773 boardBbox.Merge( item->GetBoundingBox() );
1774 }
1775 }
1776 }
1777
1778 BOOST_REQUIRE_MESSAGE( hasBbox, "Board should have Edge_Cuts outline" );
1779
1780 // Convert .alg bounding box from mils to nm
1781 int algMinXnm = static_cast<int>( algOutlines.minX * milToNm );
1782 int algMinYnm = static_cast<int>( algOutlines.minY * milToNm );
1783 int algMaxXnm = static_cast<int>( algOutlines.maxX * milToNm );
1784 int algMaxYnm = static_cast<int>( algOutlines.maxY * milToNm );
1785 int algWidthNm = algMaxXnm - algMinXnm;
1786 int algHeightNm = algMaxYnm - algMinYnm;
1787
1788 int boardWidth = boardBbox.GetWidth();
1789 int boardHeight = boardBbox.GetHeight();
1790
1791 BOOST_TEST_MESSAGE( " .alg extent (mils): "
1792 << algOutlines.minX << "," << algOutlines.minY << " to "
1793 << algOutlines.maxX << "," << algOutlines.maxY
1794 << " = " << ( algOutlines.maxX - algOutlines.minX ) << " x "
1795 << ( algOutlines.maxY - algOutlines.minY ) );
1796 BOOST_TEST_MESSAGE( " Board bbox (nm): "
1797 << boardBbox.GetLeft() << "," << boardBbox.GetTop() << " to "
1798 << boardBbox.GetRight() << "," << boardBbox.GetBottom()
1799 << " = " << boardWidth << " x " << boardHeight );
1800 BOOST_TEST_MESSAGE( " .alg (nm): " << algWidthNm << " x " << algHeightNm );
1801
1802 // KiCad bounding boxes include line width so allow 3% tolerance
1803 BOOST_CHECK_CLOSE( static_cast<double>( boardWidth ),
1804 static_cast<double>( algWidthNm ), 3.0 );
1805 BOOST_CHECK_CLOSE( static_cast<double>( boardHeight ),
1806 static_cast<double>( algHeightNm ), 3.0 );
1807 }
1808 }
1809}
1810
1811
1819BOOST_AUTO_TEST_CASE( OutlineEndpoints )
1820{
1821 std::vector<BRD_ALG_PAIR> testBoards = getBoardsWithAlg();
1822
1823 BOOST_REQUIRE_GT( testBoards.size(), 0u );
1824
1825 const double milToNm = 25400.0;
1826 const int toleranceNm = static_cast<int>( 2.0 * milToNm );
1827
1828 for( const auto& [brdFile, algFile] : testBoards )
1829 {
1830 BOOST_TEST_CONTEXT( "Board: " << brdFile )
1831 {
1833
1834 if( algOutlines.designOutlineCount == 0 )
1835 continue;
1836
1837 // Only validate endpoint-by-endpoint for pure-LINE outlines
1838 bool allLines = true;
1839
1840 for( const auto& seg : algOutlines.designOutlineSegments )
1841 {
1844 {
1845 allLines = false;
1846 break;
1847 }
1848 }
1849
1850 if( !allLines )
1851 {
1852 BOOST_TEST_MESSAGE( " Outline has arcs, skipping endpoint-level validation" );
1853 continue;
1854 }
1855
1856 BOARD* board = GetCachedBoard( brdFile );
1857 BOOST_REQUIRE( board );
1858
1859 // Collect all Edge_Cuts segment endpoints
1860 struct ENDPOINT_PAIR
1861 {
1862 VECTOR2I start;
1863 VECTOR2I end;
1864 };
1865
1866 std::vector<ENDPOINT_PAIR> boardSegments;
1867
1868 for( BOARD_ITEM* item : board->Drawings() )
1869 {
1870 if( item->Type() != PCB_SHAPE_T || item->GetLayer() != Edge_Cuts )
1871 continue;
1872
1873 PCB_SHAPE* shape = static_cast<PCB_SHAPE*>( item );
1874
1875 if( shape->GetShape() == SHAPE_T::SEGMENT )
1876 boardSegments.push_back( { shape->GetStart(), shape->GetEnd() } );
1877 }
1878
1879 // Build expected segments from .alg, expanding RECTANGLEs into 4 segments
1880 std::vector<ENDPOINT_PAIR> algSegments;
1881
1882 for( const auto& seg : algOutlines.designOutlineSegments )
1883 {
1884 if( seg.type == ALG_OUTLINE_DATA::SEGMENT_TYPE::LINE )
1885 {
1886 VECTOR2I start( static_cast<int>( seg.x1 * milToNm ),
1887 static_cast<int>( seg.y1 * milToNm ) );
1888 VECTOR2I end( static_cast<int>( seg.x2 * milToNm ),
1889 static_cast<int>( seg.y2 * milToNm ) );
1890 algSegments.push_back( { start, end } );
1891 }
1892 else if( seg.type == ALG_OUTLINE_DATA::SEGMENT_TYPE::RECTANGLE )
1893 {
1894 int x1 = static_cast<int>( seg.x1 * milToNm );
1895 int y1 = static_cast<int>( seg.y1 * milToNm );
1896 int x2 = static_cast<int>( seg.x2 * milToNm );
1897 int y2 = static_cast<int>( seg.y2 * milToNm );
1898
1899 algSegments.push_back( { { x1, y1 }, { x2, y1 } } );
1900 algSegments.push_back( { { x2, y1 }, { x2, y2 } } );
1901 algSegments.push_back( { { x2, y2 }, { x1, y2 } } );
1902 algSegments.push_back( { { x1, y2 }, { x1, y1 } } );
1903 }
1904 }
1905
1906 BOOST_CHECK_EQUAL( boardSegments.size(), algSegments.size() );
1907
1908 if( boardSegments.size() != algSegments.size() )
1909 continue;
1910
1911 // Match each .alg segment to a board segment by finding closest start/end pair.
1912 // Allegro and KiCad may have opposite Y axis, so we compare using absolute
1913 // coordinate deltas.
1914 int matchedCount = 0;
1915
1916 std::vector<bool> used( boardSegments.size(), false );
1917
1918 for( size_t ai = 0; ai < algSegments.size(); ++ai )
1919 {
1920 const auto& algSeg = algSegments[ai];
1921 int bestIdx = -1;
1922 int64_t bestDist = std::numeric_limits<int64_t>::max();
1923
1924 for( size_t bi = 0; bi < boardSegments.size(); ++bi )
1925 {
1926 if( used[bi] )
1927 continue;
1928
1929 const auto& bSeg = boardSegments[bi];
1930
1931 // Try both orientations (start-start or start-end swap)
1932 auto dist = [&]( const VECTOR2I& aAlgPt, const VECTOR2I& aBoardPt ) -> int64_t
1933 {
1934 int64_t dx = std::abs( static_cast<int64_t>( aAlgPt.x )
1935 - static_cast<int64_t>( aBoardPt.x ) );
1936 int64_t dy = std::abs( static_cast<int64_t>( aAlgPt.y )
1937 - static_cast<int64_t>( aBoardPt.y ) );
1938 return dx + dy;
1939 };
1940
1941 int64_t d1 = dist( algSeg.start, bSeg.start ) + dist( algSeg.end, bSeg.end );
1942 int64_t d2 = dist( algSeg.start, bSeg.end ) + dist( algSeg.end, bSeg.start );
1943 int64_t d = std::min( d1, d2 );
1944
1945 if( d < bestDist )
1946 {
1947 bestDist = d;
1948 bestIdx = static_cast<int>( bi );
1949 }
1950 }
1951
1952 if( bestIdx >= 0 && bestDist < 2LL * toleranceNm )
1953 {
1954 used[bestIdx] = true;
1955 matchedCount++;
1956 }
1957 else
1958 {
1959 BOOST_TEST_MESSAGE( " Unmatched .alg segment " << ai << ": ("
1960 << algSeg.start.x / 1000000.0 << ", "
1961 << algSeg.start.y / 1000000.0 << ") -> ("
1962 << algSeg.end.x / 1000000.0 << ", "
1963 << algSeg.end.y / 1000000.0 << ") mm"
1964 << " bestDist=" << bestDist );
1965 }
1966 }
1967
1968 BOOST_TEST_MESSAGE( " Matched " << matchedCount << " / " << algSegments.size()
1969 << " outline segments" );
1970
1971 BOOST_CHECK_EQUAL( matchedCount, static_cast<int>( algSegments.size() ) );
1972 }
1973 }
1974}
1975
1976
1981BOOST_AUTO_TEST_CASE( PadDrillConsistency )
1982{
1983 std::vector<std::string> boards = GetAllBoardFiles();
1984
1985 for( const std::string& boardPath : boards )
1986 {
1987 std::string boardName = std::filesystem::path( boardPath ).filename().string();
1988 BOARD* board = GetCachedBoard( boardPath );
1989
1990 if( !board )
1991 continue;
1992
1993 BOOST_TEST_CONTEXT( "Testing board: " << boardName )
1994 {
1995 int pthNoDrill = 0;
1996 int smdWithDrill = 0;
1997
1998 for( FOOTPRINT* fp : board->Footprints() )
1999 {
2000 for( PAD* pad : fp->Pads() )
2001 {
2002 PAD_ATTRIB attr = pad->GetAttribute();
2003 bool hasDrill = pad->GetDrillSizeX() > 0;
2004
2005 if( attr == PAD_ATTRIB::PTH && !hasDrill )
2006 {
2007 pthNoDrill++;
2008
2009 if( pthNoDrill <= 5 )
2010 {
2011 BOOST_TEST_MESSAGE( boardName << ": PTH pad without drill: "
2012 << fp->GetReference() << "."
2013 << pad->GetNumber() );
2014 }
2015 }
2016
2017 if( attr == PAD_ATTRIB::SMD && hasDrill )
2018 {
2019 smdWithDrill++;
2020
2021 if( smdWithDrill <= 5 )
2022 {
2023 BOOST_TEST_MESSAGE( boardName << ": SMD pad with drill: "
2024 << fp->GetReference() << "."
2025 << pad->GetNumber() );
2026 }
2027 }
2028 }
2029 }
2030
2031 BOOST_CHECK_EQUAL( pthNoDrill, 0 );
2032 BOOST_CHECK_EQUAL( smdWithDrill, 0 );
2033 }
2034 }
2035}
2036
2037
2041BOOST_AUTO_TEST_CASE( ZoneCountMatchesAlg )
2042{
2043 std::vector<BRD_ALG_PAIR> boardsWithAlg = getBoardsWithAlg();
2044
2045 BOOST_REQUIRE_GT( boardsWithAlg.size(), 0u );
2046
2047 for( const auto& [brdFile, algFile] : boardsWithAlg )
2048 {
2049 BOOST_TEST_CONTEXT( "Zone count: " << brdFile )
2050 {
2052
2053 BOARD* board = GetCachedBoard( brdFile );
2054 BOOST_REQUIRE( board );
2055
2056 size_t boardCopperZoneLayers = 0;
2057
2058 for( const ZONE* zone : board->Zones() )
2059 {
2060 if( !zone->GetIsRuleArea() )
2061 boardCopperZoneLayers += ( zone->GetLayerSet() & LSET::AllCuMask() ).count();
2062 }
2063
2064 size_t algZoneCount = algData.zonePolygons.size();
2065
2066 BOOST_TEST_MESSAGE( brdFile << ": .alg has " << algZoneCount
2067 << " zone polygons, board has " << boardCopperZoneLayers
2068 << " copper zone-layers" );
2069
2070 BOOST_CHECK_EQUAL( static_cast<size_t>( boardCopperZoneLayers ), algZoneCount );
2071 }
2072 }
2073}
2074
2075
2079BOOST_AUTO_TEST_CASE( ZoneLayerDistribution )
2080{
2081 std::vector<BRD_ALG_PAIR> boardsWithAlg = getBoardsWithAlg();
2082
2083 BOOST_REQUIRE_GT( boardsWithAlg.size(), 0u );
2084
2085 for( const auto& [brdFile, algFile] : boardsWithAlg )
2086 {
2087 BOOST_TEST_CONTEXT( "Zone layers: " << brdFile )
2088 {
2090
2091 BOARD* board = GetCachedBoard( brdFile );
2092 BOOST_REQUIRE( board );
2093
2094 std::map<wxString, int> algLayerCounts;
2095
2096 for( const ALG_ZONE_POLYGON& zone : algData.zonePolygons )
2097 algLayerCounts[zone.layer]++;
2098
2099 std::map<wxString, int> boardLayerCounts;
2100
2101 for( const ZONE* zone : board->Zones() )
2102 {
2103 if( zone->GetIsRuleArea() )
2104 continue;
2105
2106 for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
2107 {
2108 if( IsCopperLayer( layer ) )
2109 boardLayerCounts[board->GetLayerName( layer )]++;
2110 }
2111 }
2112
2113 BOOST_TEST_MESSAGE( brdFile << " layer distribution:" );
2114
2115 for( const auto& [layer, count] : algLayerCounts )
2116 {
2117 auto it = boardLayerCounts.find( layer );
2118 int boardCount = ( it != boardLayerCounts.end() ) ? it->second : 0;
2119
2120 BOOST_TEST_MESSAGE( " " << layer << ": .alg=" << count << " board=" << boardCount );
2121 BOOST_CHECK_EQUAL( boardCount, count );
2122 }
2123 }
2124 }
2125}
2126
2127
2132BOOST_AUTO_TEST_CASE( ZoneBoundingBoxes )
2133{
2134 std::vector<BRD_ALG_PAIR> boardsWithAlg = getBoardsWithAlg();
2135
2136 BOOST_REQUIRE_GT( boardsWithAlg.size(), 0u );
2137
2138 for( const auto& [brdFile, algFile] : boardsWithAlg )
2139 {
2140 BOOST_TEST_CONTEXT( "Zone bboxes: " << brdFile )
2141 {
2143
2144 BOARD* board = GetCachedBoard( brdFile );
2145 BOOST_REQUIRE( board );
2146
2147 // Collect sorted areas per layer from .alg and board, then compare distributions
2148 const double milsToNm = 25400.0;
2149
2150 std::map<wxString, std::vector<double>> algAreas;
2151
2152 for( const ALG_ZONE_POLYGON& zone : algData.zonePolygons )
2153 {
2154 double w = ( zone.maxX - zone.minX ) * milsToNm;
2155 double h = ( zone.maxY - zone.minY ) * milsToNm;
2156 algAreas[zone.layer].push_back( w * h );
2157 }
2158
2159 std::map<wxString, std::vector<double>> boardAreas;
2160
2161 for( const ZONE* zone : board->Zones() )
2162 {
2163 if( zone->GetIsRuleArea() )
2164 continue;
2165
2166 BOX2I bbox = zone->GetBoundingBox();
2167 double area = static_cast<double>( bbox.GetWidth() )
2168 * static_cast<double>( bbox.GetHeight() );
2169
2170 for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
2171 {
2172 if( IsCopperLayer( layer ) )
2173 boardAreas[board->GetLayerName( layer )].push_back( area );
2174 }
2175 }
2176
2177 int matched = 0;
2178 int mismatched = 0;
2179
2180 for( auto& [layer, algList] : algAreas )
2181 {
2182 std::sort( algList.begin(), algList.end() );
2183 auto it = boardAreas.find( layer );
2184
2185 if( it == boardAreas.end() || it->second.size() != algList.size() )
2186 continue;
2187
2188 std::sort( it->second.begin(), it->second.end() );
2189
2190 for( size_t i = 0; i < algList.size(); ++i )
2191 {
2192 double ref = std::max( algList[i], 1.0 );
2193 double err = std::abs( it->second[i] - algList[i] ) / ref;
2194
2195 if( err < 0.10 )
2196 {
2197 matched++;
2198 }
2199 else
2200 {
2201 mismatched++;
2202
2203 if( mismatched <= 5 )
2204 {
2205 BOOST_TEST_MESSAGE( " " << layer << " index " << i
2206 << ": alg area " << algList[i] / ( milsToNm * milsToNm )
2207 << " sq mils vs board area "
2208 << it->second[i] / ( milsToNm * milsToNm )
2209 << " sq mils" );
2210 }
2211 }
2212 }
2213 }
2214
2215 BOOST_TEST_MESSAGE( brdFile << ": " << matched << " zone areas matched, "
2216 << mismatched << " mismatched" );
2217
2218 int total = matched + mismatched;
2219
2220 if( total > 0 )
2221 {
2222 BOOST_CHECK_GT( matched, total * 8 / 10 );
2223 }
2224 }
2225 }
2226}
2227
2228
2235BOOST_AUTO_TEST_CASE( PadContainedInFabOutline )
2236{
2237 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
2238
2239 BOARD* board = GetCachedBoard( dataPath );
2240 BOOST_REQUIRE( board );
2241
2242 // Footprints known to have pads and fab outlines that enclose them.
2243 // P6 and P10 are excluded: they are bottom-side connectors whose assembly outlines
2244 // only cover the housing, not the full pin field.
2245 const std::set<wxString> targetRefs = { wxS( "J1" ), wxS( "P5" ), wxS( "U5" ),
2246 wxS( "U13" ), wxS( "C78" ) };
2247
2248 int testedCount = 0;
2249 int failedCount = 0;
2250
2251 for( FOOTPRINT* fp : board->Footprints() )
2252 {
2253 if( targetRefs.find( fp->GetReference() ) == targetRefs.end() )
2254 continue;
2255
2256 PCB_LAYER_ID fabLayer = fp->IsFlipped() ? B_Fab : F_Fab;
2257
2258 BOX2I fabBbox;
2259 bool hasFab = false;
2260
2261 for( BOARD_ITEM* item : fp->GraphicalItems() )
2262 {
2263 if( item->GetLayer() == fabLayer )
2264 {
2265 if( !hasFab )
2266 {
2267 fabBbox = item->GetBoundingBox();
2268 hasFab = true;
2269 }
2270 else
2271 {
2272 fabBbox.Merge( item->GetBoundingBox() );
2273 }
2274 }
2275 }
2276
2277 if( !hasFab || fp->Pads().empty() )
2278 continue;
2279
2280 // Allow generous tolerance: pad centers can extend slightly beyond the fab outline
2281 // (e.g. edge-mount connectors, thermal pads). 3mm handles most cases.
2282 BOX2I testBbox = fabBbox;
2283 testBbox.Inflate( 3000000 );
2284
2285 BOOST_TEST_CONTEXT( "Footprint " << fp->GetReference() )
2286 {
2287 testedCount++;
2288
2289 for( PAD* pad : fp->Pads() )
2290 {
2291 VECTOR2I padCenter = pad->GetPosition();
2292
2293 if( !testBbox.Contains( padCenter ) )
2294 {
2295 failedCount++;
2296 BOOST_TEST_MESSAGE( fp->GetReference() << " pad " << pad->GetNumber()
2297 << " at (" << padCenter.x / 1e6 << ", "
2298 << padCenter.y / 1e6 << ") mm is outside F.Fab bbox" );
2299 }
2300 }
2301 }
2302 }
2303
2304 BOOST_TEST_MESSAGE( "Tested " << testedCount << " footprints for pad containment" );
2305 BOOST_CHECK_GE( testedCount, 4 );
2306 BOOST_CHECK_EQUAL( failedCount, 0 );
2307}
2308
2309
2315BOOST_AUTO_TEST_CASE( PadOrientationP6P10 )
2316{
2317 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
2318
2319 BOARD* board = GetCachedBoard( dataPath );
2320 BOOST_REQUIRE( board );
2321
2322 for( FOOTPRINT* fp : board->Footprints() )
2323 {
2324 wxString ref = fp->GetReference();
2325
2326 if( ref != wxS( "P6" ) && ref != wxS( "P10" ) )
2327 continue;
2328
2329 BOOST_TEST_CONTEXT( "Footprint " << ref )
2330 {
2331 for( PAD* pad : fp->Pads() )
2332 {
2333 long padNum = 0;
2334
2335 if( !pad->GetNumber().ToLong( &padNum ) )
2336 continue;
2337
2338 // Pads 1-19 on P6/P10 are rectangular SMD pads that should be wider than tall
2339 if( padNum < 1 || padNum > 19 )
2340 continue;
2341
2342 // GetBoundingBox accounts for rotation, giving visual dimensions
2343 BOX2I bbox = pad->GetBoundingBox();
2344 auto bboxW = bbox.GetWidth();
2345 auto bboxH = bbox.GetHeight();
2346
2347 // Skip square/circular pads where orientation doesn't affect shape
2348 if( bboxW == bboxH )
2349 continue;
2350
2351 BOOST_TEST_CONTEXT( "Pad " << pad->GetNumber() )
2352 {
2353 BOOST_CHECK_MESSAGE( bboxW > bboxH,
2354 ref << " pad " << pad->GetNumber()
2355 << " should be visually wider than tall: "
2356 << bboxW / 1e6 << " x " << bboxH / 1e6 << " mm" );
2357 }
2358 }
2359 }
2360 }
2361}
2362
2363
2372{
2373 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
2374
2375 BOARD* board = GetCachedBoard( dataPath );
2376 BOOST_REQUIRE( board );
2377
2378 int oblongCount = 0;
2379
2380 for( FOOTPRINT* fp : board->Footprints() )
2381 {
2382 for( PAD* pad : fp->Pads() )
2383 {
2384 VECTOR2I drillSize = pad->GetDrillSize();
2385
2386 if( drillSize.x <= 0 || drillSize.y <= 0 )
2387 continue;
2388
2389 if( drillSize.x == drillSize.y )
2390 continue;
2391
2392 oblongCount++;
2393
2394 BOOST_TEST_CONTEXT( fp->GetReference() << " pad " << pad->GetNumber() )
2395 {
2396 BOOST_CHECK( pad->GetDrillShape() == PAD_DRILL_SHAPE::OBLONG );
2397
2398 BOOST_TEST_MESSAGE( fp->GetReference() << " pad " << pad->GetNumber()
2399 << " slot: " << drillSize.x / 1e6 << " x "
2400 << drillSize.y / 1e6 << " mm"
2401 << " attr=" << static_cast<int>( pad->GetAttribute() ) );
2402 }
2403 }
2404 }
2405
2406 BOOST_TEST_MESSAGE( "Found " << oblongCount << " oblong drill holes" );
2407 BOOST_CHECK_EQUAL( oblongCount, 7 );
2408}
2409
2410
2416BOOST_AUTO_TEST_CASE( FootprintOrientation )
2417{
2418 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
2419
2420 BOARD* board = GetCachedBoard( dataPath );
2421
2422 BOOST_REQUIRE( board );
2423
2424 FOOTPRINT* j1 = nullptr;
2425
2426 for( FOOTPRINT* fp : board->Footprints() )
2427 {
2428 if( fp->GetReference() == wxT( "J1" ) )
2429 {
2430 j1 = fp;
2431 break;
2432 }
2433 }
2434
2435 BOOST_REQUIRE_MESSAGE( j1 != nullptr, "Footprint J1 must exist in BeagleBone_Black_RevC" );
2436
2437 EDA_ANGLE orientation = j1->GetOrientation();
2438 BOOST_TEST_MESSAGE( "J1 orientation: " << orientation.AsDegrees() << " degrees" );
2439 BOOST_CHECK_CLOSE( orientation.AsDegrees(), 90.0, 0.1 );
2440}
2441
2442
2447BOOST_AUTO_TEST_CASE( NetclassTraceWidths )
2448{
2449 std::string dataPath = KI_TEST::AllegroBoardFile( "ProiectBoard/ProiectBoard.brd" );
2450
2451 BOARD* board = GetCachedBoard( dataPath );
2452 BOOST_REQUIRE( board );
2453
2454 std::shared_ptr<NET_SETTINGS> netSettings = board->GetDesignSettings().m_NetSettings;
2455
2456 BOOST_CHECK( netSettings->HasNetclass( wxS( "W20mil" ) ) );
2457 BOOST_CHECK( netSettings->HasNetclass( wxS( "W24mil" ) ) );
2458
2459 auto nc20 = netSettings->GetNetClassByName( wxS( "W20mil" ) );
2460 auto nc24 = netSettings->GetNetClassByName( wxS( "W24mil" ) );
2461
2462 BOOST_REQUIRE( nc20 );
2463 BOOST_REQUIRE( nc24 );
2464
2465 // 20 mil = 508000 nm, 24 mil = 609600 nm
2466 BOOST_CHECK_EQUAL( nc20->GetTrackWidth(), 508000 );
2467 BOOST_CHECK_EQUAL( nc24->GetTrackWidth(), 609600 );
2468
2469 // Count nets in each netclass
2470 int count20 = 0;
2471 int count24 = 0;
2472
2473 for( NETINFO_ITEM* net : board->GetNetInfo() )
2474 {
2475 if( net->GetNetCode() <= 0 )
2476 continue;
2477
2478 NETCLASS* nc = net->GetNetClass();
2479
2480 if( !nc )
2481 continue;
2482
2483 if( nc->GetName() == wxS( "W20mil" ) )
2484 count20++;
2485 else if( nc->GetName() == wxS( "W24mil" ) )
2486 count24++;
2487 }
2488
2489 BOOST_CHECK_EQUAL( count20, 17 );
2490 BOOST_CHECK_EQUAL( count24, 2 );
2491}
2492
2493
2498BOOST_AUTO_TEST_CASE( DiffPairNetclass )
2499{
2500 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
2501
2502 BOARD* board = GetCachedBoard( dataPath );
2503 BOOST_REQUIRE( board );
2504
2505 std::shared_ptr<NET_SETTINGS> netSettings = board->GetDesignSettings().m_NetSettings;
2506
2507 // HDMI_TXC is a well-known diff pair on BeagleBone Black
2508 BOOST_CHECK( netSettings->HasNetclass( wxS( "DP_HDMI_TXC" ) ) );
2509 BOOST_CHECK( netSettings->HasNetclass( wxS( "DP_USB0" ) ) );
2510
2511 // Verify HDMI_TXC has exactly 2 nets assigned
2512 int hdmiTxcCount = 0;
2513
2514 for( NETINFO_ITEM* net : board->GetNetInfo() )
2515 {
2516 if( net->GetNetCode() <= 0 )
2517 continue;
2518
2519 NETCLASS* nc = net->GetNetClass();
2520
2521 if( nc && nc->GetName() == wxS( "DP_HDMI_TXC" ) )
2522 hdmiTxcCount++;
2523 }
2524
2525 BOOST_CHECK_EQUAL( hdmiTxcCount, 2 );
2526}
2527
2528
2533BOOST_AUTO_TEST_CASE( MatchGroupNetclass )
2534{
2535 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
2536
2537 BOARD* board = GetCachedBoard( dataPath );
2538 BOOST_REQUIRE( board );
2539
2540 std::shared_ptr<NET_SETTINGS> netSettings = board->GetDesignSettings().m_NetSettings;
2541
2542 // DDR_DQ0 is a DDR byte lane with 11 nets (not a diff pair)
2543 BOOST_CHECK( netSettings->HasNetclass( wxS( "MG_DDR_DQ0" ) ) );
2544 BOOST_CHECK( netSettings->HasNetclass( wxS( "MG_DDR_ADD" ) ) );
2545
2546 // DDR_DQ0 should have 11 nets, DDR_ADD should have 26
2547 int dq0Count = 0;
2548 int addCount = 0;
2549
2550 for( NETINFO_ITEM* net : board->GetNetInfo() )
2551 {
2552 if( net->GetNetCode() <= 0 )
2553 continue;
2554
2555 NETCLASS* nc = net->GetNetClass();
2556
2557 if( !nc )
2558 continue;
2559
2560 if( nc->GetName() == wxS( "MG_DDR_DQ0" ) )
2561 dq0Count++;
2562 else if( nc->GetName() == wxS( "MG_DDR_ADD" ) )
2563 addCount++;
2564 }
2565
2566 BOOST_CHECK_EQUAL( dq0Count, 11 );
2567 BOOST_CHECK_EQUAL( addCount, 26 );
2568}
2569
2570
2575BOOST_AUTO_TEST_CASE( MatchGroupCounts )
2576{
2577 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
2578
2579 BOARD* board = GetCachedBoard( dataPath );
2580 BOOST_REQUIRE( board );
2581
2582 std::shared_ptr<NET_SETTINGS> netSettings = board->GetDesignSettings().m_NetSettings;
2583
2584 int dpCount = 0;
2585 int mgCount = 0;
2586
2587 for( const auto& [name, nc] : netSettings->GetNetclasses() )
2588 {
2589 if( name.StartsWith( wxS( "DP_" ) ) )
2590 dpCount++;
2591 else if( name.StartsWith( wxS( "MG_" ) ) )
2592 mgCount++;
2593 }
2594
2595 BOOST_CHECK_EQUAL( dpCount, 17 );
2596 BOOST_CHECK_EQUAL( mgCount, 4 );
2597}
2598
2599
2604BOOST_AUTO_TEST_CASE( NoMatchGroupsOnSimpleBoard )
2605{
2606 std::string dataPath = KI_TEST::AllegroBoardFile( "led_youtube/led_youtube.brd" );
2607
2608 BOARD* board = GetCachedBoard( dataPath );
2609 BOOST_REQUIRE( board );
2610
2611 std::shared_ptr<NET_SETTINGS> netSettings = board->GetDesignSettings().m_NetSettings;
2612
2613 for( const auto& [name, nc] : netSettings->GetNetclasses() )
2614 {
2615 BOOST_CHECK_MESSAGE( !name.StartsWith( wxS( "DP_" ) ) && !name.StartsWith( wxS( "MG_" ) ),
2616 "Simple board should not have match group netclass: " + name );
2617 }
2618}
2619
2620
2625BOOST_AUTO_TEST_CASE( ConstraintSetNetclasses )
2626{
2627 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
2628
2629 BOARD* board = GetCachedBoard( dataPath );
2630 BOOST_REQUIRE( board );
2631
2632 std::shared_ptr<NET_SETTINGS> netSettings = board->GetDesignSettings().m_NetSettings;
2633
2634 // BB Black has 5 constraint sets, all with 4.0 mil (101600 nm) clearance
2635 BOOST_CHECK( netSettings->HasNetclass( wxS( "Allegro_Default" ) ) );
2636 BOOST_CHECK( netSettings->HasNetclass( wxS( "PWR" ) ) );
2637 BOOST_CHECK( netSettings->HasNetclass( wxS( "BGA" ) ) );
2638 BOOST_CHECK( netSettings->HasNetclass( wxS( "90_OHM_DIFF" ) ) );
2639 BOOST_CHECK( netSettings->HasNetclass( wxS( "100OHM_DIFF" ) ) );
2640
2641 auto ncDefault = netSettings->GetNetClassByName( wxS( "Allegro_Default" ) );
2642 auto ncPwr = netSettings->GetNetClassByName( wxS( "PWR" ) );
2643
2644 BOOST_REQUIRE( ncDefault );
2645 BOOST_REQUIRE( ncPwr );
2646
2647 BOOST_CHECK_EQUAL( ncDefault->GetClearance(), 101600 );
2648 BOOST_CHECK_EQUAL( ncDefault->GetTrackWidth(), 120650 );
2649
2650 BOOST_CHECK_EQUAL( ncPwr->GetClearance(), 101600 );
2651 BOOST_CHECK_EQUAL( ncPwr->GetTrackWidth(), 381000 );
2652
2653 // Nets without explicit 0x1a0 field assignment fall back to DEFAULT.
2654 // BB Black nets have empty-string 0x1a0 fields which don't match any constraint set,
2655 // so all nets get assigned to DEFAULT.
2656 int defaultCount = 0;
2657
2658 for( NETINFO_ITEM* net : board->GetNetInfo() )
2659 {
2660 if( net->GetNetCode() <= 0 )
2661 continue;
2662
2663 NETCLASS* nc = net->GetNetClass();
2664
2665 if( !nc )
2666 continue;
2667
2668 if( nc->GetName() == wxS( "Allegro_Default" ) )
2669 defaultCount++;
2670 }
2671
2672 BOOST_CHECK_MESSAGE( defaultCount > 0, "DEFAULT constraint set should have assigned nets (implicit)" );
2673}
2674
2675
2680BOOST_AUTO_TEST_CASE( ConstraintSetPreV172 )
2681{
2682 std::string dataPath = KI_TEST::AllegroBoardFile( "TRS80_POWER/TRS80_POWER.brd" );
2683
2684 BOARD* board = GetCachedBoard( dataPath );
2685 BOOST_REQUIRE( board );
2686
2687 std::shared_ptr<NET_SETTINGS> netSettings = board->GetDesignSettings().m_NetSettings;
2688
2689 BOOST_CHECK( netSettings->HasNetclass( wxS( "CS_0" ) ) );
2690
2691 auto nc = netSettings->GetNetClassByName( wxS( "CS_0" ) );
2692
2693 BOOST_REQUIRE( nc );
2694
2695 // Pre-V172 f[0]=line_width=15 mil (381000 nm), f[1]=spacing=7 mil (177800 nm) used as clearance
2696 BOOST_CHECK_EQUAL( nc->GetTrackWidth(), 381000 );
2697 BOOST_CHECK_EQUAL( nc->GetClearance(), 177800 );
2698}
2699
2700
2705BOOST_AUTO_TEST_CASE( ConstraintSetAndTraceWidth )
2706{
2707 std::string dataPath = KI_TEST::AllegroBoardFile( "ProiectBoard/ProiectBoard.brd" );
2708
2709 BOARD* board = GetCachedBoard( dataPath );
2710 BOOST_REQUIRE( board );
2711
2712 std::shared_ptr<NET_SETTINGS> netSettings = board->GetDesignSettings().m_NetSettings;
2713
2714 // Constraint set netclass
2715 BOOST_CHECK( netSettings->HasNetclass( wxS( "CS_0" ) ) );
2716
2717 // Per-net trace width netclasses
2718 BOOST_CHECK( netSettings->HasNetclass( wxS( "W20mil" ) ) );
2719 BOOST_CHECK( netSettings->HasNetclass( wxS( "W24mil" ) ) );
2720
2721 // Constraint set values (pre-V172, 5 mil line/spacing/clearance)
2722 auto ncCS = netSettings->GetNetClassByName( wxS( "CS_0" ) );
2723
2724 BOOST_REQUIRE( ncCS );
2725 BOOST_CHECK_EQUAL( ncCS->GetTrackWidth(), 127000 );
2726 BOOST_CHECK_EQUAL( ncCS->GetClearance(), 127000 );
2727
2728 // Per-net trace widths still intact
2729 auto nc20 = netSettings->GetNetClassByName( wxS( "W20mil" ) );
2730 auto nc24 = netSettings->GetNetClassByName( wxS( "W24mil" ) );
2731
2732 BOOST_REQUIRE( nc20 );
2733 BOOST_REQUIRE( nc24 );
2734 BOOST_CHECK_EQUAL( nc20->GetTrackWidth(), 508000 );
2735 BOOST_CHECK_EQUAL( nc24->GetTrackWidth(), 609600 );
2736}
2737
2738
2743BOOST_AUTO_TEST_CASE( ConstraintSetDiffPairGap )
2744{
2745 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
2746
2747 BOARD* board = GetCachedBoard( dataPath );
2748 BOOST_REQUIRE( board );
2749
2750 std::shared_ptr<NET_SETTINGS> netSettings = board->GetDesignSettings().m_NetSettings;
2751
2752 // 90_OHM_DIFF: f[1]=450, f[4]=400, f[7]=650 (divisor=100, scale=254 nm/unit)
2753 auto nc90 = netSettings->GetNetClassByName( wxS( "90_OHM_DIFF" ) );
2754 BOOST_REQUIRE( nc90 );
2755 BOOST_CHECK_EQUAL( nc90->GetTrackWidth(), 114300 );
2756 BOOST_CHECK_EQUAL( nc90->GetClearance(), 101600 );
2757 BOOST_CHECK_EQUAL( nc90->GetDiffPairGap(), 165100 );
2758 BOOST_CHECK_EQUAL( nc90->GetDiffPairWidth(), 114300 );
2759
2760 // 100OHM_DIFF: f[1]=375, f[4]=400, f[7]=725
2761 auto nc100 = netSettings->GetNetClassByName( wxS( "100OHM_DIFF" ) );
2762 BOOST_REQUIRE( nc100 );
2763 BOOST_CHECK_EQUAL( nc100->GetTrackWidth(), 95250 );
2764 BOOST_CHECK_EQUAL( nc100->GetClearance(), 101600 );
2765 BOOST_CHECK_EQUAL( nc100->GetDiffPairGap(), 184150 );
2766 BOOST_CHECK_EQUAL( nc100->GetDiffPairWidth(), 95250 );
2767
2768 // BGA: f[1]=300, f[7]=300 (divisor=100, 300*254=76200 nm)
2769 auto ncBga = netSettings->GetNetClassByName( wxS( "BGA" ) );
2770 BOOST_REQUIRE( ncBga );
2771 BOOST_CHECK_EQUAL( ncBga->GetDiffPairGap(), 76200 );
2772 BOOST_CHECK_EQUAL( ncBga->GetDiffPairWidth(), 76200 );
2773}
2774
2775
2780BOOST_AUTO_TEST_CASE( ConstraintSetDiffPairGapPreV172 )
2781{
2782 std::string dataPath = KI_TEST::AllegroBoardFile( "VCU118_REV2-0/8851_HW-U1-VCU118_REV2-0_071417.brd" );
2783
2784 BOARD* board = GetCachedBoard( dataPath );
2785 BOOST_REQUIRE( board );
2786
2787 std::shared_ptr<NET_SETTINGS> netSettings = board->GetDesignSettings().m_NetSettings;
2788
2789 // DP_90_OHM: f[0]=5200 (line_width), f[7]=4800 (dp_gap) on L0 (divisor=1000, scale=25.4)
2790 auto nc = netSettings->GetNetClassByName( wxS( "DP_90_OHM" ) );
2791 BOOST_REQUIRE( nc );
2792 BOOST_CHECK_EQUAL( nc->GetTrackWidth(), 132080 );
2793 BOOST_CHECK_EQUAL( nc->GetDiffPairGap(), 121920 );
2794 BOOST_CHECK_EQUAL( nc->GetDiffPairWidth(), 132080 );
2795}
2796
2797
2802BOOST_AUTO_TEST_CASE( UIImportPath_NullBoard )
2803{
2804 std::string dataPath = KI_TEST::AllegroBoardFile( "ProiectBoard/ProiectBoard.brd" );
2805
2806 PCB_IO_ALLEGRO plugin;
2807 CAPTURING_REPORTER reporter;
2808 plugin.SetReporter( &reporter );
2809
2810 BOARD* rawBoard = nullptr;
2811
2812 try
2813 {
2814 rawBoard = plugin.LoadBoard( dataPath, nullptr, nullptr, nullptr );
2815 }
2816 catch( const IO_ERROR& e )
2817 {
2818 BOOST_TEST_MESSAGE( "IO_ERROR: " << e.What() );
2819 }
2820 catch( const std::exception& e )
2821 {
2822 BOOST_TEST_MESSAGE( "Exception: " << e.what() );
2823 }
2824
2825 reporter.PrintAllMessages( "UIImportPath_NullBoard" );
2826
2827 BOOST_REQUIRE_MESSAGE( rawBoard != nullptr, "LoadBoard with nullptr aAppendToMe must return a valid board" );
2828
2829 std::unique_ptr<BOARD> board( rawBoard );
2830
2831 BOOST_CHECK_GT( board->GetNetCount(), 0 );
2832 BOOST_CHECK_GT( board->Footprints().size(), 0 );
2833 BOOST_CHECK_GT( board->Tracks().size(), 0 );
2834 BOOST_CHECK_EQUAL( reporter.GetErrorCount(), 0 );
2835
2836 PrintBoardStats( board.get(), "ProiectBoard (UI path)" );
2837}
2838
2839
2848BOOST_AUTO_TEST_CASE( SmdPadLayerConsistency )
2849{
2850 std::vector<std::string> boards = GetAllBoardFiles();
2851
2852 for( const std::string& boardPath : boards )
2853 {
2854 std::string boardName = std::filesystem::path( boardPath ).filename().string();
2855 BOARD* board = GetCachedBoard( boardPath );
2856
2857 if( !board )
2858 continue;
2859
2860 BOOST_TEST_CONTEXT( "Testing board: " << boardName )
2861 {
2862 int inconsistentCount = 0;
2863
2864 for( FOOTPRINT* fp : board->Footprints() )
2865 {
2866 const bool onBottom = fp->IsFlipped();
2867
2868 for( PAD* pad : fp->Pads() )
2869 {
2870 if( pad->GetAttribute() != PAD_ATTRIB::SMD )
2871 continue;
2872
2873 LSET layers = pad->GetLayerSet();
2874 bool hasTopCopper = layers.Contains( F_Cu );
2875 bool hasBotCopper = layers.Contains( B_Cu );
2876
2877 if( onBottom && hasTopCopper && !hasBotCopper )
2878 {
2879 inconsistentCount++;
2880 BOOST_TEST_MESSAGE( boardName << ": " << fp->GetReference() << " pad "
2881 << pad->GetNumber() << " is on bottom footprint but SMD "
2882 << "pad has F.Cu without B.Cu" );
2883 }
2884 else if( !onBottom && hasBotCopper && !hasTopCopper )
2885 {
2886 inconsistentCount++;
2887 BOOST_TEST_MESSAGE( boardName << ": " << fp->GetReference() << " pad "
2888 << pad->GetNumber() << " is on top footprint but SMD "
2889 << "pad has B.Cu without F.Cu" );
2890 }
2891 }
2892 }
2893
2894 BOOST_CHECK_EQUAL( inconsistentCount, 0 );
2895 }
2896 }
2897}
2898
2899
2908BOOST_AUTO_TEST_CASE( SmdFootprintTechLayers )
2909{
2910 std::vector<std::string> boards = GetAllBoardFiles();
2911
2912 for( const std::string& boardPath : boards )
2913 {
2914 std::string boardName = std::filesystem::path( boardPath ).filename().string();
2915 BOARD* board = GetCachedBoard( boardPath );
2916
2917 if( !board )
2918 continue;
2919
2920 BOOST_TEST_CONTEXT( "Testing board: " << boardName )
2921 {
2922 int inconsistentCount = 0;
2923
2924 for( FOOTPRINT* fp : board->Footprints() )
2925 {
2926 // Only check SMD-only footprints (no through-hole pads)
2927 bool hasSmd = false;
2928 bool hasTH = false;
2929
2930 for( PAD* pad : fp->Pads() )
2931 {
2932 if( pad->GetAttribute() == PAD_ATTRIB::SMD )
2933 hasSmd = true;
2934 else if( pad->GetAttribute() == PAD_ATTRIB::PTH )
2935 hasTH = true;
2936 }
2937
2938 if( !hasSmd || hasTH )
2939 continue;
2940
2941 const bool onBottom = fp->IsFlipped();
2942
2943 for( BOARD_ITEM* item : fp->GraphicalItems() )
2944 {
2945 PCB_LAYER_ID layer = item->GetLayer();
2946
2947 if( !IsFrontLayer( layer ) && !IsBackLayer( layer ) )
2948 continue;
2949
2950 bool wrongSide = false;
2951
2952 if( onBottom && IsFrontLayer( layer ) )
2953 wrongSide = true;
2954 else if( !onBottom && IsBackLayer( layer ) )
2955 wrongSide = true;
2956
2957 if( wrongSide )
2958 {
2959 inconsistentCount++;
2961 boardName << ": " << fp->GetReference() << " is "
2962 << ( onBottom ? "bottom" : "top" ) << "-side SMD but has "
2963 << item->GetClass() << " on "
2964 << board->GetLayerName( layer ) );
2965 }
2966 }
2967 }
2968
2969 BOOST_CHECK_EQUAL( inconsistentCount, 0 );
2970 }
2971 }
2972}
2973
2974
2981BOOST_AUTO_TEST_CASE( BeagleBone_DrillSlotOrientation )
2982{
2983 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
2984
2985 BOARD* board = GetCachedBoard( dataPath );
2986 BOOST_REQUIRE( board );
2987
2988 struct SLOT_CHECK
2989 {
2990 wxString fpRef;
2991 wxString padNum;
2992 };
2993
2994 // These pads have vertical oblong copper shapes and should have taller-than-wide drill slots
2995 std::vector<SLOT_CHECK> checks = {
2996 { wxS( "P6" ), wxS( "20" ) },
2997 { wxS( "P6" ), wxS( "21" ) },
2998 { wxS( "P3" ), wxS( "6" ) },
2999 { wxS( "P1" ), wxS( "1" ) },
3000 { wxS( "P1" ), wxS( "2" ) },
3001 { wxS( "P1" ), wxS( "3" ) },
3002 };
3003
3004 for( const auto& check : checks )
3005 {
3006 BOOST_TEST_CONTEXT( check.fpRef << " pad " << check.padNum )
3007 {
3008 FOOTPRINT* fp = nullptr;
3009
3010 for( FOOTPRINT* candidate : board->Footprints() )
3011 {
3012 if( candidate->GetReference() == check.fpRef )
3013 {
3014 fp = candidate;
3015 break;
3016 }
3017 }
3018
3019 BOOST_REQUIRE_MESSAGE( fp != nullptr, "Footprint " << check.fpRef << " should exist" );
3020
3021 PAD* pad = nullptr;
3022
3023 for( PAD* candidate : fp->Pads() )
3024 {
3025 if( candidate->GetNumber() == check.padNum )
3026 {
3027 pad = candidate;
3028 break;
3029 }
3030 }
3031
3032 BOOST_REQUIRE_MESSAGE( pad != nullptr,
3033 "Pad " << check.padNum << " should exist on " << check.fpRef );
3034
3035 VECTOR2I padSize = pad->GetSize( F_Cu );
3036 VECTOR2I drillSize = pad->GetDrillSize();
3037
3038 BOOST_TEST_MESSAGE( check.fpRef << " pad " << check.padNum
3039 << ": pad=" << padSize.x << "x" << padSize.y
3040 << " drill=" << drillSize.x << "x" << drillSize.y );
3041
3042 // If the pad is oblong, the drill should match the pad's aspect ratio
3043 if( drillSize.x != drillSize.y )
3044 {
3045 bool padIsTaller = ( padSize.y > padSize.x );
3046 bool drillIsTaller = ( drillSize.y > drillSize.x );
3047
3048 BOOST_CHECK_MESSAGE( padIsTaller == drillIsTaller,
3049 "Drill slot should match pad orientation" );
3050 }
3051 }
3052 }
3053}
3054
3055
3060BOOST_AUTO_TEST_CASE( BeagleBone_ZoneFills )
3061{
3062 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
3063
3064 BOARD* board = GetCachedBoard( dataPath );
3065 BOOST_REQUIRE( board );
3066
3067 int filledZoneCount = 0;
3068 int totalCopperZones = 0;
3069
3070 for( const ZONE* zone : board->Zones() )
3071 {
3072 if( zone->GetIsRuleArea() || zone->GetNetCode() == 0 )
3073 continue;
3074
3075 totalCopperZones++;
3076
3077 if( zone->IsFilled() )
3078 {
3079 filledZoneCount++;
3080
3081 BOOST_TEST_MESSAGE( "Filled zone: net=" << zone->GetNetname()
3082 << " layers=" << zone->GetLayerSet().count() );
3083 }
3084 }
3085
3086 BOOST_TEST_MESSAGE( "Total copper zones: " << totalCopperZones
3087 << ", filled: " << filledZoneCount );
3088
3089 BOOST_CHECK_GT( totalCopperZones, 0 );
3090 BOOST_CHECK_GT( filledZoneCount, 0 );
3091}
3092
3093
3098BOOST_AUTO_TEST_CASE( BeagleBone_Teardrops )
3099{
3100 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
3101
3102 BOARD* board = GetCachedBoard( dataPath );
3103 BOOST_REQUIRE( board );
3104
3105 int teardropZones = 0;
3106
3107 for( const ZONE* zone : board->Zones() )
3108 {
3109 if( zone->IsTeardropArea() )
3110 teardropZones++;
3111 }
3112
3113 BOOST_CHECK_GT( teardropZones, 1000 );
3114 BOOST_TEST_MESSAGE( "Teardrop zones: " << teardropZones );
3115
3116 // Pads and vias anchoring teardrops must have teardrops enabled
3117 int padsWithTeardrops = 0;
3118 int totalPads = 0;
3119
3120 for( const FOOTPRINT* fp : board->Footprints() )
3121 {
3122 for( const PAD* pad : fp->Pads() )
3123 {
3124 totalPads++;
3125
3126 if( pad->GetTeardropsEnabled() )
3127 padsWithTeardrops++;
3128 }
3129 }
3130
3131 BOOST_CHECK_GT( padsWithTeardrops, 0 );
3132 BOOST_TEST_MESSAGE( "Pads with teardrops enabled: " << padsWithTeardrops << " / " << totalPads );
3133
3134 int viasWithTeardrops = 0;
3135 int totalVias = 0;
3136
3137 for( const PCB_TRACK* track : board->Tracks() )
3138 {
3139 if( track->Type() != PCB_VIA_T )
3140 continue;
3141
3142 totalVias++;
3143
3144 if( static_cast<const PCB_VIA*>( track )->GetTeardropsEnabled() )
3145 viasWithTeardrops++;
3146 }
3147
3148 BOOST_CHECK_GT( viasWithTeardrops, 0 );
3149 BOOST_TEST_MESSAGE( "Vias with teardrops enabled: " << viasWithTeardrops << " / " << totalVias );
3150}
3151
3152
3157BOOST_AUTO_TEST_CASE( PreV172_NoTeardrops )
3158{
3159 std::string dataPath = KI_TEST::AllegroBoardFile( "ProiectBoard/ProiectBoard.brd" );
3160
3161 BOARD* board = GetCachedBoard( dataPath );
3162 BOOST_REQUIRE( board );
3163
3164 int teardropZones = 0;
3165
3166 for( const ZONE* zone : board->Zones() )
3167 {
3168 if( zone->IsTeardropArea() )
3169 teardropZones++;
3170 }
3171
3172 BOOST_CHECK_EQUAL( teardropZones, 0 );
3173
3174 int padsWithTeardrops = 0;
3175
3176 for( const FOOTPRINT* fp : board->Footprints() )
3177 {
3178 for( const PAD* pad : fp->Pads() )
3179 {
3180 if( pad->GetTeardropsEnabled() )
3181 padsWithTeardrops++;
3182 }
3183 }
3184
3185 BOOST_CHECK_EQUAL( padsWithTeardrops, 0 );
3186}
3187
3188
3194BOOST_AUTO_TEST_CASE( LegacyNetclassFlags )
3195{
3196 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
3197
3198 PCB_IO_ALLEGRO plugin;
3199 CAPTURING_REPORTER reporter;
3200 plugin.SetReporter( &reporter );
3201
3202 BOARD* rawBoard = plugin.LoadBoard( dataPath, nullptr, nullptr, nullptr );
3203
3204 BOOST_REQUIRE( rawBoard );
3205
3206 std::unique_ptr<BOARD> board( rawBoard );
3207
3208 BOOST_CHECK_MESSAGE( board->m_LegacyNetclassesLoaded,
3209 "m_LegacyNetclassesLoaded must be true after Allegro import" );
3210 BOOST_CHECK_MESSAGE( board->m_LegacyDesignSettingsLoaded,
3211 "m_LegacyDesignSettingsLoaded must be true after Allegro import" );
3212}
3213
3214
3219BOOST_AUTO_TEST_CASE( NetclassesCreatedForAllBoards )
3220{
3221 std::vector<std::string> boards = GetAllBoardFiles();
3222
3223 for( const std::string& boardPath : boards )
3224 {
3225 std::string boardName = std::filesystem::path( boardPath ).filename().string();
3226 BOARD* board = GetCachedBoard( boardPath );
3227
3228 if( !board )
3229 continue;
3230
3231 BOOST_TEST_CONTEXT( "Testing board: " << boardName )
3232 {
3233 std::shared_ptr<NET_SETTINGS> netSettings = board->GetDesignSettings().m_NetSettings;
3234 const auto& netclasses = netSettings->GetNetclasses();
3235
3236 BOOST_TEST_MESSAGE( boardName << ": " << netclasses.size() << " netclasses" );
3237
3238 for( const auto& [name, nc] : netclasses )
3239 {
3240 BOOST_TEST_MESSAGE( " " << name << ": track="
3241 << nc->GetTrackWidth() << " clearance="
3242 << nc->GetClearance() );
3243
3244 // Constraint set netclasses should have positive track width.
3245 // Skip generated netclasses (DP_, MG_, W*mil) which may not set track width.
3246 if( !name.StartsWith( wxS( "DP_" ) )
3247 && !name.StartsWith( wxS( "MG_" ) )
3248 && !name.StartsWith( wxS( "W" ) ) )
3249 {
3250 BOOST_CHECK_MESSAGE( nc->HasTrackWidth(),
3251 name << " should have a track width" );
3252 BOOST_CHECK_MESSAGE( nc->GetTrackWidth() > 0,
3253 name << " track width should be positive" );
3254 }
3255 }
3256 }
3257 }
3258}
3259
3260
3265BOOST_AUTO_TEST_CASE( BeagleBone_NetclassAssignments )
3266{
3267 std::string dataPath = KI_TEST::AllegroBoardFile( "BeagleBone_Black_RevC/BeagleBone_Black_RevC.brd" );
3268
3269 BOARD* board = GetCachedBoard( dataPath );
3270 BOOST_REQUIRE( board );
3271
3272 std::shared_ptr<NET_SETTINGS> netSettings = board->GetDesignSettings().m_NetSettings;
3273
3274 struct ExpectedNC
3275 {
3276 const char* name;
3277 int trackWidth;
3278 int clearance;
3279 };
3280
3281 ExpectedNC expectedSets[] = {
3282 { "Allegro_Default", 120650, 101600 },
3283 { "PWR", 381000, 101600 },
3284 { "BGA", 76200, 101600 },
3285 { "90_OHM_DIFF", 114300, 101600 },
3286 { "100OHM_DIFF", 95250, 101600 },
3287 };
3288
3289 for( const auto& expected : expectedSets )
3290 {
3291 wxString ncName( expected.name );
3292
3293 BOOST_CHECK_MESSAGE( netSettings->HasNetclass( ncName ),
3294 "Missing netclass: " << expected.name );
3295
3296 auto nc = netSettings->GetNetClassByName( ncName );
3297
3298 BOOST_REQUIRE_MESSAGE( nc, "Cannot retrieve netclass: " << expected.name );
3299 BOOST_CHECK_EQUAL( nc->GetTrackWidth(), expected.trackWidth );
3300 BOOST_CHECK_EQUAL( nc->GetClearance(), expected.clearance );
3301 }
3302
3303 // Diff pair netclasses
3304 BOOST_CHECK( netSettings->HasNetclass( wxS( "DP_USB0" ) ) );
3305 BOOST_CHECK( netSettings->HasNetclass( wxS( "DP_USB1" ) ) );
3306 BOOST_CHECK( netSettings->HasNetclass( wxS( "DP_HDMI_TX0" ) ) );
3307
3308 // Match group netclasses
3309 BOOST_CHECK( netSettings->HasNetclass( wxS( "MG_DDR_ADD" ) ) );
3310 BOOST_CHECK( netSettings->HasNetclass( wxS( "MG_DDR_DQ0" ) ) );
3311 BOOST_CHECK( netSettings->HasNetclass( wxS( "MG_DDR_DQ1" ) ) );
3312
3313 // Per-net trace width netclass
3314 BOOST_CHECK( netSettings->HasNetclass( wxS( "W8mil" ) ) );
3315
3316 auto ncW8 = netSettings->GetNetClassByName( wxS( "W8mil" ) );
3317
3318 BOOST_REQUIRE( ncW8 );
3319 BOOST_CHECK_EQUAL( ncW8->GetTrackWidth(), 203200 );
3320
3321 // At least some nets should have non-default netclass assignments
3322 int assignedCount = 0;
3323
3324 for( NETINFO_ITEM* net : board->GetNetInfo() )
3325 {
3326 if( net->GetNetCode() <= 0 )
3327 continue;
3328
3329 NETCLASS* nc = net->GetNetClass();
3330
3331 if( nc && nc->GetName() != NETCLASS::Default )
3332 assignedCount++;
3333 }
3334
3335 BOOST_CHECK_MESSAGE( assignedCount > 0,
3336 "At least some nets should have non-default netclass assignments" );
3337 BOOST_TEST_MESSAGE( "Nets with non-default netclass: " << assignedCount );
3338}
3339
3340
const char * name
General utilities for PCB file IO for QA programs.
BOX2< VECTOR2I > BOX2I
Definition box2.h:922
std::shared_ptr< NET_SETTINGS > m_NetSettings
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:84
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:322
const NETINFO_LIST & GetNetInfo() const
Definition board.h:996
const ZONES & Zones() const
Definition board.h:367
PCB_LAYER_ID GetLayerID(const wxString &aLayerName) const
Return the ID of a layer.
Definition board.cpp:709
const FOOTPRINTS & Footprints() const
Definition board.h:363
const TRACKS & Tracks() const
Definition board.h:361
const wxString GetLayerName(PCB_LAYER_ID aLayer) const
Return the name of a aLayer.
Definition board.cpp:729
BOARD_DESIGN_SETTINGS & GetDesignSettings() const
Definition board.cpp:1083
const DRAWINGS & Drawings() const
Definition board.h:365
constexpr BOX2< Vec > & Inflate(coord_type dx, coord_type dy)
Inflates the rectangle horizontally by dx and vertically by dy.
Definition box2.h:558
constexpr size_type GetWidth() const
Definition box2.h:214
constexpr BOX2< Vec > & Merge(const BOX2< Vec > &aRect)
Modify the position and size of the rectangle in order to contain aRect.
Definition box2.h:658
constexpr size_type GetHeight() const
Definition box2.h:215
constexpr coord_type GetLeft() const
Definition box2.h:228
constexpr bool Contains(const Vec &aPoint) const
Definition box2.h:168
constexpr coord_type GetRight() const
Definition box2.h:217
constexpr coord_type GetTop() const
Definition box2.h:229
constexpr coord_type GetBottom() const
Definition box2.h:222
EDA_ANGLE Normalize()
Definition eda_angle.h:229
double AsDegrees() const
Definition eda_angle.h:116
std::vector< VECTOR2I > GetPolyPoints() const
Duplicate the polygon outlines into a flat list of VECTOR2I points.
SHAPE_T GetShape() const
Definition eda_shape.h:169
const VECTOR2I & GetEnd() const
Return the ending point of the graphic.
Definition eda_shape.h:216
const VECTOR2I & GetStart() const
Return the starting point of the graphic.
Definition eda_shape.h:174
EDA_ANGLE GetOrientation() const
Definition footprint.h:330
std::deque< PAD * > & Pads()
Definition footprint.h:306
PCB_LAYER_ID GetLayer() const override
Return the primary layer this item is on.
Definition footprint.h:339
bool IsFlipped() const
Definition footprint.h:524
virtual void SetReporter(REPORTER *aReporter)
Set an optional reporter for warnings/errors.
Definition io_base.h:89
Hold an error message and may be used when throwing exceptions containing meaningful error messages.
virtual const wxString What() const
A composite of Problem() and Where()
virtual const char * what() const override
std::exception interface, returned as UTF-8
static ALLEGRO_CACHED_LOADER & GetInstance()
Get the singleton instance of the Allegro board cache loader.
BOARD * GetCachedBoard(const std::string &aFilePath)
Get a cached board for the given file path, or load it if not already cached, without forcing a reloa...
Custom REPORTER that captures all messages for later analysis in the unit test framework.
void PrintAllMessages(const std::string &aContext) const
LSET is a set of PCB_LAYER_IDs.
Definition lset.h:37
static LSET AllCuMask(int aCuLayerCount)
Return a mask holding the requested number of Cu PCB_LAYER_IDs.
Definition lset.cpp:599
bool Contains(PCB_LAYER_ID aLayer) const
See if the layer set contains a PCB layer.
Definition lset.h:63
A collection of nets and the parameters used to route or test these nets.
Definition netclass.h:45
static const char Default[]
the name of the default NETCLASS
Definition netclass.h:47
const wxString GetName() const
Gets the name of this (maybe aggregate) netclass in a format for internal usage or for export to exte...
Definition netclass.cpp:328
Handle the data for a net.
Definition netinfo.h:54
Definition pad.h:55
BOARD * LoadBoard(const wxString &aFileName, BOARD *aAppendToMe, const std::map< std::string, UTF8 > *aProperties, PROJECT *aProject) override
Load information from some input file format that this PCB_IO implementation knows about into either ...
PCB_LAYER_ID GetLayer() const override
Return the primary layer this item is on.
Definition pcb_shape.h:71
const VECTOR2I & GetStart() const
Definition pcb_track.h:97
const VECTOR2I & GetEnd() const
Definition pcb_track.h:94
Definition seg.h:42
Handle a list of polygons defining a copper zone.
Definition zone.h:73
bool IsFilled() const
Definition zone.h:297
PCB_LAYER_ID GetFirstLayer() const
Definition zone.cpp:523
@ SEGMENT
Definition eda_shape.h:45
@ RECTANGLE
Use RECTANGLE instead of RECT to avoid collision in a Windows header.
Definition eda_shape.h:46
bool IsFrontLayer(PCB_LAYER_ID aLayerId)
Layer classification: check if it's a front layer.
Definition layer_ids.h:780
bool IsBackLayer(PCB_LAYER_ID aLayerId)
Layer classification: check if it's a back layer.
Definition layer_ids.h:803
bool IsCopperLayer(int aLayerId)
Test whether a layer is a copper layer.
Definition layer_ids.h:677
PCB_LAYER_ID
A quick note on layer IDs:
Definition layer_ids.h:60
@ Edge_Cuts
Definition layer_ids.h:112
@ B_Cu
Definition layer_ids.h:65
@ F_Fab
Definition layer_ids.h:119
@ UNDEFINED_LAYER
Definition layer_ids.h:61
@ F_Cu
Definition layer_ids.h:64
@ B_Fab
Definition layer_ids.h:118
std::array< SEG, 4 > BoxToSegs(const BOX2I &aBox)
Decompose a BOX2 into four segments.
std::string AllegroBoardDataDir(const std::string &aBoardName)
void PrintBoardStats(const BOARD *aBoard, const std::string &aBoardName)
Print detailed board statistics for debugging using test-framework logging.
std::string AllegroBoardFile(const std::string &aFileName)
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:400
PAD_ATTRIB
The set of pad shapes, used with PAD::{Set,Get}Attribute().
Definition padstack.h:97
@ NPTH
like PAD_PTH, but not plated mechanical use only, no connection allowed
Definition padstack.h:103
@ SMD
Smd pad, appears on the solder paste layer (default)
Definition padstack.h:99
@ PTH
Plated through hole pad.
Definition padstack.h:98
Utility functions for working with shapes.
Parse board outline geometry from a .alg ASCII reference file.
void updateBounds(double aX, double aY)
static ALG_OUTLINE_DATA ParseAlgOutlines(const std::string &aPath)
std::vector< OUTLINE_SEGMENT > designOutlineSegments
std::vector< OUTLINE_SEGMENT > outlineSegments
int expectedEdgeCutsSegments() const
Expected number of Edge_Cuts segments when translating to KiCad.
std::set< wxString > netNames
static ALG_REFERENCE_DATA ParseAlgFile(const std::string &aPath)
std::map< wxString, wxString > refDesToSymName
std::set< wxString > refDes
static std::vector< wxString > SplitAlgLine(const wxString &aLine)
std::vector< ALG_ZONE_POLYGON > zonePolygons
static int ParseRecordId(const wxString &aTag)
Extract the integer record ID from a RECORD_TAG field like "36 1 0".
std::map< wxString, std::set< wxString > > netToRefDes
Parse a FabMaster .alg file and extract reference data for cross-validation.
void AddPoint(double aX, double aY)
Data for parameterized all-boards test.
Fixture for comprehensive board import tests with error capturing.
BOARD * GetCachedBoard(const std::string &aFilePath)
Get a cached board, loading it on first access.
static std::vector< std::string > GetAllBoardFiles()
Get list of all .brd files in the Allegro test data directory.
std::unique_ptr< BOARD > LoadAllegroBoard(const std::string &aFileName)
BOOST_AUTO_TEST_CASE(FootprintRefDes)
Test that footprints have valid reference designators.
static void AssertOutlineValid(const BOARD &aBoard)
static std::vector< BRD_ALG_PAIR > getBoardsWithAlg()
Get a list of all board files in the test data that have a corresponding .alg reference file.
static unsigned CountOutlineElements(const BOARD &board)
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
VECTOR3I expected(15, 30, 45)
VECTOR2I end
BOOST_TEST_CONTEXT("Test Clearance")
int clearance
BOOST_TEST_MESSAGE("Polyline has "<< chain.PointCount()<< " points")
BOOST_CHECK_EQUAL(result, "25.4")
@ PCB_SHAPE_T
class PCB_SHAPE, a segment not on copper layers
Definition typeinfo.h:88
@ PCB_VIA_T
class PCB_VIA, a via (like a track segment on a copper layer)
Definition typeinfo.h:97
@ PCB_TEXT_T
class PCB_TEXT, text on a layer
Definition typeinfo.h:92
@ PCB_ARC_T
class PCB_ARC, an arc track segment on a copper layer
Definition typeinfo.h:98
@ PCB_TRACE_T
class PCB_TRACK, a track segment (segment on a copper layer)
Definition typeinfo.h:96
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:695