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
32
35
36#include <board.h>
39#include <footprint.h>
40#include <pad.h>
41#include <pcb_track.h>
42#include <base_units.h>
43
44#include <wx/dir.h>
45#include <wx/file.h>
46#include <wx/filename.h>
47#include <wx/process.h>
48#include <wx/txtstrm.h>
49
50#include <fstream>
51#include <sstream>
52
53
54namespace
55{
56
60bool IsXmllintAvailable()
61{
62 wxArrayString output;
63 wxArrayString errors;
64 int result = wxExecute( "xmllint --version", output, errors, wxEXEC_SYNC );
65 return result == 0;
66}
67
68
73wxString ValidateXmlWithXsd( const wxString& aXmlPath, const wxString& aXsdPath )
74{
75 wxString cmd = wxString::Format( "xmllint --noout --schema \"%s\" \"%s\"",
76 aXsdPath, aXmlPath );
77
78 wxArrayString output;
79 wxArrayString errors;
80 int result = wxExecute( cmd, output, errors, wxEXEC_SYNC );
81
82 if( result != 0 )
83 {
84 wxString errorMsg;
85
86 for( const wxString& line : errors )
87 errorMsg += line + "\n";
88
89 return errorMsg;
90 }
91
92 return wxEmptyString;
93}
94
95
99bool FileContainsPattern( const wxString& aFilePath, const wxString& aPattern )
100{
101 std::ifstream file( aFilePath.ToStdString() );
102
103 if( !file.is_open() )
104 return false;
105
106 std::stringstream buffer;
107 buffer << file.rdbuf();
108 std::string content = buffer.str();
109
110 return content.find( aPattern.ToStdString() ) != std::string::npos;
111}
112
113
118static const std::vector<std::string> VALIDATION_TEST_BOARDS = {
119 "custom_pads.kicad_pcb",
120 "notched_zones.kicad_pcb",
121 "sliver.kicad_pcb",
122 "tracks_arcs_vias.kicad_pcb",
123 "issue7241.kicad_pcb",
124 "issue10906.kicad_pcb",
125 "issue22798.kicad_pcb",
126 "padstacks_complex.kicad_pcb",
127 "issue12609.kicad_pcb",
128 "issue22794.kicad_pcb",
129};
130
131} // anonymous namespace
132
133
135{
137 m_xmllintAvailable( IsXmllintAvailable() )
138 {
139 }
140
142 {
143 // Clean up temporary files
144 for( const wxString& path : m_tempFiles )
145 {
146 if( wxFileExists( path ) )
147 wxRemoveFile( path );
148 }
149 }
150
151 wxString CreateTempFile( const wxString& aSuffix = wxT( "" ) )
152 {
153 wxString path = wxFileName::CreateTempFileName( wxT( "kicad_ipc2581_test" ) );
154
155 if( !aSuffix.IsEmpty() )
156 path += aSuffix;
157 else
158 path += wxT( ".xml" );
159
160 m_tempFiles.push_back( path );
161 return path;
162 }
163
164 wxString GetXsdPath( char aVersion )
165 {
166 wxString filename = ( aVersion == 'C' ) ? wxT( "IPC-2581C.xsd" ) : wxT( "IPC-2581B1.xsd" );
167 return KI_TEST::GetPcbnewTestDataDir() + "ipc2581/" + filename;
168 }
169
170 std::unique_ptr<BOARD> LoadBoard( const std::string& aRelativePath )
171 {
172 std::string fullPath = KI_TEST::GetPcbnewTestDataDir() + aRelativePath;
173 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
174
175 m_kicadPlugin.LoadBoard( fullPath, board.get(), nullptr, nullptr );
176
177 return board;
178 }
179
180 bool ExportAndValidate( BOARD* aBoard, char aVersion, wxString& aErrorMsg )
181 {
182 wxString tempPath = CreateTempFile();
183
184 std::map<std::string, UTF8> props;
185 props["units"] = "mm";
186 props["version"] = std::string( 1, aVersion );
187 props["sigfig"] = "3";
188
189 try
190 {
191 m_ipc2581Plugin.SaveBoard( tempPath, aBoard, &props );
192 }
193 catch( const std::exception& e )
194 {
195 aErrorMsg = wxString::Format( "Export failed: %s", e.what() );
196 return false;
197 }
198
199 if( !wxFileExists( tempPath ) )
200 {
201 aErrorMsg = "Export file was not created";
202 return false;
203 }
204
206 {
207 wxString xsdPath = GetXsdPath( aVersion );
208
209 if( wxFileExists( xsdPath ) )
210 {
211 aErrorMsg = ValidateXmlWithXsd( tempPath, xsdPath );
212 return aErrorMsg.IsEmpty();
213 }
214 }
215
216 // If xmllint not available, just check that export succeeded
217 return true;
218 }
219
221 std::vector<wxString> m_tempFiles;
224};
225
226
227BOOST_FIXTURE_TEST_SUITE( Ipc2581Export, IPC2581_EXPORT_FIXTURE )
228
229
230
238BOOST_AUTO_TEST_CASE( SurfaceFinishExport )
239{
240 // Load a board with ENIG surface finish (issue3812.kicad_pcb has ENIG)
241 std::unique_ptr<BOARD> board = LoadBoard( "issue3812.kicad_pcb" );
242
243 BOOST_REQUIRE( board );
244
245 // Verify the board has ENIG finish
246 const BOARD_STACKUP& stackup = board->GetDesignSettings().GetStackupDescriptor();
247 BOOST_CHECK_EQUAL( stackup.m_FinishType, wxT( "ENIG" ) );
248
249 // Export to IPC-2581 version C
250 wxString tempPath = CreateTempFile();
251
252 std::map<std::string, UTF8> props;
253 props["units"] = "mm";
254 props["version"] = "C";
255 props["sigfig"] = "3";
256
257 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
258
259 BOOST_REQUIRE( wxFileExists( tempPath ) );
260
261 // Verify SurfaceFinish element is present with correct type attribute
262 // Schema requires: <SurfaceFinish type="ENIG-N"/>
263 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "<SurfaceFinish" ) ),
264 "SurfaceFinish element should be present" );
265 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "type=\"ENIG-N\"" ) ),
266 "SurfaceFinish type should be ENIG-N" );
267
268 // Verify coating layers are present
269 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "COATING_TOP" ) ),
270 "COATING_TOP layer should be present" );
271 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "COATING_BOTTOM" ) ),
272 "COATING_BOTTOM layer should be present" );
273 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "layerFunction=\"COATINGCOND\"" ) ),
274 "Coating layers should have layerFunction=COATINGCOND" );
275
276 // Note: XSD validation is done separately in SchemaValidation tests.
277 // This test focuses on verifying the surface finish elements are present.
278}
279
280
284BOOST_AUTO_TEST_CASE( NoSurfaceFinishExport )
285{
286 // Load a board without surface finish (vme-wren.kicad_pcb has "None")
287 std::unique_ptr<BOARD> board = LoadBoard( "vme-wren.kicad_pcb" );
288
289 BOOST_REQUIRE( board );
290
291 // Verify the board has no finish
292 const BOARD_STACKUP& stackup = board->GetDesignSettings().GetStackupDescriptor();
293 BOOST_CHECK( stackup.m_FinishType == wxT( "None" ) || stackup.m_FinishType.IsEmpty() );
294
295 // Export to IPC-2581 version C
296 wxString tempPath = CreateTempFile();
297
298 std::map<std::string, UTF8> props;
299 props["units"] = "mm";
300 props["version"] = "C";
301 props["sigfig"] = "3";
302
303 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
304
305 BOOST_REQUIRE( wxFileExists( tempPath ) );
306
307 // Verify SurfaceFinish element is NOT present
308 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "<SurfaceFinish" ) ),
309 "SurfaceFinish element should not be present for 'None' finish" );
310
311 // Verify coating layers are NOT present
312 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "COATING_TOP" ) ),
313 "COATING_TOP layer should not be present" );
314 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "COATING_BOTTOM" ) ),
315 "COATING_BOTTOM layer should not be present" );
316
317 // Note: XSD validation is done separately in SchemaValidation tests.
318 // This test focuses on verifying coating layers are NOT present for "None" finish.
319}
320
321
328BOOST_AUTO_TEST_CASE( SchemaValidationVersionB )
329{
330 if( !m_xmllintAvailable )
331 {
332 BOOST_WARN_MESSAGE( false, "xmllint not available, skipping schema validation tests" );
333 return;
334 }
335
336 wxString xsdPath = GetXsdPath( 'B' );
337
338 if( !wxFileExists( xsdPath ) )
339 {
340 BOOST_WARN_MESSAGE( false, "IPC-2581B1.xsd not found, skipping schema validation" );
341 return;
342 }
343
344 for( const std::string& boardFile : VALIDATION_TEST_BOARDS )
345 {
346 BOOST_TEST_CONTEXT( "Board: " << boardFile << " (Version B)" )
347 {
348 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
349
350 if( !board )
351 {
352 BOOST_WARN_MESSAGE( false, "Could not load board: " + boardFile );
353 continue;
354 }
355
356 wxString errorMsg;
357 bool valid = ExportAndValidate( board.get(), 'B', errorMsg );
358
359 BOOST_CHECK_MESSAGE( valid, "IPC-2581B validation failed for " + boardFile + ": " + errorMsg );
360 }
361 }
362}
363
364
371BOOST_AUTO_TEST_CASE( SchemaValidationVersionC )
372{
373 if( !m_xmllintAvailable )
374 {
375 BOOST_WARN_MESSAGE( false, "xmllint not available, skipping schema validation tests" );
376 return;
377 }
378
379 wxString xsdPath = GetXsdPath( 'C' );
380
381 if( !wxFileExists( xsdPath ) )
382 {
383 BOOST_WARN_MESSAGE( false, "IPC-2581C.xsd not found, skipping schema validation" );
384 return;
385 }
386
387 for( const std::string& boardFile : VALIDATION_TEST_BOARDS )
388 {
389 BOOST_TEST_CONTEXT( "Board: " << boardFile << " (Version C)" )
390 {
391 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
392
393 if( !board )
394 {
395 BOOST_WARN_MESSAGE( false, "Could not load board: " + boardFile );
396 continue;
397 }
398
399 wxString errorMsg;
400 bool valid = ExportAndValidate( board.get(), 'C', errorMsg );
401
402 BOOST_CHECK_MESSAGE( valid, "IPC-2581C validation failed for " + boardFile + ": " + errorMsg );
403 }
404 }
405}
406
407
414BOOST_AUTO_TEST_CASE( ComplexBoardExport )
415{
416 // Test boards with specific complex features
417 static const std::vector<std::string> complexBoards = {
418 "intersectingzones.kicad_pcb",
419 "custom_pads.kicad_pcb",
420 };
421
422 for( const std::string& boardFile : complexBoards )
423 {
424 BOOST_TEST_CONTEXT( "Complex board: " << boardFile )
425 {
426 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
427
428 if( !board )
429 {
430 BOOST_WARN_MESSAGE( false, "Could not load board: " + boardFile );
431 continue;
432 }
433
434 // Test both versions
435 for( char version : { 'B', 'C' } )
436 {
437 BOOST_TEST_CONTEXT( "Version " << version )
438 {
439 wxString errorMsg;
440 bool valid = ExportAndValidate( board.get(), version, errorMsg );
441
442 BOOST_CHECK_MESSAGE( valid,
443 wxString::Format( "Export/validation failed for %s version %c: %s",
444 boardFile, version, errorMsg ) );
445 }
446 }
447 }
448 }
449}
450
451
459BOOST_AUTO_TEST_CASE( SmdPadSolderMaskExport_Issue16658 )
460{
461 // Load a board with standard SMD components (capacitors using SMD footprints)
462 std::unique_ptr<BOARD> board = LoadBoard( "issue16658/issue16658.kicad_pcb" );
463
464 BOOST_REQUIRE( board );
465
466 // Verify the board has SMD pads with implicit mask openings
467 bool hasSmtPad = false;
468
469 for( FOOTPRINT* fp : board->Footprints() )
470 {
471 for( PAD* pad : fp->Pads() )
472 {
473 if( pad->GetAttribute() == PAD_ATTRIB::SMD )
474 {
475 hasSmtPad = true;
476
477 // Verify pad is on copper but NOT explicitly on mask layer
478 bool isOnCopperOnly = pad->IsOnLayer( F_Cu ) && !pad->IsOnLayer( F_Mask );
479
480 if( isOnCopperOnly )
481 {
482 // This is the condition we're testing
483 break;
484 }
485 }
486 }
487
488 if( hasSmtPad )
489 break;
490 }
491
492 BOOST_REQUIRE_MESSAGE( hasSmtPad, "Test board should have SMD pads" );
493
494 // Export to IPC-2581 version C
495 wxString tempPath = CreateTempFile();
496
497 std::map<std::string, UTF8> props;
498 props["units"] = "mm";
499 props["version"] = "C";
500 props["sigfig"] = "3";
501
502 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
503
504 BOOST_REQUIRE( wxFileExists( tempPath ) );
505
506 // Verify that F_Mask layer features are present in the export
507 // (this was the bug - mask layers were empty for SMD pads)
508 bool hasFMaskLayer = FileContainsPattern( tempPath, wxT( "layerRef=\"F.Mask\"" ) )
509 || FileContainsPattern( tempPath, wxT( "layerRef=\"TSM\"" ) );
510
511 BOOST_CHECK_MESSAGE( hasFMaskLayer,
512 "IPC-2581 export should contain F.Mask layer features for SMD pads" );
513
514 // Also check for LayerFeature element with mask layer reference
515 bool hasLayerFeature = FileContainsPattern( tempPath, wxT( "<LayerFeature" ) );
516 BOOST_CHECK_MESSAGE( hasLayerFeature, "IPC-2581 export should contain LayerFeature elements" );
517}
518
519
527BOOST_AUTO_TEST_CASE( EmptyRefDesProducesValidXml )
528{
529 std::unique_ptr<BOARD> board = LoadBoard( "padstacks_complex.kicad_pcb" );
530 BOOST_REQUIRE( board );
531
532 for( char version : { 'B', 'C' } )
533 {
534 BOOST_TEST_CONTEXT( "Version " << version )
535 {
536 wxString tempPath = CreateTempFile();
537
538 std::map<std::string, UTF8> props;
539 props["units"] = "mm";
540 props["version"] = std::string( 1, version );
541 props["sigfig"] = "3";
542
543 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
544 BOOST_REQUIRE( wxFileExists( tempPath ) );
545
546 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "refDes=\"\"" ) ),
547 "Empty refDes attribute found" );
548 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "<RefDes name=\"\"" ) ),
549 "Empty RefDes/@name attribute found" );
550 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "componentRef=\"\"" ) ),
551 "Empty PinRef/@componentRef attribute found" );
552 }
553
554 m_ipc2581Plugin = PCB_IO_IPC2581();
555 }
556}
557
558
567BOOST_AUTO_TEST_CASE( FlippedComponentRotation )
568{
569 std::unique_ptr<BOARD> board = LoadBoard( "issue12609.kicad_pcb" );
570 BOOST_REQUIRE( board );
571
572 wxString tempPath = CreateTempFile();
573
574 std::map<std::string, UTF8> props;
575 props["units"] = "mm";
576 props["version"] = "C";
577 props["sigfig"] = "3";
578
579 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
580 BOOST_REQUIRE( wxFileExists( tempPath ) );
581
582 // C5 is on B.Cu at 90 degrees. Its Component Xform must be rotation="90.0",
583 // not the inverted "270.0". Check by finding Component refDes="C5" and verifying
584 // its Xform has rotation="90.0".
585 std::ifstream xmlFile( tempPath.ToStdString() );
586 BOOST_REQUIRE( xmlFile.is_open() );
587
588 std::string xmlContent( ( std::istreambuf_iterator<char>( xmlFile ) ),
589 std::istreambuf_iterator<char>() );
590
591 // Find the C5 component and check its rotation
592 size_t c5Pos = xmlContent.find( "refDes=\"C5\"" );
593
594 if( c5Pos == std::string::npos )
595 c5Pos = xmlContent.find( "refDes=\"NOREF_" );
596
597 BOOST_REQUIRE_MESSAGE( c5Pos != std::string::npos,
598 "C5 component should exist in export" );
599
600 // Look for the Xform within the next 200 chars after refDes="C5"
601 std::string c5Region = xmlContent.substr( c5Pos, 200 );
602 BOOST_CHECK_MESSAGE( c5Region.find( "rotation=\"90.0\"" ) != std::string::npos
603 || c5Region.find( "rotation=\"90.00\"" ) != std::string::npos,
604 "C5 component rotation should be 90, not inverted. Region: "
605 + c5Region );
606}
607
608
612BOOST_AUTO_TEST_CASE( ContentBomRef )
613{
614 std::unique_ptr<BOARD> board = LoadBoard( "issue12609.kicad_pcb" );
615 BOOST_REQUIRE( board );
616
617 wxString tempPath = CreateTempFile();
618
619 std::map<std::string, UTF8> props;
620 props["units"] = "mm";
621 props["version"] = "C";
622 props["sigfig"] = "3";
623
624 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
625 BOOST_REQUIRE( wxFileExists( tempPath ) );
626
627 bool hasBom = FileContainsPattern( tempPath, wxT( "<Bom " ) );
628 bool hasBomRef = FileContainsPattern( tempPath, wxT( "<BomRef " ) );
629
630 if( hasBom )
631 {
632 BOOST_CHECK_MESSAGE( hasBomRef,
633 "Content should have BomRef when Bom section is present" );
634 }
635}
636
637
651BOOST_AUTO_TEST_CASE( KnockoutTextMultiContour_Issue23968 )
652{
653 std::unique_ptr<BOARD> board = LoadBoard( "test_copper_graphics.kicad_pcb" );
654 BOOST_REQUIRE( board );
655
656 for( char version : { 'B', 'C' } )
657 {
658 BOOST_TEST_CONTEXT( "Version " << version )
659 {
660 wxString errorMsg;
661 bool valid = ExportAndValidate( board.get(), version, errorMsg );
662
663 BOOST_CHECK_MESSAGE( valid,
664 wxString::Format( "Knockout text export should be schema-valid "
665 "(version %c): %s", version, errorMsg ) );
666 }
667
668 m_ipc2581Plugin = PCB_IO_IPC2581();
669 }
670}
671
672
684BOOST_AUTO_TEST_CASE( BackdrillSpecEncoding )
685{
686 // Build a minimal 6-layer synthetic board so we can place a via whose
687 // backdrill targets a specific must-cut layer.
688 BOARD board;
689 board.SetCopperLayerCount( 6 );
690
693
694 // Front-side backdrill: drill from F_Cu, must cut through In3_Cu. The
695 // must-not-cut layer should therefore resolve to In4_Cu (the next signal
696 // layer past must-cut going inward from the start surface).
697 auto* via = new PCB_VIA( &board );
698 via->SetPosition( VECTOR2I( pcbIUScale.mmToIU( 5 ), pcbIUScale.mmToIU( 5 ) ) );
699 via->SetLayerPair( F_Cu, B_Cu );
700 via->SetDrill( pcbIUScale.mmToIU( 0.30 ) );
701 via->SetWidth( pcbIUScale.mmToIU( 0.60 ) );
702 via->SetSecondaryDrillSize( pcbIUScale.mmToIU( 0.40 ) );
703 via->SetSecondaryDrillStartLayer( F_Cu );
704 via->SetSecondaryDrillEndLayer( In3_Cu );
705 via->SetFrontPostMachiningMode( PAD_DRILL_POST_MACHINING_MODE::COUNTERSINK );
706 board.Add( via );
707
708 wxString tempPath = CreateTempFile();
709 std::map<std::string, UTF8> props;
710 props["units"] = "mm";
711 props["version"] = "C";
712 props["sigfig"] = "4";
713
714 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, &board, &props ) );
715 BOOST_REQUIRE( wxFileExists( tempPath ) );
716
718 FileContainsPattern( tempPath, wxT( "<Backdrill type=\"START_LAYER\"" ) ),
719 "Backdrill spec should declare a START_LAYER child" );
721 FileContainsPattern( tempPath, wxT( "<Backdrill type=\"MUST_NOT_CUT_LAYER\"" ) ),
722 "Backdrill spec should declare a MUST_NOT_CUT_LAYER child" );
724 FileContainsPattern( tempPath, wxT( "<Backdrill type=\"MAX_STUB_LENGTH\"" ) ),
725 "Backdrill spec should declare a MAX_STUB_LENGTH child" );
726
727 // Schema requires layer references via Property layerOrGroupRef, not as
728 // Backdrill attributes.
729 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "layerOrGroupRef=" ) ),
730 "Backdrill must convey layers through Property layerOrGroupRef" );
731 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "startLayerRef=" ) ),
732 "Backdrill should not use schema-invalid startLayerRef attribute" );
733 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "mustNotCutLayerRef=" ) ),
734 "Backdrill should not use schema-invalid mustNotCutLayerRef attribute" );
735 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "maxStubLength=" ) ),
736 "Backdrill should not use schema-invalid maxStubLength attribute" );
737 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "postMachining=" ) ),
738 "Backdrill should not use the non-standard postMachining attribute" );
739
740 // The must-not-cut layer for a backdrill from F_Cu through In3_Cu in a
741 // 6-layer stack must resolve to In4_Cu, not the must-cut layer itself.
743 FileContainsPattern( tempPath, wxT( "layerOrGroupRef=\"In4.Cu\"" ) ),
744 "Front backdrill must-not-cut layer should be In4.Cu" );
745
746 // The third (primary) backdrill spec slot has been removed; through-drills
747 // must not be exported as backdrill specs.
748 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "BD_1C" ) ),
749 "Exporter should not emit a primary backdrill spec slot" );
750
751 // Counterbore/countersink encoded as a Backdrill type=OTHER child with
752 // a comment, never as a non-standard postMachining attribute.
754 FileContainsPattern( tempPath, wxT( "<Backdrill type=\"OTHER\"" ) ),
755 "Post-machining hint should produce a Backdrill type=OTHER child" );
757 FileContainsPattern( tempPath, wxT( "comment=\"post-machining=COUNTERSINK\"" ) ),
758 "OTHER Backdrill should carry the post-machining comment" );
759}
760
761
773BOOST_AUTO_TEST_CASE( ExposedPadPasteRespected_Issue24318 )
774{
775 BOARD board;
776
777 FOOTPRINT* fp = new FOOTPRINT( &board );
778 fp->SetReference( wxT( "U1" ) );
779 fp->SetPosition( VECTOR2I( pcbIUScale.mmToIU( 50 ), pcbIUScale.mmToIU( 50 ) ) );
780 board.Add( fp );
781
782 // Copper-only thermal pad: F.Cu + F.Mask, deliberately NOT on F.Paste.
783 PAD* thermalPad = new PAD( fp );
784 thermalPad->SetNumber( wxT( "33" ) );
785 thermalPad->SetAttribute( PAD_ATTRIB::SMD );
786 thermalPad->SetProperty( PAD_PROP::HEATSINK );
788 thermalPad->SetSize( PADSTACK::ALL_LAYERS,
789 VECTOR2I( pcbIUScale.mmToIU( 3.45 ), pcbIUScale.mmToIU( 3.45 ) ) );
790 thermalPad->SetLayerSet( LSET( { F_Cu, F_Mask } ) );
791 fp->Add( thermalPad );
792
793 // Paste-only aperture pad, models a stencil opening for the thermal pad.
794 PAD* pasteAperture = new PAD( fp );
795 pasteAperture->SetNumber( wxEmptyString );
796 pasteAperture->SetAttribute( PAD_ATTRIB::SMD );
798 pasteAperture->SetSize( PADSTACK::ALL_LAYERS,
799 VECTOR2I( pcbIUScale.mmToIU( 0.93 ),
800 pcbIUScale.mmToIU( 0.93 ) ) );
801 pasteAperture->SetPosition( fp->GetPosition()
802 + VECTOR2I( pcbIUScale.mmToIU( 1.15 ),
803 pcbIUScale.mmToIU( 1.15 ) ) );
804 pasteAperture->SetLayerSet( LSET( { F_Paste } ) );
805 fp->Add( pasteAperture );
806
807 // Add a control pad whose paste IS authored (F.Cu + F.Mask + F.Paste). It must still
808 // appear on the paste layer, confirming the fix doesn't suppress legitimate paste pads.
809 FOOTPRINT* fp2 = new FOOTPRINT( &board );
810 fp2->SetReference( wxT( "R1" ) );
811 fp2->SetPosition( VECTOR2I( pcbIUScale.mmToIU( 60 ), pcbIUScale.mmToIU( 60 ) ) );
812 board.Add( fp2 );
813
814 PAD* normalSmd = new PAD( fp2 );
815 normalSmd->SetNumber( wxT( "1" ) );
816 normalSmd->SetAttribute( PAD_ATTRIB::SMD );
818 normalSmd->SetSize( PADSTACK::ALL_LAYERS,
819 VECTOR2I( pcbIUScale.mmToIU( 1.0 ), pcbIUScale.mmToIU( 1.0 ) ) );
820 normalSmd->SetLayerSet( LSET( { F_Cu, F_Mask, F_Paste } ) );
821 fp2->Add( normalSmd );
822
823 // Implicit-mask control pad: F.Cu only. Mask must be added implicitly by the exporter,
824 // matching the #16658 fix. This guards against accidental removal of the mask code path.
825 PAD* implicitMaskSmd = new PAD( fp2 );
826 implicitMaskSmd->SetNumber( wxT( "2" ) );
827 implicitMaskSmd->SetAttribute( PAD_ATTRIB::SMD );
829 implicitMaskSmd->SetSize( PADSTACK::ALL_LAYERS,
830 VECTOR2I( pcbIUScale.mmToIU( 1.0 ), pcbIUScale.mmToIU( 1.0 ) ) );
831 implicitMaskSmd->SetPosition( fp2->GetPosition()
832 + VECTOR2I( pcbIUScale.mmToIU( 2.0 ), 0 ) );
833 implicitMaskSmd->SetLayerSet( LSET( { F_Cu } ) );
834 fp2->Add( implicitMaskSmd );
835
836 wxString tempPath = CreateTempFile();
837 std::map<std::string, UTF8> props;
838 props["units"] = "mm";
839 props["version"] = "C";
840 props["sigfig"] = "4";
841
842 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, &board, &props ) );
843 BOOST_REQUIRE( wxFileExists( tempPath ) );
844
845 std::ifstream xmlFile( tempPath.ToStdString() );
846 BOOST_REQUIRE( xmlFile.is_open() );
847
848 std::string xml( ( std::istreambuf_iterator<char>( xmlFile ) ),
849 std::istreambuf_iterator<char>() );
850
851 // Locate the F.Paste LayerFeature block, if any.
852 const std::string pasteOpen = "<LayerFeature layerRef=\"F.Paste\"";
853 size_t pasteStart = xml.find( pasteOpen );
854
855 if( pasteStart != std::string::npos )
856 {
857 size_t pasteEnd = xml.find( "</LayerFeature>", pasteStart );
858 BOOST_REQUIRE( pasteEnd != std::string::npos );
859
860 std::string pasteRegion = xml.substr( pasteStart, pasteEnd - pasteStart );
861
862 // The exposed thermal pad (U1 pin 33) must NOT appear on F.Paste.
864 pasteRegion.find( "<PinRef componentRef=\"U1\" pin=\"33\"" ) == std::string::npos,
865 "Copper-only thermal pad U1.33 must not appear on F.Paste layer feature" );
866
867 // The normal SMD pad (R1 pin 1) SHOULD still appear on F.Paste.
869 pasteRegion.find( "<PinRef componentRef=\"R1\" pin=\"1\"" ) != std::string::npos,
870 "Normal SMD pad with explicit F.Paste in layer set should still emit a paste "
871 "feature" );
872
873 // The implicit-mask control pad (R1 pin 2) had F.Cu only and must NOT have paste.
875 pasteRegion.find( "<PinRef componentRef=\"R1\" pin=\"2\"" ) == std::string::npos,
876 "Copper-only SMD pad R1.2 must not appear on F.Paste layer feature" );
877 }
878 else
879 {
880 // If no F.Paste layer feature was emitted at all the regression would be hidden, so
881 // require its presence (R1 must drive its creation).
882 BOOST_FAIL( "Expected an F.Paste LayerFeature for the explicitly-pasted control pad" );
883 }
884
885 // Mask must still be added implicitly for the thermal pad. Confirm an F.Mask
886 // LayerFeature exists with U1 pin 33 so the #16658 behavior is preserved for mask.
887 const std::string maskOpen = "<LayerFeature layerRef=\"F.Mask\"";
888 size_t maskStart = xml.find( maskOpen );
889 BOOST_REQUIRE_MESSAGE( maskStart != std::string::npos,
890 "F.Mask LayerFeature should still be emitted for SMD copper pads" );
891
892 size_t maskEnd = xml.find( "</LayerFeature>", maskStart );
893 BOOST_REQUIRE( maskEnd != std::string::npos );
894
895 std::string maskRegion = xml.substr( maskStart, maskEnd - maskStart );
896
898 maskRegion.find( "<PinRef componentRef=\"U1\" pin=\"33\"" ) != std::string::npos,
899 "Thermal pad with explicit F.Mask should appear on F.Mask layer feature" );
900
901 // The truly implicit case: R1 pin 2 had F.Cu only and must still acquire an F.Mask
902 // entry. This is the actual regression guard for the #16658 implicit-mask behavior.
904 maskRegion.find( "<PinRef componentRef=\"R1\" pin=\"2\"" ) != std::string::npos,
905 "SMD copper pad without explicit F.Mask must get an implicit F.Mask opening" );
906}
907
908
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:121
General utilities for PCB file IO for QA programs.
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:320
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Removes an item from the container.
Definition board.cpp:1258
void SetCopperLayerCount(int aCount)
Definition board.cpp:954
BOARD_DESIGN_SETTINGS & GetDesignSettings() const
Definition board.cpp:1112
void SetPosition(const VECTOR2I &aPos) override
void SetReference(const wxString &aReference)
Definition footprint.h:847
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:403
LSET is a set of PCB_LAYER_IDs.
Definition lset.h:37
static constexpr PCB_LAYER_ID ALL_LAYERS
! Temporary layer identifier to identify code that is not padstack-aware
Definition padstack.h:177
Definition pad.h:61
void SetAttribute(PAD_ATTRIB aAttribute)
Definition pad.cpp:1615
void SetShape(PCB_LAYER_ID aLayer, PAD_SHAPE aShape)
Set the new shape of this pad.
Definition pad.h:193
void SetProperty(PAD_PROP aProperty)
Definition pad.cpp:1688
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:234
void SetSize(PCB_LAYER_ID aLayer, const VECTOR2I &aSize)
Definition pad.cpp:254
void SetLayerSet(const LSET &aLayers) override
Definition pad.cpp:1931
A #PLUGIN derivation for saving and loading Pcbnew s-expression formatted files.
@ F_Paste
Definition layer_ids.h:100
@ B_Cu
Definition layer_ids.h:61
@ F_Mask
Definition layer_ids.h:93
@ 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.
@ SMD
Smd pad, appears on the solder paste layer (default)
Definition padstack.h:99
@ RECTANGLE
Definition padstack.h:54
@ HEATSINK
a pad used as heat sink, usually in SMD footprints
Definition padstack.h:120
wxString GetXsdPath(char aVersion)
bool ExportAndValidate(BOARD *aBoard, char aVersion, wxString &aErrorMsg)
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)
std::string path
nlohmann::json output
BOOST_CHECK_MESSAGE(totalMismatches==0, std::to_string(totalMismatches)+" board(s) with strategy disagreements")
BOOST_TEST_CONTEXT("Test Clearance")
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683