KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_ipc2581_export.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, see <https://www.gnu.org/licenses/>.
18 */
19
28
31#include <qa_utils/file_utils.h>
33
36
37#include <board.h>
40#include <footprint.h>
41#include <netinfo.h>
42#include <pad.h>
43#include <pcb_shape.h>
44#include <pcb_textbox.h>
45#include <pcb_track.h>
46#include <base_units.h>
47
48#include <wx/dir.h>
49#include <wx/file.h>
50#include <wx/filename.h>
51#include <wx/process.h>
52#include <wx/txtstrm.h>
53
54#include <cmath>
55#include <fstream>
56#include <regex>
57#include <sstream>
58
59
60namespace
61{
62
66bool IsXmllintAvailable()
67{
68 wxArrayString output;
69 wxArrayString errors;
70 int result = wxExecute( "xmllint --version", output, errors, wxEXEC_SYNC );
71 return result == 0;
72}
73
74
79wxString ValidateXmlWithXsd( const wxString& aXmlPath, const wxString& aXsdPath )
80{
81 wxString cmd = wxString::Format( "xmllint --noout --schema \"%s\" \"%s\"",
82 aXsdPath, aXmlPath );
83
84 wxArrayString output;
85 wxArrayString errors;
86 int result = wxExecute( cmd, output, errors, wxEXEC_SYNC );
87
88 if( result != 0 )
89 {
90 wxString errorMsg;
91
92 for( const wxString& line : errors )
93 errorMsg += line + "\n";
94
95 return errorMsg;
96 }
97
98 return wxEmptyString;
99}
100
101
105bool FileContainsPattern( const wxString& aFilePath, const wxString& aPattern )
106{
107 std::ifstream file( aFilePath.ToStdString() );
108
109 if( !file.is_open() )
110 return false;
111
112 std::stringstream buffer;
113 buffer << file.rdbuf();
114 std::string content = buffer.str();
115
116 return content.find( aPattern.ToStdString() ) != std::string::npos;
117}
118
119
120bool XmlRegionHasCoordinateNear( const std::string& aRegion, char aAxis, double aExpected, double aTolerance )
121{
122 std::regex coordinateRegex( std::string( 1, aAxis ) + "=\"(-?[0-9]+(?:\\.[0-9]+)?)\"" );
123
124 for( std::sregex_iterator it( aRegion.begin(), aRegion.end(), coordinateRegex ), end; it != end; ++it )
125 {
126 if( std::abs( std::stod( ( *it )[1].str() ) - aExpected ) <= aTolerance )
127 return true;
128 }
129
130 return false;
131}
132
133
138static const std::vector<std::string> VALIDATION_TEST_BOARDS = {
139 "custom_pads.kicad_pcb",
140 "notched_zones.kicad_pcb",
141 "sliver.kicad_pcb",
142 "tracks_arcs_vias.kicad_pcb",
143 "issue7241.kicad_pcb",
144 "issue10906.kicad_pcb",
145 "issue22798.kicad_pcb",
146 "padstacks_complex.kicad_pcb",
147 "issue12609.kicad_pcb",
148 "issue22794.kicad_pcb",
149};
150
151} // anonymous namespace
152
153
155{
157 m_xmllintAvailable( IsXmllintAvailable() )
158 {
159 }
160
162 {
163 // Clean up temporary files
164 for( const wxString& path : m_tempFiles )
165 {
166 if( wxFileExists( path ) )
167 wxRemoveFile( path );
168 }
169 }
170
171 wxString CreateTempFile( const wxString& aSuffix = wxT( "" ) )
172 {
173 wxString path = wxFileName::CreateTempFileName( wxT( "kicad_ipc2581_test" ) );
174
175 if( !aSuffix.IsEmpty() )
176 path += aSuffix;
177 else
178 path += wxT( ".xml" );
179
180 m_tempFiles.push_back( path );
181 return path;
182 }
183
184 wxString GetXsdPath( char aVersion )
185 {
186 wxString filename = ( aVersion == 'C' ) ? wxT( "IPC-2581C.xsd" ) : wxT( "IPC-2581B1.xsd" );
187 return KI_TEST::GetPcbnewTestDataDir() + "ipc2581/" + filename;
188 }
189
190 std::unique_ptr<BOARD> LoadBoard( const std::string& aRelativePath )
191 {
192 std::string fullPath = KI_TEST::GetPcbnewTestDataDir() + aRelativePath;
193
194 return m_kicadPlugin.LoadBoard( fullPath );
195 }
196
197 bool ExportAndValidate( BOARD& aBoard, char aVersion, wxString& aErrorMsg,
198 const std::string& aMode = std::string(),
199 const std::string& aRefDes = std::string(),
200 const std::string& aSections = std::string() )
201 {
202 wxString tempPath = CreateTempFile();
203
204 std::map<std::string, UTF8> props;
205 props["units"] = "mm";
206 props["version"] = std::string( 1, aVersion );
207 props["sigfig"] = "3";
208
209 if( !aMode.empty() )
210 props["mode"] = aMode;
211
212 if( !aRefDes.empty() )
213 {
214 props["refdes"] = aRefDes;
215 props["netnames"] = "anonymize";
216 }
217
218 if( !aSections.empty() )
219 props["sections"] = aSections;
220
221 try
222 {
223 m_ipc2581Plugin.SaveBoard( tempPath, aBoard, &props );
224 }
225 catch( const std::exception& e )
226 {
227 aErrorMsg = wxString::Format( "Export failed: %s", e.what() );
228 return false;
229 }
230
231 if( !wxFileExists( tempPath ) )
232 {
233 aErrorMsg = "Export file was not created";
234 return false;
235 }
236
238 {
239 wxString xsdPath = GetXsdPath( aVersion );
240
241 if( wxFileExists( xsdPath ) )
242 {
243 aErrorMsg = ValidateXmlWithXsd( tempPath, xsdPath );
244 return aErrorMsg.IsEmpty();
245 }
246 }
247
248 // If xmllint not available, just check that export succeeded
249 return true;
250 }
251
253 std::vector<wxString> m_tempFiles;
256};
257
258
259BOOST_FIXTURE_TEST_SUITE( Ipc2581Export, IPC2581_EXPORT_FIXTURE )
260
261
262
270BOOST_AUTO_TEST_CASE( SurfaceFinishExport )
271{
272 // Load a board with ENIG surface finish (issue3812.kicad_pcb has ENIG)
273 std::unique_ptr<BOARD> board = LoadBoard( "issue3812.kicad_pcb" );
274
275 BOOST_REQUIRE( board );
276
277 // Verify the board has ENIG finish
278 const BOARD_STACKUP& stackup = board->GetDesignSettings().GetStackupDescriptor();
279 BOOST_CHECK_EQUAL( stackup.m_FinishType, wxT( "ENIG" ) );
280
281 // Export to IPC-2581 version C
282 wxString tempPath = CreateTempFile();
283
284 std::map<std::string, UTF8> props;
285 props["units"] = "mm";
286 props["version"] = "C";
287 props["sigfig"] = "3";
288
289 m_ipc2581Plugin.SaveBoard( tempPath, *board, &props );
290
291 BOOST_REQUIRE( wxFileExists( tempPath ) );
292
293 // Verify SurfaceFinish element is present with correct type attribute
294 // Schema requires: <SurfaceFinish type="ENIG-N"/>
295 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "<SurfaceFinish" ) ),
296 "SurfaceFinish element should be present" );
297 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "type=\"ENIG-N\"" ) ),
298 "SurfaceFinish type should be ENIG-N" );
299
300 // Verify coating layers are present
301 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "COATING_TOP" ) ),
302 "COATING_TOP layer should be present" );
303 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "COATING_BOTTOM" ) ),
304 "COATING_BOTTOM layer should be present" );
305 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "layerFunction=\"COATINGCOND\"" ) ),
306 "Coating layers should have layerFunction=COATINGCOND" );
307
308 // Note: XSD validation is done separately in SchemaValidation tests.
309 // This test focuses on verifying the surface finish elements are present.
310}
311
312
316BOOST_AUTO_TEST_CASE( NoSurfaceFinishExport )
317{
318 // Load a board without surface finish (vme-wren.kicad_pcb has "None")
319 std::unique_ptr<BOARD> board = LoadBoard( "vme-wren.kicad_pcb" );
320
321 BOOST_REQUIRE( board );
322
323 // Verify the board has no finish
324 const BOARD_STACKUP& stackup = board->GetDesignSettings().GetStackupDescriptor();
325 BOOST_CHECK( stackup.m_FinishType == wxT( "None" ) || stackup.m_FinishType.IsEmpty() );
326
327 // Export to IPC-2581 version C
328 wxString tempPath = CreateTempFile();
329
330 std::map<std::string, UTF8> props;
331 props["units"] = "mm";
332 props["version"] = "C";
333 props["sigfig"] = "3";
334
335 m_ipc2581Plugin.SaveBoard( tempPath, *board, &props );
336
337 BOOST_REQUIRE( wxFileExists( tempPath ) );
338
339 // Verify SurfaceFinish element is NOT present
340 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "<SurfaceFinish" ) ),
341 "SurfaceFinish element should not be present for 'None' finish" );
342
343 // Verify coating layers are NOT present
344 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "COATING_TOP" ) ),
345 "COATING_TOP layer should not be present" );
346 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "COATING_BOTTOM" ) ),
347 "COATING_BOTTOM layer should not be present" );
348
349 // Note: XSD validation is done separately in SchemaValidation tests.
350 // This test focuses on verifying coating layers are NOT present for "None" finish.
351}
352
353
360BOOST_AUTO_TEST_CASE( SchemaValidationVersionB )
361{
362 if( !m_xmllintAvailable )
363 {
364 BOOST_WARN_MESSAGE( false, "xmllint not available, skipping schema validation tests" );
365 return;
366 }
367
368 wxString xsdPath = GetXsdPath( 'B' );
369
370 if( !wxFileExists( xsdPath ) )
371 {
372 BOOST_WARN_MESSAGE( false, "IPC-2581B1.xsd not found, skipping schema validation" );
373 return;
374 }
375
376 for( const std::string& boardFile : VALIDATION_TEST_BOARDS )
377 {
378 BOOST_TEST_CONTEXT( "Board: " << boardFile << " (Version B)" )
379 {
380 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
381
382 if( !board )
383 {
384 BOOST_WARN_MESSAGE( false, "Could not load board: " + boardFile );
385 continue;
386 }
387
388 wxString errorMsg;
389 bool valid = ExportAndValidate( *board, 'B', errorMsg );
390
391 BOOST_CHECK_MESSAGE( valid, "IPC-2581B validation failed for " + boardFile + ": " + errorMsg );
392 }
393 }
394}
395
396
403BOOST_AUTO_TEST_CASE( SchemaValidationVersionC )
404{
405 if( !m_xmllintAvailable )
406 {
407 BOOST_WARN_MESSAGE( false, "xmllint not available, skipping schema validation tests" );
408 return;
409 }
410
411 wxString xsdPath = GetXsdPath( 'C' );
412
413 if( !wxFileExists( xsdPath ) )
414 {
415 BOOST_WARN_MESSAGE( false, "IPC-2581C.xsd not found, skipping schema validation" );
416 return;
417 }
418
419 for( const std::string& boardFile : VALIDATION_TEST_BOARDS )
420 {
421 BOOST_TEST_CONTEXT( "Board: " << boardFile << " (Version C)" )
422 {
423 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
424
425 if( !board )
426 {
427 BOOST_WARN_MESSAGE( false, "Could not load board: " + boardFile );
428 continue;
429 }
430
431 wxString errorMsg;
432 bool valid = ExportAndValidate( *board, 'C', errorMsg );
433
434 BOOST_CHECK_MESSAGE( valid, "IPC-2581C validation failed for " + boardFile + ": " + errorMsg );
435 }
436 }
437}
438
439
450BOOST_AUTO_TEST_CASE( FunctionModeSchemaValidation )
451{
452 if( !m_xmllintAvailable )
453 {
454 BOOST_WARN_MESSAGE( false, "xmllint not available, skipping schema validation tests" );
455 return;
456 }
457
458 static const std::vector<std::string> modes = { "userdef", "bom", "stackup", "fabrication",
459 "assembly", "test", "stencil" };
460
461 // Boards chosen for differing content: inner copper, custom pad shapes, and vias
462 static const std::vector<std::string> boards = { "padstacks_complex.kicad_pcb",
463 "custom_pads.kicad_pcb",
464 "tracks_arcs_vias.kicad_pcb" };
465
466 for( const std::string& boardFile : boards )
467 {
468 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
469
470 if( !board )
471 {
472 BOOST_WARN_MESSAGE( false, "Could not load board: " + boardFile );
473 continue;
474 }
475
476 for( const std::string& mode : modes )
477 {
478 for( char version : { 'B', 'C' } )
479 {
480 if( !wxFileExists( GetXsdPath( version ) ) )
481 continue;
482
483 for( const std::string& refdes : { std::string(), std::string( "omit" ) } )
484 {
485 BOOST_TEST_CONTEXT( "Board: " << boardFile << " Mode: " << mode
486 << " Version: " << version
487 << " RefDes: " << ( refdes.empty() ? "include" : refdes ) )
488 {
489 wxString errorMsg;
490 bool valid = ExportAndValidate( *board, version, errorMsg, mode,
491 refdes );
492
493 BOOST_CHECK_MESSAGE( valid, "validation failed: " + errorMsg );
494 }
495 }
496 }
497 }
498 }
499}
500
501
506BOOST_AUTO_TEST_CASE( FunctionModeSuppressedReferences )
507{
508 if( !m_xmllintAvailable )
509 {
510 BOOST_WARN_MESSAGE( false, "xmllint not available, skipping schema validation tests" );
511 return;
512 }
513
514 // Components without packages, without a BOM, and without padstacks respectively
515 static const std::vector<std::string> sectionKeys = { "AOU", "ACOU", "KACOU", "ABOU" };
516
517 std::unique_ptr<BOARD> board = LoadBoard( "padstacks_complex.kicad_pcb" );
518
519 BOOST_REQUIRE( board );
520
521 for( const std::string& sections : sectionKeys )
522 {
523 for( char version : { 'B', 'C' } )
524 {
525 if( !wxFileExists( GetXsdPath( version ) ) )
526 continue;
527
528 BOOST_TEST_CONTEXT( "Sections: " << sections << " Version: " << version )
529 {
530 wxString errorMsg;
531 bool valid = ExportAndValidate( *board, version, errorMsg, "userdef",
532 std::string(), sections );
533
534 // Revision B cannot express components without packages and must say so
535 if( version == 'B' && sections.find( 'C' ) == std::string::npos )
536 BOOST_CHECK_MESSAGE( !valid, "IPC-2581B accepted components with no package" );
537 else
538 BOOST_CHECK_MESSAGE( valid, "validation failed: " + errorMsg );
539 }
540 }
541 }
542}
543
544
545BOOST_AUTO_TEST_CASE( ComplexBoardExport )
546{
547 // Test boards with specific complex features
548 static const std::vector<std::string> complexBoards = {
549 "intersectingzones.kicad_pcb",
550 "custom_pads.kicad_pcb",
551 };
552
553 for( const std::string& boardFile : complexBoards )
554 {
555 BOOST_TEST_CONTEXT( "Complex board: " << boardFile )
556 {
557 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
558
559 if( !board )
560 {
561 BOOST_WARN_MESSAGE( false, "Could not load board: " + boardFile );
562 continue;
563 }
564
565 // Test both versions
566 for( char version : { 'B', 'C' } )
567 {
568 BOOST_TEST_CONTEXT( "Version " << version )
569 {
570 wxString errorMsg;
571 bool valid = ExportAndValidate( *board, version, errorMsg );
572
573 BOOST_CHECK_MESSAGE( valid,
574 wxString::Format( "Export/validation failed for %s version %c: %s",
575 boardFile, version, errorMsg ) );
576 }
577 }
578 }
579 }
580}
581
582
583BOOST_AUTO_TEST_CASE( DegenerateTrackArcExportsAsLine )
584{
585 BOARD board;
586 board.SetCopperLayerCount( 2 );
587
588 NETINFO_ITEM* net = new NETINFO_ITEM( &board, wxT( "TestNet" ), 1 );
589 board.Add( net );
590
591 PCB_ARC* arc = new PCB_ARC( &board );
592 arc->SetStart( VECTOR2I( 110737101, 51206997 ) );
593 arc->SetMid( VECTOR2I( 110737003, 51206898 ) );
594 arc->SetEnd( VECTOR2I( 110736905, 51206799 ) );
595 arc->SetWidth( pcbIUScale.mmToIU( 0.11684 ) );
596 arc->SetLayer( F_Cu );
597 arc->SetNet( net );
598 board.Add( arc );
599
600 wxString tempPath = CreateTempFile();
601
602 std::map<std::string, UTF8> props;
603 props["units"] = "mm";
604 props["version"] = "C";
605 props["sigfig"] = "6";
606
607 m_ipc2581Plugin.SaveBoard( tempPath, board, &props );
608 BOOST_REQUIRE( wxFileExists( tempPath ) );
609
610 BOOST_CHECK( FileContainsPattern( tempPath, wxT( "<Line " ) ) );
611 BOOST_CHECK( !FileContainsPattern( tempPath, wxT( "<Arc " ) ) );
612}
613
614
622BOOST_AUTO_TEST_CASE( TextBoxUsesDrawPosition )
623{
624 BOARD board;
625
626 PCB_TEXTBOX* textbox = new PCB_TEXTBOX( &board );
627 textbox->SetLayer( F_SilkS );
628 textbox->SetStart( { pcbIUScale.mmToIU( 100 ), pcbIUScale.mmToIU( 50 ) } );
629 textbox->SetEnd( { pcbIUScale.mmToIU( 120 ), pcbIUScale.mmToIU( 60 ) } );
630 textbox->SetText( wxT( "IPC textbox" ) );
631 textbox->SetTextSize( { pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) } );
632 textbox->SetTextThickness( pcbIUScale.mmToIU( 0.15 ) );
635 textbox->SetMarginLeft( 0 );
636 textbox->SetMarginTop( 0 );
637 textbox->SetBorderEnabled( false );
638 board.Add( textbox );
639
640 wxString tempPath = CreateTempFile();
641
642 std::map<std::string, UTF8> props;
643 props["units"] = "mm";
644 props["version"] = "C";
645 props["sigfig"] = "6";
646
647 m_ipc2581Plugin.SaveBoard( tempPath, board, &props );
648 BOOST_REQUIRE( wxFileExists( tempPath ) );
649
650 std::ifstream xmlFile( tempPath.ToStdString() );
651 BOOST_REQUIRE( xmlFile.is_open() );
652
653 std::string xmlContent( ( std::istreambuf_iterator<char>( xmlFile ) ), std::istreambuf_iterator<char>() );
654 size_t textStart = xmlContent.find( "value=\"IPC textbox\"" );
655 BOOST_REQUIRE_MESSAGE( textStart != std::string::npos, "Export should contain the text box feature" );
656
657 size_t setEnd = xmlContent.find( "</Set>", textStart );
658 BOOST_REQUIRE( setEnd != std::string::npos );
659
660 std::string textFeature = xmlContent.substr( textStart, setEnd - textStart );
661
662 BOOST_CHECK_MESSAGE( XmlRegionHasCoordinateNear( textFeature, 'x', 100.0, 5.0 ),
663 "Text box geometry should use its X drawing position" );
664 BOOST_CHECK_MESSAGE( XmlRegionHasCoordinateNear( textFeature, 'y', -50.0, 5.0 ),
665 "Text box geometry should use its Y drawing position" );
666}
667
668
676BOOST_AUTO_TEST_CASE( SmdPadSolderMaskExport_Issue16658 )
677{
678 // Load a board with standard SMD components (capacitors using SMD footprints)
679 std::unique_ptr<BOARD> board = LoadBoard( "issue16658/issue16658.kicad_pcb" );
680
681 BOOST_REQUIRE( board );
682
683 // Verify the board has SMD pads with implicit mask openings
684 bool hasSmtPad = false;
685
686 for( FOOTPRINT* fp : board->Footprints() )
687 {
688 for( PAD* pad : fp->Pads() )
689 {
690 if( pad->GetAttribute() == PAD_ATTRIB::SMD )
691 {
692 hasSmtPad = true;
693
694 // Verify pad is on copper but NOT explicitly on mask layer
695 bool isOnCopperOnly = pad->IsOnLayer( F_Cu ) && !pad->IsOnLayer( F_Mask );
696
697 if( isOnCopperOnly )
698 {
699 // This is the condition we're testing
700 break;
701 }
702 }
703 }
704
705 if( hasSmtPad )
706 break;
707 }
708
709 BOOST_REQUIRE_MESSAGE( hasSmtPad, "Test board should have SMD pads" );
710
711 // Export to IPC-2581 version C
712 wxString tempPath = CreateTempFile();
713
714 std::map<std::string, UTF8> props;
715 props["units"] = "mm";
716 props["version"] = "C";
717 props["sigfig"] = "3";
718
719 m_ipc2581Plugin.SaveBoard( tempPath, *board, &props );
720
721 BOOST_REQUIRE( wxFileExists( tempPath ) );
722
723 // Verify that F_Mask layer features are present in the export
724 // (this was the bug - mask layers were empty for SMD pads)
725 bool hasFMaskLayer = FileContainsPattern( tempPath, wxT( "layerRef=\"F.Mask\"" ) )
726 || FileContainsPattern( tempPath, wxT( "layerRef=\"TSM\"" ) );
727
728 BOOST_CHECK_MESSAGE( hasFMaskLayer,
729 "IPC-2581 export should contain F.Mask layer features for SMD pads" );
730
731 // Also check for LayerFeature element with mask layer reference
732 bool hasLayerFeature = FileContainsPattern( tempPath, wxT( "<LayerFeature" ) );
733 BOOST_CHECK_MESSAGE( hasLayerFeature, "IPC-2581 export should contain LayerFeature elements" );
734}
735
736
744BOOST_AUTO_TEST_CASE( EmptyRefDesProducesValidXml )
745{
746 std::unique_ptr<BOARD> board = LoadBoard( "padstacks_complex.kicad_pcb" );
747 BOOST_REQUIRE( board );
748
749 for( char version : { 'B', 'C' } )
750 {
751 BOOST_TEST_CONTEXT( "Version " << version )
752 {
753 wxString tempPath = CreateTempFile();
754
755 std::map<std::string, UTF8> props;
756 props["units"] = "mm";
757 props["version"] = std::string( 1, version );
758 props["sigfig"] = "3";
759
760 m_ipc2581Plugin.SaveBoard( tempPath, *board, &props );
761 BOOST_REQUIRE( wxFileExists( tempPath ) );
762
763 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "refDes=\"\"" ) ),
764 "Empty refDes attribute found" );
765 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "<RefDes name=\"\"" ) ),
766 "Empty RefDes/@name attribute found" );
767 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "componentRef=\"\"" ) ),
768 "Empty PinRef/@componentRef attribute found" );
769 }
770
771 m_ipc2581Plugin = PCB_IO_IPC2581();
772 }
773}
774
775
784BOOST_AUTO_TEST_CASE( FlippedComponentRotation )
785{
786 std::unique_ptr<BOARD> board = LoadBoard( "issue12609.kicad_pcb" );
787 BOOST_REQUIRE( board );
788
789 wxString tempPath = CreateTempFile();
790
791 std::map<std::string, UTF8> props;
792 props["units"] = "mm";
793 props["version"] = "C";
794 props["sigfig"] = "3";
795
796 m_ipc2581Plugin.SaveBoard( tempPath, *board, &props );
797 BOOST_REQUIRE( wxFileExists( tempPath ) );
798
799 // C5 is on B.Cu at 90 degrees. Its Component Xform must be rotation="90.0",
800 // not the inverted "270.0". Check by finding Component refDes="C5" and verifying
801 // its Xform has rotation="90.0".
802 std::ifstream xmlFile( tempPath.ToStdString() );
803 BOOST_REQUIRE( xmlFile.is_open() );
804
805 std::string xmlContent( ( std::istreambuf_iterator<char>( xmlFile ) ),
806 std::istreambuf_iterator<char>() );
807
808 // Find the C5 component and check its rotation
809 size_t c5Pos = xmlContent.find( "refDes=\"C5\"" );
810
811 if( c5Pos == std::string::npos )
812 c5Pos = xmlContent.find( "refDes=\"NOREF_" );
813
814 BOOST_REQUIRE_MESSAGE( c5Pos != std::string::npos,
815 "C5 component should exist in export" );
816
817 // Look for the Xform within the next 200 chars after refDes="C5"
818 std::string c5Region = xmlContent.substr( c5Pos, 200 );
819 BOOST_CHECK_MESSAGE( c5Region.find( "rotation=\"90.0\"" ) != std::string::npos
820 || c5Region.find( "rotation=\"90.00\"" ) != std::string::npos,
821 "C5 component rotation should be 90, not inverted. Region: "
822 + c5Region );
823}
824
825
829BOOST_AUTO_TEST_CASE( ContentBomRef )
830{
831 std::unique_ptr<BOARD> board = LoadBoard( "issue12609.kicad_pcb" );
832 BOOST_REQUIRE( board );
833
834 wxString tempPath = CreateTempFile();
835
836 std::map<std::string, UTF8> props;
837 props["units"] = "mm";
838 props["version"] = "C";
839 props["sigfig"] = "3";
840
841 m_ipc2581Plugin.SaveBoard( tempPath, *board, &props );
842 BOOST_REQUIRE( wxFileExists( tempPath ) );
843
844 bool hasBom = FileContainsPattern( tempPath, wxT( "<Bom " ) );
845 bool hasBomRef = FileContainsPattern( tempPath, wxT( "<BomRef " ) );
846
847 if( hasBom )
848 {
849 BOOST_CHECK_MESSAGE( hasBomRef,
850 "Content should have BomRef when Bom section is present" );
851 }
852}
853
854
868BOOST_AUTO_TEST_CASE( KnockoutTextMultiContour_Issue23968 )
869{
870 std::unique_ptr<BOARD> board = LoadBoard( "test_copper_graphics.kicad_pcb" );
871 BOOST_REQUIRE( board );
872
873 for( char version : { 'B', 'C' } )
874 {
875 BOOST_TEST_CONTEXT( "Version " << version )
876 {
877 wxString errorMsg;
878 bool valid = ExportAndValidate( *board, version, errorMsg );
879
880 BOOST_CHECK_MESSAGE( valid,
881 wxString::Format( "Knockout text export should be schema-valid "
882 "(version %c): %s", version, errorMsg ) );
883 }
884
885 m_ipc2581Plugin = PCB_IO_IPC2581();
886 }
887}
888
889
901BOOST_AUTO_TEST_CASE( BackdrillSpecEncoding )
902{
903 // Build a minimal 6-layer synthetic board so we can place a via whose
904 // backdrill targets a specific must-cut layer.
905 BOARD board;
906 board.SetCopperLayerCount( 6 );
907
910
911 // Front-side backdrill: drill from F_Cu, must cut through In3_Cu. The
912 // must-not-cut layer should therefore resolve to In4_Cu (the next signal
913 // layer past must-cut going inward from the start surface).
914 PCB_VIA* via = new PCB_VIA( &board );
915 via->SetPadstackMode( PADSTACK::MODE::NORMAL );
916 via->SetPosition( VECTOR2I( pcbIUScale.mmToIU( 5 ), pcbIUScale.mmToIU( 5 ) ) );
917 via->SetLayerPair( F_Cu, B_Cu );
918 via->SetDrill( pcbIUScale.mmToIU( 0.30 ) );
919 via->SetWidth( PADSTACK::ALL_LAYERS, pcbIUScale.mmToIU( 0.60 ) );
920 via->SetSecondaryDrillSize( pcbIUScale.mmToIU( 0.40 ) );
921 via->SetSecondaryDrillStartLayer( F_Cu );
922 via->SetSecondaryDrillEndLayer( In3_Cu );
923 via->SetFrontPostMachiningMode( PAD_DRILL_POST_MACHINING_MODE::COUNTERSINK );
924 board.Add( via );
925
926 wxString tempPath = CreateTempFile();
927 std::map<std::string, UTF8> props;
928 props["units"] = "mm";
929 props["version"] = "C";
930 props["sigfig"] = "4";
931
932 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, board, &props ) );
933 BOOST_REQUIRE( wxFileExists( tempPath ) );
934
935 BOOST_CHECK_MESSAGE(
936 FileContainsPattern( tempPath, wxT( "<Backdrill type=\"START_LAYER\"" ) ),
937 "Backdrill spec should declare a START_LAYER child" );
938 BOOST_CHECK_MESSAGE(
939 FileContainsPattern( tempPath, wxT( "<Backdrill type=\"MUST_NOT_CUT_LAYER\"" ) ),
940 "Backdrill spec should declare a MUST_NOT_CUT_LAYER child" );
941 BOOST_CHECK_MESSAGE(
942 FileContainsPattern( tempPath, wxT( "<Backdrill type=\"MAX_STUB_LENGTH\"" ) ),
943 "Backdrill spec should declare a MAX_STUB_LENGTH child" );
944
945 // Schema requires layer references via Property layerOrGroupRef, not as
946 // Backdrill attributes.
947 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "layerOrGroupRef=" ) ),
948 "Backdrill must convey layers through Property layerOrGroupRef" );
949 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "startLayerRef=" ) ),
950 "Backdrill should not use schema-invalid startLayerRef attribute" );
951 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "mustNotCutLayerRef=" ) ),
952 "Backdrill should not use schema-invalid mustNotCutLayerRef attribute" );
953 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "maxStubLength=" ) ),
954 "Backdrill should not use schema-invalid maxStubLength attribute" );
955 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "postMachining=" ) ),
956 "Backdrill should not use the non-standard postMachining attribute" );
957
958 // The must-not-cut layer for a backdrill from F_Cu through In3_Cu in a
959 // 6-layer stack must resolve to In4_Cu, not the must-cut layer itself.
960 BOOST_CHECK_MESSAGE(
961 FileContainsPattern( tempPath, wxT( "layerOrGroupRef=\"In4.Cu\"" ) ),
962 "Front backdrill must-not-cut layer should be In4.Cu" );
963
964 // The third (primary) backdrill spec slot has been removed; through-drills
965 // must not be exported as backdrill specs.
966 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "BD_1C" ) ),
967 "Exporter should not emit a primary backdrill spec slot" );
968
969 // Counterbore/countersink encoded as a Backdrill type=OTHER child with
970 // a comment, never as a non-standard postMachining attribute.
971 BOOST_CHECK_MESSAGE(
972 FileContainsPattern( tempPath, wxT( "<Backdrill type=\"OTHER\"" ) ),
973 "Post-machining hint should produce a Backdrill type=OTHER child" );
974 BOOST_CHECK_MESSAGE(
975 FileContainsPattern( tempPath, wxT( "comment=\"post-machining=COUNTERSINK\"" ) ),
976 "OTHER Backdrill should carry the post-machining comment" );
977}
978
979
991BOOST_AUTO_TEST_CASE( ExposedPadPasteRespected_Issue24318 )
992{
993 BOARD board;
994
995 FOOTPRINT* fp = new FOOTPRINT( &board );
996 fp->SetReference( wxT( "U1" ) );
997 fp->SetPosition( VECTOR2I( pcbIUScale.mmToIU( 50 ), pcbIUScale.mmToIU( 50 ) ) );
998 board.Add( fp );
999
1000 // Copper-only thermal pad: F.Cu + F.Mask, deliberately NOT on F.Paste.
1001 PAD* thermalPad = new PAD( fp );
1003 thermalPad->SetNumber( wxT( "33" ) );
1004 thermalPad->SetAttribute( PAD_ATTRIB::SMD );
1005 thermalPad->SetProperty( PAD_PROP::HEATSINK );
1007 thermalPad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 3.45 ), pcbIUScale.mmToIU( 3.45 ) ) );
1008 thermalPad->SetLayerSet( LSET( { F_Cu, F_Mask } ) );
1009 fp->Add( thermalPad );
1010
1011 // Paste-only aperture pad, models a stencil opening for the thermal pad.
1012 PAD* pasteAperture = new PAD( fp );
1013 pasteAperture->SetPadstackMode( PADSTACK::MODE::NORMAL );
1014 pasteAperture->SetNumber( wxEmptyString );
1015 pasteAperture->SetAttribute( PAD_ATTRIB::SMD );
1017 pasteAperture->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 0.93 ),
1018 pcbIUScale.mmToIU( 0.93 ) ) );
1019 pasteAperture->SetPosition( fp->GetPosition() + VECTOR2I( pcbIUScale.mmToIU( 1.15 ),
1020 pcbIUScale.mmToIU( 1.15 ) ) );
1021 pasteAperture->SetLayerSet( LSET( { F_Paste } ) );
1022 fp->Add( pasteAperture );
1023
1024 // Add a control pad whose paste IS authored (F.Cu + F.Mask + F.Paste). It must still
1025 // appear on the paste layer, confirming the fix doesn't suppress legitimate paste pads.
1026 FOOTPRINT* fp2 = new FOOTPRINT( &board );
1027 fp2->SetReference( wxT( "R1" ) );
1028 fp2->SetPosition( VECTOR2I( pcbIUScale.mmToIU( 60 ), pcbIUScale.mmToIU( 60 ) ) );
1029 board.Add( fp2 );
1030
1031 PAD* normalSmd = new PAD( fp2 );
1033 normalSmd->SetNumber( wxT( "1" ) );
1034 normalSmd->SetAttribute( PAD_ATTRIB::SMD );
1036 normalSmd->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 1.0 ), pcbIUScale.mmToIU( 1.0 ) ) );
1037 normalSmd->SetLayerSet( LSET( { F_Cu, F_Mask, F_Paste } ) );
1038 fp2->Add( normalSmd );
1039
1040 // Implicit-mask control pad: F.Cu only. Mask must be added implicitly by the exporter,
1041 // matching the #16658 fix. This guards against accidental removal of the mask code path.
1042 PAD* implicitMaskSmd = new PAD( fp2 );
1043 implicitMaskSmd->SetPadstackMode( PADSTACK::MODE::NORMAL );
1044 implicitMaskSmd->SetNumber( wxT( "2" ) );
1045 implicitMaskSmd->SetAttribute( PAD_ATTRIB::SMD );
1047 implicitMaskSmd->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 1.0 ), pcbIUScale.mmToIU( 1.0 ) ) );
1048 implicitMaskSmd->SetPosition( fp2->GetPosition() + VECTOR2I( pcbIUScale.mmToIU( 2.0 ), 0 ) );
1049 implicitMaskSmd->SetLayerSet( LSET( { F_Cu } ) );
1050 fp2->Add( implicitMaskSmd );
1051
1052 wxString tempPath = CreateTempFile();
1053 std::map<std::string, UTF8> props;
1054 props["units"] = "mm";
1055 props["version"] = "C";
1056 props["sigfig"] = "4";
1057
1058 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, board, &props ) );
1059 BOOST_REQUIRE( wxFileExists( tempPath ) );
1060
1061 std::ifstream xmlFile( tempPath.ToStdString() );
1062 BOOST_REQUIRE( xmlFile.is_open() );
1063
1064 std::string xml( ( std::istreambuf_iterator<char>( xmlFile ) ),
1065 std::istreambuf_iterator<char>() );
1066
1067 // Locate the F.Paste LayerFeature block, if any.
1068 const std::string pasteOpen = "<LayerFeature layerRef=\"F.Paste\"";
1069 size_t pasteStart = xml.find( pasteOpen );
1070
1071 if( pasteStart != std::string::npos )
1072 {
1073 size_t pasteEnd = xml.find( "</LayerFeature>", pasteStart );
1074 BOOST_REQUIRE( pasteEnd != std::string::npos );
1075
1076 std::string pasteRegion = xml.substr( pasteStart, pasteEnd - pasteStart );
1077
1078 // The exposed thermal pad (U1 pin 33) must NOT appear on F.Paste.
1079 BOOST_CHECK_MESSAGE(
1080 pasteRegion.find( "<PinRef componentRef=\"U1\" pin=\"33\"" ) == std::string::npos,
1081 "Copper-only thermal pad U1.33 must not appear on F.Paste layer feature" );
1082
1083 // The normal SMD pad (R1 pin 1) SHOULD still appear on F.Paste.
1084 BOOST_CHECK_MESSAGE(
1085 pasteRegion.find( "<PinRef componentRef=\"R1\" pin=\"1\"" ) != std::string::npos,
1086 "Normal SMD pad with explicit F.Paste in layer set should still emit a paste "
1087 "feature" );
1088
1089 // The implicit-mask control pad (R1 pin 2) had F.Cu only and must NOT have paste.
1090 BOOST_CHECK_MESSAGE(
1091 pasteRegion.find( "<PinRef componentRef=\"R1\" pin=\"2\"" ) == std::string::npos,
1092 "Copper-only SMD pad R1.2 must not appear on F.Paste layer feature" );
1093 }
1094 else
1095 {
1096 // If no F.Paste layer feature was emitted at all the regression would be hidden, so
1097 // require its presence (R1 must drive its creation).
1098 BOOST_FAIL( "Expected an F.Paste LayerFeature for the explicitly-pasted control pad" );
1099 }
1100
1101 // Mask must still be added implicitly for the thermal pad. Confirm an F.Mask
1102 // LayerFeature exists with U1 pin 33 so the #16658 behavior is preserved for mask.
1103 const std::string maskOpen = "<LayerFeature layerRef=\"F.Mask\"";
1104 size_t maskStart = xml.find( maskOpen );
1105 BOOST_REQUIRE_MESSAGE( maskStart != std::string::npos,
1106 "F.Mask LayerFeature should still be emitted for SMD copper pads" );
1107
1108 size_t maskEnd = xml.find( "</LayerFeature>", maskStart );
1109 BOOST_REQUIRE( maskEnd != std::string::npos );
1110
1111 std::string maskRegion = xml.substr( maskStart, maskEnd - maskStart );
1112
1113 BOOST_CHECK_MESSAGE(
1114 maskRegion.find( "<PinRef componentRef=\"U1\" pin=\"33\"" ) != std::string::npos,
1115 "Thermal pad with explicit F.Mask should appear on F.Mask layer feature" );
1116
1117 // The truly implicit case: R1 pin 2 had F.Cu only and must still acquire an F.Mask
1118 // entry. This is the actual regression guard for the #16658 implicit-mask behavior.
1119 BOOST_CHECK_MESSAGE(
1120 maskRegion.find( "<PinRef componentRef=\"R1\" pin=\"2\"" ) != std::string::npos,
1121 "SMD copper pad without explicit F.Mask must get an implicit F.Mask opening" );
1122}
1123
1124
1129static std::string LayerFeatureRegion( const std::string& aXml, const std::string& aLayerRef )
1130{
1131 const std::string open = "<LayerFeature layerRef=\"" + aLayerRef + "\"";
1132 size_t start = aXml.find( open );
1133
1134 if( start == std::string::npos )
1135 return std::string();
1136
1137 size_t end = aXml.find( "</LayerFeature>", start );
1138
1139 if( end == std::string::npos )
1140 return std::string();
1141
1142 return aXml.substr( start, end - start );
1143}
1144
1145
1153BOOST_AUTO_TEST_CASE( GrRectCornerRadius_Issue24754 )
1154{
1155 BOARD board;
1156 board.SetCopperLayerCount( 2 );
1157
1158 PCB_SHAPE* rect = new PCB_SHAPE( &board, SHAPE_T::RECTANGLE );
1159 rect->SetLayer( Edge_Cuts );
1160 rect->SetStart( VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 10 ) ) );
1161 rect->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 11.5 ), pcbIUScale.mmToIU( 17 ) ) );
1162 rect->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.05 ), LINE_STYLE::SOLID ) );
1163 rect->SetFilled( false );
1164 rect->SetCornerRadius( pcbIUScale.mmToIU( 0.75 ) );
1165 board.Add( rect );
1166
1167 wxString tempPath = CreateTempFile();
1168 std::map<std::string, UTF8> props;
1169 props["units"] = "mm";
1170 props["version"] = "C";
1171 props["sigfig"] = "4";
1172
1173 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, board, &props ) );
1174 BOOST_REQUIRE( wxFileExists( tempPath ) );
1175
1176 std::string xml = KI_TEST::LoadStringData( tempPath );
1177
1178 // The RectRound must round its corners and carry the 0.75 mm radius, not stroke_width/2.
1179 size_t rectPos = xml.find( "<RectRound" );
1180 BOOST_REQUIRE_MESSAGE( rectPos != std::string::npos, "Export should contain a RectRound" );
1181
1182 std::string rectNode = xml.substr( rectPos, xml.find( '>', rectPos ) - rectPos );
1183
1184 BOOST_CHECK_MESSAGE( rectNode.find( "radius=\"0.750\"" ) != std::string::npos,
1185 "Rounded gr_rect must export its 0.75 mm corner radius. Node: "
1186 + rectNode );
1187 BOOST_CHECK_MESSAGE( rectNode.find( "upperRight=\"true\"" ) != std::string::npos
1188 && rectNode.find( "lowerLeft=\"true\"" ) != std::string::npos,
1189 "Rounded gr_rect must set its corner flags. Node: " + rectNode );
1190 BOOST_CHECK_MESSAGE( rectNode.find( "radius=\"0.025\"" ) == std::string::npos,
1191 "Corner radius must not collapse to stroke_width/2. Node: " + rectNode );
1192}
1193
1194
1201BOOST_AUTO_TEST_CASE( BackOnlyMaskNoFrontOpening_Issue24753 )
1202{
1203 BOARD board;
1204 board.SetCopperLayerCount( 2 );
1205
1206 FOOTPRINT* fp = new FOOTPRINT( &board );
1207 fp->SetReference( wxT( "J29" ) );
1208 fp->SetPosition( VECTOR2I( pcbIUScale.mmToIU( 50 ), pcbIUScale.mmToIU( 50 ) ) );
1209 board.Add( fp );
1210
1211 // Through-hole pad masked on the back only: *.Cu + B.Mask, deliberately no F.Mask.
1212 PAD* pad = new PAD( fp );
1213 pad->SetPadstackMode( PADSTACK::MODE::NORMAL );
1214 pad->SetNumber( wxT( "1" ) );
1215 pad->SetAttribute( PAD_ATTRIB::PTH );
1217 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 2.5 ), pcbIUScale.mmToIU( 4.0 ) ) );
1218 pad->SetDrillSize( VECTOR2I( pcbIUScale.mmToIU( 1.65 ), pcbIUScale.mmToIU( 1.65 ) ) );
1219 pad->SetLayerSet( LSET( { F_Cu, B_Cu, B_Mask } ) );
1220 fp->Add( pad );
1221
1222 wxString tempPath = CreateTempFile();
1223 std::map<std::string, UTF8> props;
1224 props["units"] = "mm";
1225 props["version"] = "C";
1226 props["sigfig"] = "4";
1227
1228 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, board, &props ) );
1229 BOOST_REQUIRE( wxFileExists( tempPath ) );
1230
1231 std::string xml = KI_TEST::LoadStringData( tempPath );
1232
1233 // The pad must appear on B.Mask (authored) but never on F.Mask.
1234 std::string fMask = LayerFeatureRegion( xml, "F.Mask" );
1235 BOOST_CHECK_MESSAGE( fMask.find( "<PinRef componentRef=\"J29\" pin=\"1\"" ) == std::string::npos,
1236 "Back-only masked pad must not appear on F.Mask layer feature" );
1237
1238 std::string bMask = LayerFeatureRegion( xml, "B.Mask" );
1239 BOOST_CHECK_MESSAGE( bMask.find( "<PinRef componentRef=\"J29\" pin=\"1\"" ) != std::string::npos,
1240 "Back-only masked pad should appear on B.Mask layer feature" );
1241}
1242
1243
1251BOOST_AUTO_TEST_CASE( RoundRectMaskRadius_Issue24751 )
1252{
1253 BOARD board;
1254 board.SetCopperLayerCount( 2 );
1255
1256 FOOTPRINT* fp = new FOOTPRINT( &board );
1257 fp->SetReference( wxT( "R1" ) );
1258 fp->SetPosition( VECTOR2I( pcbIUScale.mmToIU( 40 ), pcbIUScale.mmToIU( 40 ) ) );
1259 board.Add( fp );
1260
1261 PAD* pad = new PAD( fp );
1262 pad->SetPadstackMode( PADSTACK::MODE::NORMAL );
1263 pad->SetNumber( wxT( "1" ) );
1264 pad->SetAttribute( PAD_ATTRIB::SMD );
1266 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 0.45 ), pcbIUScale.mmToIU( 0.30 ) ) );
1267 pad->SetRoundRectRadiusRatio( PADSTACK::ALL_LAYERS, 0.3333333333 );
1268 pad->SetLayerSet( LSET( { F_Cu, F_Mask } ) );
1269 pad->SetLocalSolderMaskMargin( pcbIUScale.mmToIU( -0.05 ) );
1270 fp->Add( pad );
1271
1272 wxString tempPath = CreateTempFile();
1273 std::map<std::string, UTF8> props;
1274 props["units"] = "mm";
1275 props["version"] = "C";
1276 props["sigfig"] = "4";
1277
1278 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, board, &props ) );
1279 BOOST_REQUIRE( wxFileExists( tempPath ) );
1280
1281 std::string xml = KI_TEST::LoadStringData( tempPath );
1282
1283 // Copper roundrect radius is 0.10 mm; with a -0.05 mm per-side margin the mask aperture is
1284 // 0.35 x 0.20 with radius 0.05 mm. Both distinct primitives must be present.
1285 BOOST_CHECK_MESSAGE( xml.find( "radius=\"0.10\"" ) != std::string::npos,
1286 "Copper roundrect should keep its 0.10 mm radius" );
1287 BOOST_CHECK_MESSAGE( xml.find( "radius=\"0.050\"" ) != std::string::npos,
1288 "Mask roundrect aperture must shrink its radius to 0.05 mm" );
1289 BOOST_CHECK_MESSAGE( xml.find( "width=\"0.350\"" ) != std::string::npos,
1290 "Mask roundrect aperture should be 0.35 mm wide" );
1291}
1292
1293
1300BOOST_AUTO_TEST_CASE( MultiLayerFootprintGraphic_Issue24752 )
1301{
1302 BOARD board;
1303 board.SetCopperLayerCount( 2 );
1304
1305 FOOTPRINT* fp = new FOOTPRINT( &board );
1306 fp->SetReference( wxT( "G1" ) );
1307 fp->SetPosition( VECTOR2I( pcbIUScale.mmToIU( 30 ), pcbIUScale.mmToIU( 30 ) ) );
1308 board.Add( fp );
1309
1310 PCB_SHAPE* poly = new PCB_SHAPE( fp, SHAPE_T::POLY );
1311 poly->SetLayerSet( LSET( { F_Cu, F_Mask } ) );
1312 poly->SetFilled( true );
1314
1315 SHAPE_POLY_SET polySet;
1316 polySet.NewOutline();
1317 polySet.Append( pcbIUScale.mmToIU( 30 ), pcbIUScale.mmToIU( 30 ) );
1318 polySet.Append( pcbIUScale.mmToIU( 31 ), pcbIUScale.mmToIU( 30 ) );
1319 polySet.Append( pcbIUScale.mmToIU( 31 ), pcbIUScale.mmToIU( 31 ) );
1320 polySet.Append( pcbIUScale.mmToIU( 30 ), pcbIUScale.mmToIU( 31 ) );
1321 poly->SetPolyShape( polySet );
1322 fp->Add( poly );
1323
1324 wxString tempPath = CreateTempFile();
1325 std::map<std::string, UTF8> props;
1326 props["units"] = "mm";
1327 props["version"] = "C";
1328 props["sigfig"] = "4";
1329
1330 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, board, &props ) );
1331 BOOST_REQUIRE( wxFileExists( tempPath ) );
1332
1333 std::string xml = KI_TEST::LoadStringData( tempPath );
1334
1335 // The polygon graphic must show up under both F.Cu and F.Mask.
1336 std::string fCu = LayerFeatureRegion( xml, "F.Cu" );
1337 std::string fMask = LayerFeatureRegion( xml, "F.Mask" );
1338
1339 BOOST_CHECK_MESSAGE( fCu.find( "UserPrimitiveRef" ) != std::string::npos,
1340 "Footprint graphic should be present on F.Cu" );
1341 BOOST_CHECK_MESSAGE( fMask.find( "UserPrimitiveRef" ) != std::string::npos,
1342 "Multi-layer footprint graphic must also appear on F.Mask" );
1343}
1344
1345
1355BOOST_AUTO_TEST_CASE( SolderMaskMarginExpandsMaskPrimitive_Issue24749 )
1356{
1357 BOARD board;
1358
1359 FOOTPRINT* fp = new FOOTPRINT( &board );
1360 fp->SetReference( wxT( "FID4" ) );
1361 fp->SetPosition( VECTOR2I( pcbIUScale.mmToIU( 50 ), pcbIUScale.mmToIU( 50 ) ) );
1362 board.Add( fp );
1363
1364 PAD* pad = new PAD( fp );
1365 pad->SetPadstackMode( PADSTACK::MODE::NORMAL );
1366 pad->SetNumber( wxT( "1" ) );
1367 pad->SetAttribute( PAD_ATTRIB::SMD );
1369 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 1.0 ), pcbIUScale.mmToIU( 1.0 ) ) );
1370 pad->SetLayerSet( LSET( { F_Cu, F_Mask } ) );
1371 pad->SetLocalSolderMaskMargin( pcbIUScale.mmToIU( 0.5 ) );
1372 fp->Add( pad );
1373
1374 wxString tempPath = CreateTempFile();
1375 std::map<std::string, UTF8> props;
1376 props["units"] = "mm";
1377 props["version"] = "C";
1378 props["sigfig"] = "4";
1379
1380 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, board, &props ) );
1381 BOOST_REQUIRE( wxFileExists( tempPath ) );
1382
1383 std::ifstream xmlFile( tempPath.ToStdString() );
1384 BOOST_REQUIRE( xmlFile.is_open() );
1385
1386 std::string xml( ( std::istreambuf_iterator<char>( xmlFile ) ),
1387 std::istreambuf_iterator<char>() );
1388
1389 // Resolve the primitive id referenced by the F.Mask padstack entry, then confirm
1390 // that primitive's circle diameter is the mask-expanded 2.0 mm, not the 1.0 mm copper size.
1391 const std::string maskRef = "<PadstackPadDef layerRef=\"F.Mask\"";
1392 size_t maskDefPos = xml.find( maskRef );
1393 BOOST_REQUIRE_MESSAGE( maskDefPos != std::string::npos,
1394 "F.Mask padstack pad definition should be present" );
1395
1396 size_t refPos = xml.find( "StandardPrimitiveRef id=\"", maskDefPos );
1397 BOOST_REQUIRE( refPos != std::string::npos );
1398 refPos += std::string( "StandardPrimitiveRef id=\"" ).size();
1399 size_t refEnd = xml.find( '"', refPos );
1400 std::string maskPrimId = xml.substr( refPos, refEnd - refPos );
1401
1402 std::string entry = "<EntryStandard id=\"" + maskPrimId + "\"";
1403 size_t entryPos = xml.find( entry );
1404 BOOST_REQUIRE_MESSAGE( entryPos != std::string::npos,
1405 "Mask primitive EntryStandard should be defined" );
1406
1407 size_t diaPos = xml.find( "diameter=\"", entryPos );
1408 BOOST_REQUIRE( diaPos != std::string::npos );
1409 diaPos += std::string( "diameter=\"" ).size();
1410 size_t diaEnd = xml.find( '"', diaPos );
1411 double diameter = std::stod( xml.substr( diaPos, diaEnd - diaPos ) );
1412
1413 BOOST_CHECK_MESSAGE( std::abs( diameter - 2.0 ) < 1e-4,
1414 "F.Mask primitive diameter should be 2.0 mm (1.0 mm pad + 0.5 mm "
1415 "margin per side), got " << diameter );
1416}
1417
1418
1422BOOST_AUTO_TEST_CASE( SchemaValidation_Issue25149 )
1423{
1424 static const std::vector<std::string> boards = {
1425 "ipc2581/dielectric-sublayer.kicad_pcb",
1426 "ipc2581/edgecuts-circles-only.kicad_pcb",
1427 "ipc2581/filled-silk-circle.kicad_pcb",
1428 "ipc2581/filled-tented-via.kicad_pcb",
1429 "ipc2581/textbox-border.kicad_pcb",
1430 "ipc2581/thirty-copper-layers.kicad_pcb",
1431 "ipc2581/whitespace-text.kicad_pcb",
1432 };
1433
1434 for( const std::string& boardFile : boards )
1435 {
1436 BOOST_TEST_CONTEXT( "Board: " << boardFile )
1437 {
1438 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
1439 BOOST_REQUIRE( board );
1440
1441 wxString errorMsg;
1442 bool valid = ExportAndValidate( *board, 'C', errorMsg );
1443
1444 BOOST_CHECK_MESSAGE( valid, "IPC-2581C validation failed for " + boardFile + ": " + errorMsg );
1445 }
1446 }
1447}
1448
1449
1454BOOST_AUTO_TEST_CASE( NoClosedOutline_Issue25149 )
1455{
1456 std::unique_ptr<BOARD> board = LoadBoard( "ipc2581/edgecuts-circles-only.kicad_pcb" );
1457 BOOST_REQUIRE( board );
1458
1459 wxString tempPath = CreateTempFile();
1460 std::map<std::string, UTF8> props;
1461 props["units"] = "mm";
1462 props["version"] = "C";
1463 props["sigfig"] = "3";
1464
1465 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, *board, &props ) );
1466 BOOST_REQUIRE( wxFileExists( tempPath ) );
1467
1468 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "<Profile" ) ),
1469 "A board with no closed outline must not write a Profile" );
1470}
1471
1472
1476BOOST_AUTO_TEST_CASE( ProcessLayerViaPads_Issue25149 )
1477{
1478 std::unique_ptr<BOARD> board = LoadBoard( "ipc2581/filled-tented-via.kicad_pcb" );
1479 BOOST_REQUIRE( board );
1480
1481 wxString tempPath = CreateTempFile();
1482 std::map<std::string, UTF8> props;
1483 props["units"] = "mm";
1484 props["version"] = "C";
1485 props["sigfig"] = "3";
1486
1487 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, *board, &props ) );
1488 BOOST_REQUIRE( wxFileExists( tempPath ) );
1489
1490 std::string xml = KI_TEST::LoadStringData( tempPath );
1491
1492 std::string region = LayerFeatureRegion( xml, "F.Cu_2" );
1493 BOOST_REQUIRE_MESSAGE( !region.empty(), "Export should contain the F.Cu_2 process layer" );
1494
1495 BOOST_CHECK_MESSAGE( region.find( "<Set" ) != std::string::npos,
1496 "Process-layer via pads must be wrapped in a Set" );
1497 BOOST_CHECK_MESSAGE( region.find( "x=\"120.0\" y=\"-115.0\"" ) != std::string::npos,
1498 "Process-layer via pad must be at the via position" );
1499 BOOST_CHECK_MESSAGE( region.find( "x=\"0.0\" y=\"0.0\"" ) == std::string::npos,
1500 "Process-layer via pad must not be at (0,0)" );
1501}
1502
1503
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:121
General utilities for PCB file IO for QA programs.
virtual void SetNet(NETINFO_ITEM *aNetInfo)
Set a NET_INFO object for the item.
void SetLayer(PCB_LAYER_ID aLayer) override
Set the layer this item is on.
Container for design settings for a BOARD object.
BOARD_STACKUP & GetStackupDescriptor()
Manage layers needed to make a physical board.
void BuildDefaultStackupList(const BOARD_DESIGN_SETTINGS *aSettings, int aActiveCopperLayersCount=0)
Create a default stackup, according to the current BOARD_DESIGN_SETTINGS settings.
wxString m_FinishType
The name of external copper finish.
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:409
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Removes an item from the container.
Definition board.cpp:1497
void SetCopperLayerCount(int aCount)
Definition board.cpp:1137
BOARD_DESIGN_SETTINGS & GetDesignSettings() const
Definition board.cpp:1299
void SetCornerRadius(int aRadius)
virtual void SetFilled(bool aFlag)
Definition eda_shape.h:142
void SetVertJustify(GR_TEXT_V_ALIGN_T aType)
Definition eda_text.cpp:373
virtual void SetText(const wxString &aText)
Definition eda_text.cpp:231
void SetHorizJustify(GR_TEXT_H_ALIGN_T aType)
Definition eda_text.cpp:365
void SetPosition(const VECTOR2I &aPos) override
void SetReference(const wxString &aReference)
Definition footprint.h:907
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Removes an item from the container.
VECTOR2I GetPosition() const override
Definition footprint.h:435
LSET is a set of PCB_LAYER_IDs.
Definition lset.h:37
Handle the data for a net.
Definition netinfo.h:50
@ NORMAL
Shape is the same on all layers.
Definition padstack.h:170
static constexpr PCB_LAYER_ID ALL_LAYERS
! The layer identifier to use for the single defintion on normal padstacks
Definition padstack.h:179
Definition pad.h:61
void SetAttribute(PAD_ATTRIB aAttribute)
Definition pad.cpp:1639
void SetPadstackMode(PADSTACK::MODE aMode)
Definition pad.h:190
void SetShape(PCB_LAYER_ID aLayer, PAD_SHAPE aShape)
Set the new shape of this pad.
Definition pad.h:196
void SetProperty(PAD_PROP aProperty)
Definition pad.cpp:1712
void SetNumber(const wxString &aNumber)
Set the pad number (note that it can be alphanumeric, such as the array reference "AA12").
Definition pad.h:142
void SetPosition(const VECTOR2I &aPos) override
Definition pad.cpp:235
void SetSize(PCB_LAYER_ID aLayer, const VECTOR2I &aSize)
Definition pad.cpp:255
void SetLayerSet(const LSET &aLayers) override
Definition pad.cpp:1955
void SetMid(const VECTOR2I &aMid)
Definition pcb_track.h:286
A #PLUGIN derivation for saving and loading Pcbnew s-expression formatted files.
virtual void SetLayerSet(const LSET &aLayers) override
void SetEnd(const VECTOR2I &aEnd) override
void SetPolyShape(const SHAPE_POLY_SET &aShape) override
void SetLayer(PCB_LAYER_ID aLayer) override
Set the layer this item is on.
void SetStart(const VECTOR2I &aStart) override
void SetStroke(const STROKE_PARAMS &aStroke) override
void SetBorderEnabled(bool enabled)
void SetMarginTop(int aTop)
void SetMarginLeft(int aLeft)
void SetTextThickness(int aWidth) override
The TextThickness is that set by the user.
void SetTextSize(VECTOR2I aNewSize, bool aEnforceMinTextSize=true) override
void SetEnd(const VECTOR2I &aEnd)
Definition pcb_track.h:89
void SetStart(const VECTOR2I &aStart)
Definition pcb_track.h:92
virtual void SetWidth(int aWidth)
Definition pcb_track.h:86
Represent a set of closed polygons.
int Append(int x, int y, int aOutline=-1, int aHole=-1, bool aAllowDuplication=false)
Appends a vertex at the end of the given outline/hole (default: the last outline)
int NewOutline()
Creates a new empty polygon in the set and returns its index.
Simple container to manage line stroke parameters.
@ RECTANGLE
Use RECTANGLE instead of RECT to avoid collision in a Windows header.
Definition eda_shape.h:57
@ Edge_Cuts
Definition layer_ids.h:108
@ F_Paste
Definition layer_ids.h:100
@ B_Mask
Definition layer_ids.h:94
@ B_Cu
Definition layer_ids.h:61
@ F_Mask
Definition layer_ids.h:93
@ F_SilkS
Definition layer_ids.h:96
@ In3_Cu
Definition layer_ids.h:64
@ F_Cu
Definition layer_ids.h:60
std::string GetPcbnewTestDataDir()
Utility which returns a path to the data directory where the test board files are stored.
std::string LoadStringData(const wxString &aPath)
Load the contents of a file into a string.
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:411
@ SMD
Smd pad, appears on the solder paste layer (default)
Definition padstack.h:98
@ PTH
Plated through hole pad.
Definition padstack.h:97
@ ROUNDRECT
Definition padstack.h:56
@ RECTANGLE
Definition padstack.h:53
@ HEATSINK
a pad used as heat sink, usually in SMD footprints
Definition padstack.h:119
wxString GetXsdPath(char aVersion)
bool ExportAndValidate(BOARD &aBoard, char aVersion, wxString &aErrorMsg, const std::string &aMode=std::string(), const std::string &aRefDes=std::string(), const std::string &aSections=std::string())
std::vector< wxString > m_tempFiles
PCB_IO_KICAD_SEXPR m_kicadPlugin
wxString CreateTempFile(const wxString &aSuffix=wxT(""))
std::unique_ptr< BOARD > LoadBoard(const std::string &aRelativePath)
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
BOOST_AUTO_TEST_CASE(SurfaceFinishExport)
Test that surface finish is exported correctly (Issue #22690)
static std::string LayerFeatureRegion(const std::string &aXml, const std::string &aLayerRef)
Extract the text of the first LayerFeature block for a given layer reference.
std::string path
VECTOR2I end
BOOST_TEST_CONTEXT("Test Clearance")
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")
@ GR_TEXT_H_ALIGN_LEFT
@ GR_TEXT_V_ALIGN_TOP
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683