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, 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
32
36
39
40#include <board.h>
43#include <footprint.h>
44#include <pad.h>
45
46#include <wx/dir.h>
47#include <wx/file.h>
48#include <wx/filename.h>
49#include <wx/process.h>
50#include <wx/txtstrm.h>
51
52#include <fstream>
53#include <sstream>
54
55
56namespace
57{
58
62bool IsXmllintAvailable()
63{
64 wxArrayString output;
65 wxArrayString errors;
66 int result = wxExecute( "xmllint --version", output, errors, wxEXEC_SYNC );
67 return result == 0;
68}
69
70
75wxString ValidateXmlWithXsd( const wxString& aXmlPath, const wxString& aXsdPath )
76{
77 wxString cmd = wxString::Format( "xmllint --noout --schema \"%s\" \"%s\"",
78 aXsdPath, aXmlPath );
79
80 wxArrayString output;
81 wxArrayString errors;
82 int result = wxExecute( cmd, output, errors, wxEXEC_SYNC );
83
84 if( result != 0 )
85 {
86 wxString errorMsg;
87
88 for( const wxString& line : errors )
89 errorMsg += line + "\n";
90
91 return errorMsg;
92 }
93
94 return wxEmptyString;
95}
96
97
101bool FileContainsPattern( const wxString& aFilePath, const wxString& aPattern )
102{
103 std::ifstream file( aFilePath.ToStdString() );
104
105 if( !file.is_open() )
106 return false;
107
108 std::stringstream buffer;
109 buffer << file.rdbuf();
110 std::string content = buffer.str();
111
112 return content.find( aPattern.ToStdString() ) != std::string::npos;
113}
114
115
120static const std::vector<std::string> VALIDATION_TEST_BOARDS = {
121 "custom_pads.kicad_pcb",
122 "notched_zones.kicad_pcb",
123 "sliver.kicad_pcb",
124 "tracks_arcs_vias.kicad_pcb",
125 "issue7241.kicad_pcb",
126 "issue10906.kicad_pcb",
127 "issue22798.kicad_pcb",
128 "padstacks_complex.kicad_pcb",
129 "issue12609.kicad_pcb",
130 "issue22794.kicad_pcb",
131};
132
133} // anonymous namespace
134
135
137{
139 m_xmllintAvailable( IsXmllintAvailable() )
140 {
141 }
142
144 {
145 // Clean up temporary files
146 for( const wxString& path : m_tempFiles )
147 {
148 if( wxFileExists( path ) )
149 wxRemoveFile( path );
150 }
151 }
152
153 wxString CreateTempFile( const wxString& aSuffix = wxT( "" ) )
154 {
155 wxString path = wxFileName::CreateTempFileName( wxT( "kicad_ipc2581_test" ) );
156
157 if( !aSuffix.IsEmpty() )
158 path += aSuffix;
159 else
160 path += wxT( ".xml" );
161
162 m_tempFiles.push_back( path );
163 return path;
164 }
165
166 wxString GetXsdPath( char aVersion )
167 {
168 wxString filename = ( aVersion == 'C' ) ? wxT( "IPC-2581C.xsd" ) : wxT( "IPC-2581B1.xsd" );
169 return KI_TEST::GetPcbnewTestDataDir() + "ipc2581/" + filename;
170 }
171
172 std::unique_ptr<BOARD> LoadBoard( const std::string& aRelativePath )
173 {
174 std::string fullPath = KI_TEST::GetPcbnewTestDataDir() + aRelativePath;
175 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
176
177 m_kicadPlugin.LoadBoard( fullPath, board.get(), nullptr, nullptr );
178
179 return board;
180 }
181
182 bool ExportAndValidate( BOARD* aBoard, char aVersion, wxString& aErrorMsg )
183 {
184 wxString tempPath = CreateTempFile();
185
186 std::map<std::string, UTF8> props;
187 props["units"] = "mm";
188 props["version"] = std::string( 1, aVersion );
189 props["sigfig"] = "3";
190
191 try
192 {
193 m_ipc2581Plugin.SaveBoard( tempPath, aBoard, &props );
194 }
195 catch( const std::exception& e )
196 {
197 aErrorMsg = wxString::Format( "Export failed: %s", e.what() );
198 return false;
199 }
200
201 if( !wxFileExists( tempPath ) )
202 {
203 aErrorMsg = "Export file was not created";
204 return false;
205 }
206
208 {
209 wxString xsdPath = GetXsdPath( aVersion );
210
211 if( wxFileExists( xsdPath ) )
212 {
213 aErrorMsg = ValidateXmlWithXsd( tempPath, xsdPath );
214 return aErrorMsg.IsEmpty();
215 }
216 }
217
218 // If xmllint not available, just check that export succeeded
219 return true;
220 }
221
223 std::vector<wxString> m_tempFiles;
226};
227
228
229BOOST_FIXTURE_TEST_SUITE( Ipc2581Export, IPC2581_EXPORT_FIXTURE )
230
231
232
240BOOST_AUTO_TEST_CASE( SurfaceFinishExport )
241{
242 // Load a board with ENIG surface finish (issue3812.kicad_pcb has ENIG)
243 std::unique_ptr<BOARD> board = LoadBoard( "issue3812.kicad_pcb" );
244
245 BOOST_REQUIRE( board );
246
247 // Verify the board has ENIG finish
248 const BOARD_STACKUP& stackup = board->GetDesignSettings().GetStackupDescriptor();
249 BOOST_CHECK_EQUAL( stackup.m_FinishType, wxT( "ENIG" ) );
250
251 // Export to IPC-2581 version C
252 wxString tempPath = CreateTempFile();
253
254 std::map<std::string, UTF8> props;
255 props["units"] = "mm";
256 props["version"] = "C";
257 props["sigfig"] = "3";
258
259 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
260
261 BOOST_REQUIRE( wxFileExists( tempPath ) );
262
263 // Verify SurfaceFinish element is present with correct type attribute
264 // Schema requires: <SurfaceFinish type="ENIG-N"/>
265 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "<SurfaceFinish" ) ),
266 "SurfaceFinish element should be present" );
267 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "type=\"ENIG-N\"" ) ),
268 "SurfaceFinish type should be ENIG-N" );
269
270 // Verify coating layers are present
271 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "COATING_TOP" ) ),
272 "COATING_TOP layer should be present" );
273 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "COATING_BOTTOM" ) ),
274 "COATING_BOTTOM layer should be present" );
275 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "layerFunction=\"COATINGCOND\"" ) ),
276 "Coating layers should have layerFunction=COATINGCOND" );
277
278 // Note: XSD validation is done separately in SchemaValidation tests.
279 // This test focuses on verifying the surface finish elements are present.
280}
281
282
286BOOST_AUTO_TEST_CASE( NoSurfaceFinishExport )
287{
288 // Load a board without surface finish (vme-wren.kicad_pcb has "None")
289 std::unique_ptr<BOARD> board = LoadBoard( "vme-wren.kicad_pcb" );
290
291 BOOST_REQUIRE( board );
292
293 // Verify the board has no finish
294 const BOARD_STACKUP& stackup = board->GetDesignSettings().GetStackupDescriptor();
295 BOOST_CHECK( stackup.m_FinishType == wxT( "None" ) || stackup.m_FinishType.IsEmpty() );
296
297 // Export to IPC-2581 version C
298 wxString tempPath = CreateTempFile();
299
300 std::map<std::string, UTF8> props;
301 props["units"] = "mm";
302 props["version"] = "C";
303 props["sigfig"] = "3";
304
305 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
306
307 BOOST_REQUIRE( wxFileExists( tempPath ) );
308
309 // Verify SurfaceFinish element is NOT present
310 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "<SurfaceFinish" ) ),
311 "SurfaceFinish element should not be present for 'None' finish" );
312
313 // Verify coating layers are NOT present
314 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "COATING_TOP" ) ),
315 "COATING_TOP layer should not be present" );
316 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "COATING_BOTTOM" ) ),
317 "COATING_BOTTOM layer should not be present" );
318
319 // Note: XSD validation is done separately in SchemaValidation tests.
320 // This test focuses on verifying coating layers are NOT present for "None" finish.
321}
322
323
330BOOST_AUTO_TEST_CASE( SchemaValidationVersionB )
331{
332 if( !m_xmllintAvailable )
333 {
334 BOOST_WARN_MESSAGE( false, "xmllint not available, skipping schema validation tests" );
335 return;
336 }
337
338 wxString xsdPath = GetXsdPath( 'B' );
339
340 if( !wxFileExists( xsdPath ) )
341 {
342 BOOST_WARN_MESSAGE( false, "IPC-2581B1.xsd not found, skipping schema validation" );
343 return;
344 }
345
346 for( const std::string& boardFile : VALIDATION_TEST_BOARDS )
347 {
348 BOOST_TEST_CONTEXT( "Board: " << boardFile << " (Version B)" )
349 {
350 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
351
352 if( !board )
353 {
354 BOOST_WARN_MESSAGE( false, "Could not load board: " + boardFile );
355 continue;
356 }
357
358 wxString errorMsg;
359 bool valid = ExportAndValidate( board.get(), 'B', errorMsg );
360
361 BOOST_CHECK_MESSAGE( valid, "IPC-2581B validation failed for " + boardFile + ": " + errorMsg );
362 }
363 }
364}
365
366
373BOOST_AUTO_TEST_CASE( SchemaValidationVersionC )
374{
375 if( !m_xmllintAvailable )
376 {
377 BOOST_WARN_MESSAGE( false, "xmllint not available, skipping schema validation tests" );
378 return;
379 }
380
381 wxString xsdPath = GetXsdPath( 'C' );
382
383 if( !wxFileExists( xsdPath ) )
384 {
385 BOOST_WARN_MESSAGE( false, "IPC-2581C.xsd not found, skipping schema validation" );
386 return;
387 }
388
389 for( const std::string& boardFile : VALIDATION_TEST_BOARDS )
390 {
391 BOOST_TEST_CONTEXT( "Board: " << boardFile << " (Version C)" )
392 {
393 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
394
395 if( !board )
396 {
397 BOOST_WARN_MESSAGE( false, "Could not load board: " + boardFile );
398 continue;
399 }
400
401 wxString errorMsg;
402 bool valid = ExportAndValidate( board.get(), 'C', errorMsg );
403
404 BOOST_CHECK_MESSAGE( valid, "IPC-2581C validation failed for " + boardFile + ": " + errorMsg );
405 }
406 }
407}
408
409
416BOOST_AUTO_TEST_CASE( ComplexBoardExport )
417{
418 // Test boards with specific complex features
419 static const std::vector<std::string> complexBoards = {
420 "intersectingzones.kicad_pcb",
421 "custom_pads.kicad_pcb",
422 };
423
424 for( const std::string& boardFile : complexBoards )
425 {
426 BOOST_TEST_CONTEXT( "Complex board: " << boardFile )
427 {
428 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
429
430 if( !board )
431 {
432 BOOST_WARN_MESSAGE( false, "Could not load board: " + boardFile );
433 continue;
434 }
435
436 // Test both versions
437 for( char version : { 'B', 'C' } )
438 {
439 BOOST_TEST_CONTEXT( "Version " << version )
440 {
441 wxString errorMsg;
442 bool valid = ExportAndValidate( board.get(), version, errorMsg );
443
444 BOOST_CHECK_MESSAGE( valid,
445 wxString::Format( "Export/validation failed for %s version %c: %s",
446 boardFile, version, errorMsg ) );
447 }
448 }
449 }
450 }
451}
452
453
461BOOST_AUTO_TEST_CASE( SmdPadSolderMaskExport_Issue16658 )
462{
463 // Load a board with standard SMD components (capacitors using SMD footprints)
464 std::unique_ptr<BOARD> board = LoadBoard( "issue16658/issue16658.kicad_pcb" );
465
466 BOOST_REQUIRE( board );
467
468 // Verify the board has SMD pads with implicit mask openings
469 bool hasSmtPad = false;
470
471 for( FOOTPRINT* fp : board->Footprints() )
472 {
473 for( PAD* pad : fp->Pads() )
474 {
475 if( pad->GetAttribute() == PAD_ATTRIB::SMD )
476 {
477 hasSmtPad = true;
478
479 // Verify pad is on copper but NOT explicitly on mask layer
480 bool isOnCopperOnly = pad->IsOnLayer( F_Cu ) && !pad->IsOnLayer( F_Mask );
481
482 if( isOnCopperOnly )
483 {
484 // This is the condition we're testing
485 break;
486 }
487 }
488 }
489
490 if( hasSmtPad )
491 break;
492 }
493
494 BOOST_REQUIRE_MESSAGE( hasSmtPad, "Test board should have SMD pads" );
495
496 // Export to IPC-2581 version C
497 wxString tempPath = CreateTempFile();
498
499 std::map<std::string, UTF8> props;
500 props["units"] = "mm";
501 props["version"] = "C";
502 props["sigfig"] = "3";
503
504 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
505
506 BOOST_REQUIRE( wxFileExists( tempPath ) );
507
508 // Verify that F_Mask layer features are present in the export
509 // (this was the bug - mask layers were empty for SMD pads)
510 bool hasFMaskLayer = FileContainsPattern( tempPath, wxT( "layerRef=\"F.Mask\"" ) )
511 || FileContainsPattern( tempPath, wxT( "layerRef=\"TSM\"" ) );
512
513 BOOST_CHECK_MESSAGE( hasFMaskLayer,
514 "IPC-2581 export should contain F.Mask layer features for SMD pads" );
515
516 // Also check for LayerFeature element with mask layer reference
517 bool hasLayerFeature = FileContainsPattern( tempPath, wxT( "<LayerFeature" ) );
518 BOOST_CHECK_MESSAGE( hasLayerFeature, "IPC-2581 export should contain LayerFeature elements" );
519}
520
521
529BOOST_AUTO_TEST_CASE( EmptyRefDesProducesValidXml )
530{
531 std::unique_ptr<BOARD> board = LoadBoard( "padstacks_complex.kicad_pcb" );
532 BOOST_REQUIRE( board );
533
534 for( char version : { 'B', 'C' } )
535 {
536 BOOST_TEST_CONTEXT( "Version " << version )
537 {
538 wxString tempPath = CreateTempFile();
539
540 std::map<std::string, UTF8> props;
541 props["units"] = "mm";
542 props["version"] = std::string( 1, version );
543 props["sigfig"] = "3";
544
545 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
546 BOOST_REQUIRE( wxFileExists( tempPath ) );
547
548 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "refDes=\"\"" ) ),
549 "Empty refDes attribute found" );
550 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "<RefDes name=\"\"" ) ),
551 "Empty RefDes/@name attribute found" );
552 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "componentRef=\"\"" ) ),
553 "Empty PinRef/@componentRef attribute found" );
554 }
555
556 m_ipc2581Plugin = PCB_IO_IPC2581();
557 }
558}
559
560
569BOOST_AUTO_TEST_CASE( FlippedComponentRotation )
570{
571 std::unique_ptr<BOARD> board = LoadBoard( "issue12609.kicad_pcb" );
572 BOOST_REQUIRE( board );
573
574 wxString tempPath = CreateTempFile();
575
576 std::map<std::string, UTF8> props;
577 props["units"] = "mm";
578 props["version"] = "C";
579 props["sigfig"] = "3";
580
581 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
582 BOOST_REQUIRE( wxFileExists( tempPath ) );
583
584 // C5 is on B.Cu at 90 degrees. Its Component Xform must be rotation="90.0",
585 // not the inverted "270.0". Check by finding Component refDes="C5" and verifying
586 // its Xform has rotation="90.0".
587 std::ifstream xmlFile( tempPath.ToStdString() );
588 BOOST_REQUIRE( xmlFile.is_open() );
589
590 std::string xmlContent( ( std::istreambuf_iterator<char>( xmlFile ) ),
591 std::istreambuf_iterator<char>() );
592
593 // Find the C5 component and check its rotation
594 size_t c5Pos = xmlContent.find( "refDes=\"C5\"" );
595
596 if( c5Pos == std::string::npos )
597 c5Pos = xmlContent.find( "refDes=\"NOREF_" );
598
599 BOOST_REQUIRE_MESSAGE( c5Pos != std::string::npos,
600 "C5 component should exist in export" );
601
602 // Look for the Xform within the next 200 chars after refDes="C5"
603 std::string c5Region = xmlContent.substr( c5Pos, 200 );
604 BOOST_CHECK_MESSAGE( c5Region.find( "rotation=\"90.0\"" ) != std::string::npos
605 || c5Region.find( "rotation=\"90.00\"" ) != std::string::npos,
606 "C5 component rotation should be 90, not inverted. Region: "
607 + c5Region );
608}
609
610
614BOOST_AUTO_TEST_CASE( ContentBomRef )
615{
616 std::unique_ptr<BOARD> board = LoadBoard( "issue12609.kicad_pcb" );
617 BOOST_REQUIRE( board );
618
619 wxString tempPath = CreateTempFile();
620
621 std::map<std::string, UTF8> props;
622 props["units"] = "mm";
623 props["version"] = "C";
624 props["sigfig"] = "3";
625
626 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
627 BOOST_REQUIRE( wxFileExists( tempPath ) );
628
629 bool hasBom = FileContainsPattern( tempPath, wxT( "<Bom " ) );
630 bool hasBomRef = FileContainsPattern( tempPath, wxT( "<BomRef " ) );
631
632 if( hasBom )
633 {
634 BOOST_CHECK_MESSAGE( hasBomRef,
635 "Content should have BomRef when Bom section is present" );
636 }
637}
638
639
653BOOST_AUTO_TEST_CASE( KnockoutTextMultiContour_Issue23968 )
654{
655 std::unique_ptr<BOARD> board = LoadBoard( "test_copper_graphics.kicad_pcb" );
656 BOOST_REQUIRE( board );
657
658 for( char version : { 'B', 'C' } )
659 {
660 BOOST_TEST_CONTEXT( "Version " << version )
661 {
662 wxString errorMsg;
663 bool valid = ExportAndValidate( board.get(), version, errorMsg );
664
665 BOOST_CHECK_MESSAGE( valid,
666 wxString::Format( "Knockout text export should be schema-valid "
667 "(version %c): %s", version, errorMsg ) );
668 }
669
670 m_ipc2581Plugin = PCB_IO_IPC2581();
671 }
672}
673
674
General utilities for PCB file IO for QA programs.
Manage layers needed to make a physical board.
wxString m_FinishType
The name of external copper finish.
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:323
Definition pad.h:55
A #PLUGIN derivation for saving and loading Pcbnew s-expression formatted files.
@ F_Mask
Definition layer_ids.h:97
@ F_Cu
Definition layer_ids.h:64
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
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")