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