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};
131
132} // anonymous namespace
133
134
136{
138 m_xmllintAvailable( IsXmllintAvailable() )
139 {
140 }
141
143 {
144 // Clean up temporary files
145 for( const wxString& path : m_tempFiles )
146 {
147 if( wxFileExists( path ) )
148 wxRemoveFile( path );
149 }
150 }
151
152 wxString CreateTempFile( const wxString& aSuffix = wxT( "" ) )
153 {
154 wxString path = wxFileName::CreateTempFileName( wxT( "kicad_ipc2581_test" ) );
155
156 if( !aSuffix.IsEmpty() )
157 path += aSuffix;
158 else
159 path += wxT( ".xml" );
160
161 m_tempFiles.push_back( path );
162 return path;
163 }
164
165 wxString GetXsdPath( char aVersion )
166 {
167 wxString filename = ( aVersion == 'C' ) ? wxT( "IPC-2581C.xsd" ) : wxT( "IPC-2581B1.xsd" );
168 return KI_TEST::GetPcbnewTestDataDir() + "ipc2581/" + filename;
169 }
170
171 std::unique_ptr<BOARD> LoadBoard( const std::string& aRelativePath )
172 {
173 std::string fullPath = KI_TEST::GetPcbnewTestDataDir() + aRelativePath;
174 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
175
176 m_kicadPlugin.LoadBoard( fullPath, board.get(), nullptr, nullptr );
177
178 return board;
179 }
180
181 bool ExportAndValidate( BOARD* aBoard, char aVersion, wxString& aErrorMsg )
182 {
183 wxString tempPath = CreateTempFile();
184
185 std::map<std::string, UTF8> props;
186 props["units"] = "mm";
187 props["version"] = std::string( 1, aVersion );
188 props["sigfig"] = "3";
189
190 try
191 {
192 m_ipc2581Plugin.SaveBoard( tempPath, aBoard, &props );
193 }
194 catch( const std::exception& e )
195 {
196 aErrorMsg = wxString::Format( "Export failed: %s", e.what() );
197 return false;
198 }
199
200 if( !wxFileExists( tempPath ) )
201 {
202 aErrorMsg = "Export file was not created";
203 return false;
204 }
205
207 {
208 wxString xsdPath = GetXsdPath( aVersion );
209
210 if( wxFileExists( xsdPath ) )
211 {
212 aErrorMsg = ValidateXmlWithXsd( tempPath, xsdPath );
213 return aErrorMsg.IsEmpty();
214 }
215 }
216
217 // If xmllint not available, just check that export succeeded
218 return true;
219 }
220
222 std::vector<wxString> m_tempFiles;
225};
226
227
228BOOST_FIXTURE_TEST_SUITE( Ipc2581Export, IPC2581_EXPORT_FIXTURE )
229
230
231
239BOOST_AUTO_TEST_CASE( SurfaceFinishExport )
240{
241 // Load a board with ENIG surface finish (issue3812.kicad_pcb has ENIG)
242 std::unique_ptr<BOARD> board = LoadBoard( "issue3812.kicad_pcb" );
243
244 BOOST_REQUIRE( board );
245
246 // Verify the board has ENIG finish
247 const BOARD_STACKUP& stackup = board->GetDesignSettings().GetStackupDescriptor();
248 BOOST_CHECK_EQUAL( stackup.m_FinishType, wxT( "ENIG" ) );
249
250 // Export to IPC-2581 version C
251 wxString tempPath = CreateTempFile();
252
253 std::map<std::string, UTF8> props;
254 props["units"] = "mm";
255 props["version"] = "C";
256 props["sigfig"] = "3";
257
258 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
259
260 BOOST_REQUIRE( wxFileExists( tempPath ) );
261
262 // Verify SurfaceFinish element is present with correct type
263 // Upstream uses nested structure: <SurfaceFinish><Finish type="ENIG-N"/></SurfaceFinish>
264 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "<SurfaceFinish" ) ),
265 "SurfaceFinish element should be present" );
266 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "<Finish" ) ),
267 "Finish element should be present" );
268 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "type=\"ENIG-N\"" ) ),
269 "Finish type should be ENIG-N" );
270
271 // Verify coating layers are present
272 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "COATING_TOP" ) ),
273 "COATING_TOP layer should be present" );
274 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "COATING_BOTTOM" ) ),
275 "COATING_BOTTOM layer should be present" );
276 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "layerFunction=\"COATINGCOND\"" ) ),
277 "Coating layers should have layerFunction=COATINGCOND" );
278
279 // Note: XSD validation is done separately in SchemaValidation tests.
280 // This test focuses on verifying the surface finish elements are present.
281}
282
283
287BOOST_AUTO_TEST_CASE( NoSurfaceFinishExport )
288{
289 // Load a board without surface finish (vme-wren.kicad_pcb has "None")
290 std::unique_ptr<BOARD> board = LoadBoard( "vme-wren.kicad_pcb" );
291
292 BOOST_REQUIRE( board );
293
294 // Verify the board has no finish
295 const BOARD_STACKUP& stackup = board->GetDesignSettings().GetStackupDescriptor();
296 BOOST_CHECK( stackup.m_FinishType == wxT( "None" ) || stackup.m_FinishType.IsEmpty() );
297
298 // Export to IPC-2581 version C
299 wxString tempPath = CreateTempFile();
300
301 std::map<std::string, UTF8> props;
302 props["units"] = "mm";
303 props["version"] = "C";
304 props["sigfig"] = "3";
305
306 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
307
308 BOOST_REQUIRE( wxFileExists( tempPath ) );
309
310 // Verify SurfaceFinish element is NOT present
311 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "<SurfaceFinish" ) ),
312 "SurfaceFinish element should not be present for 'None' finish" );
313
314 // Verify coating layers are NOT present
315 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "COATING_TOP" ) ),
316 "COATING_TOP layer should not be present" );
317 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "COATING_BOTTOM" ) ),
318 "COATING_BOTTOM layer should not be present" );
319
320 // Note: XSD validation is done separately in SchemaValidation tests.
321 // This test focuses on verifying coating layers are NOT present for "None" finish.
322}
323
324
331BOOST_AUTO_TEST_CASE( SchemaValidationVersionB )
332{
333 if( !m_xmllintAvailable )
334 {
335 BOOST_WARN_MESSAGE( false, "xmllint not available, skipping schema validation tests" );
336 return;
337 }
338
339 wxString xsdPath = GetXsdPath( 'B' );
340
341 if( !wxFileExists( xsdPath ) )
342 {
343 BOOST_WARN_MESSAGE( false, "IPC-2581B1.xsd not found, skipping schema validation" );
344 return;
345 }
346
347 for( const std::string& boardFile : VALIDATION_TEST_BOARDS )
348 {
349 BOOST_TEST_CONTEXT( "Board: " << boardFile << " (Version B)" )
350 {
351 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
352
353 if( !board )
354 {
355 BOOST_WARN_MESSAGE( false, "Could not load board: " + boardFile );
356 continue;
357 }
358
359 wxString errorMsg;
360 bool valid = ExportAndValidate( board.get(), 'B', errorMsg );
361
362 BOOST_CHECK_MESSAGE( valid, "IPC-2581B validation failed for " + boardFile + ": " + errorMsg );
363 }
364 }
365}
366
367
374BOOST_AUTO_TEST_CASE( SchemaValidationVersionC )
375{
376 if( !m_xmllintAvailable )
377 {
378 BOOST_WARN_MESSAGE( false, "xmllint not available, skipping schema validation tests" );
379 return;
380 }
381
382 wxString xsdPath = GetXsdPath( 'C' );
383
384 if( !wxFileExists( xsdPath ) )
385 {
386 BOOST_WARN_MESSAGE( false, "IPC-2581C.xsd not found, skipping schema validation" );
387 return;
388 }
389
390 for( const std::string& boardFile : VALIDATION_TEST_BOARDS )
391 {
392 BOOST_TEST_CONTEXT( "Board: " << boardFile << " (Version C)" )
393 {
394 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
395
396 if( !board )
397 {
398 BOOST_WARN_MESSAGE( false, "Could not load board: " + boardFile );
399 continue;
400 }
401
402 wxString errorMsg;
403 bool valid = ExportAndValidate( board.get(), 'C', errorMsg );
404
405 BOOST_CHECK_MESSAGE( valid, "IPC-2581C validation failed for " + boardFile + ": " + errorMsg );
406 }
407 }
408}
409
410
417BOOST_AUTO_TEST_CASE( ComplexBoardExport )
418{
419 // Test boards with specific complex features
420 static const std::vector<std::string> complexBoards = {
421 "intersectingzones.kicad_pcb",
422 "custom_pads.kicad_pcb",
423 };
424
425 for( const std::string& boardFile : complexBoards )
426 {
427 BOOST_TEST_CONTEXT( "Complex board: " << boardFile )
428 {
429 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
430
431 if( !board )
432 {
433 BOOST_WARN_MESSAGE( false, "Could not load board: " + boardFile );
434 continue;
435 }
436
437 // Test both versions
438 for( char version : { 'B', 'C' } )
439 {
440 BOOST_TEST_CONTEXT( "Version " << version )
441 {
442 wxString errorMsg;
443 bool valid = ExportAndValidate( board.get(), version, errorMsg );
444
445 BOOST_CHECK_MESSAGE( valid,
446 wxString::Format( "Export/validation failed for %s version %c: %s",
447 boardFile, version, errorMsg ) );
448 }
449 }
450 }
451 }
452}
453
454
462BOOST_AUTO_TEST_CASE( SmdPadSolderMaskExport_Issue16658 )
463{
464 // Load a board with standard SMD components (capacitors using SMD footprints)
465 std::unique_ptr<BOARD> board = LoadBoard( "issue16658/issue16658.kicad_pcb" );
466
467 BOOST_REQUIRE( board );
468
469 // Verify the board has SMD pads with implicit mask openings
470 bool hasSmtPad = false;
471
472 for( FOOTPRINT* fp : board->Footprints() )
473 {
474 for( PAD* pad : fp->Pads() )
475 {
476 if( pad->GetAttribute() == PAD_ATTRIB::SMD )
477 {
478 hasSmtPad = true;
479
480 // Verify pad is on copper but NOT explicitly on mask layer
481 bool isOnCopperOnly = pad->IsOnLayer( F_Cu ) && !pad->IsOnLayer( F_Mask );
482
483 if( isOnCopperOnly )
484 {
485 // This is the condition we're testing
486 break;
487 }
488 }
489 }
490
491 if( hasSmtPad )
492 break;
493 }
494
495 BOOST_REQUIRE_MESSAGE( hasSmtPad, "Test board should have SMD pads" );
496
497 // Export to IPC-2581 version C
498 wxString tempPath = CreateTempFile();
499
500 std::map<std::string, UTF8> props;
501 props["units"] = "mm";
502 props["version"] = "C";
503 props["sigfig"] = "3";
504
505 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
506
507 BOOST_REQUIRE( wxFileExists( tempPath ) );
508
509 // Verify that F_Mask layer features are present in the export
510 // (this was the bug - mask layers were empty for SMD pads)
511 bool hasFMaskLayer = FileContainsPattern( tempPath, wxT( "layerRef=\"F.Mask\"" ) )
512 || FileContainsPattern( tempPath, wxT( "layerRef=\"TSM\"" ) );
513
514 BOOST_CHECK_MESSAGE( hasFMaskLayer,
515 "IPC-2581 export should contain F.Mask layer features for SMD pads" );
516
517 // Also check for LayerFeature element with mask layer reference
518 bool hasLayerFeature = FileContainsPattern( tempPath, wxT( "<LayerFeature" ) );
519 BOOST_CHECK_MESSAGE( hasLayerFeature, "IPC-2581 export should contain LayerFeature elements" );
520}
521
522
530BOOST_AUTO_TEST_CASE( EmptyRefDesProducesValidXml )
531{
532 std::unique_ptr<BOARD> board = LoadBoard( "padstacks_complex.kicad_pcb" );
533 BOOST_REQUIRE( board );
534
535 for( char version : { 'B', 'C' } )
536 {
537 BOOST_TEST_CONTEXT( "Version " << version )
538 {
539 wxString tempPath = CreateTempFile();
540
541 std::map<std::string, UTF8> props;
542 props["units"] = "mm";
543 props["version"] = std::string( 1, version );
544 props["sigfig"] = "3";
545
546 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
547 BOOST_REQUIRE( wxFileExists( tempPath ) );
548
549 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "refDes=\"\"" ) ),
550 "Empty refDes attribute found" );
551 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "<RefDes name=\"\"" ) ),
552 "Empty RefDes/@name attribute found" );
553 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "componentRef=\"\"" ) ),
554 "Empty PinRef/@componentRef attribute found" );
555 }
556
557 m_ipc2581Plugin = PCB_IO_IPC2581();
558 }
559}
560
561
570BOOST_AUTO_TEST_CASE( FlippedComponentRotation )
571{
572 std::unique_ptr<BOARD> board = LoadBoard( "issue12609.kicad_pcb" );
573 BOOST_REQUIRE( board );
574
575 wxString tempPath = CreateTempFile();
576
577 std::map<std::string, UTF8> props;
578 props["units"] = "mm";
579 props["version"] = "C";
580 props["sigfig"] = "3";
581
582 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
583 BOOST_REQUIRE( wxFileExists( tempPath ) );
584
585 // C5 is on B.Cu at 90 degrees. Its Component Xform must be rotation="90.0",
586 // not the inverted "270.0". Check by finding Component refDes="C5" and verifying
587 // its Xform has rotation="90.0".
588 std::ifstream xmlFile( tempPath.ToStdString() );
589 BOOST_REQUIRE( xmlFile.is_open() );
590
591 std::string xmlContent( ( std::istreambuf_iterator<char>( xmlFile ) ),
592 std::istreambuf_iterator<char>() );
593
594 // Find the C5 component and check its rotation
595 size_t c5Pos = xmlContent.find( "refDes=\"C5\"" );
596
597 if( c5Pos == std::string::npos )
598 c5Pos = xmlContent.find( "refDes=\"NOREF_" );
599
600 BOOST_REQUIRE_MESSAGE( c5Pos != std::string::npos,
601 "C5 component should exist in export" );
602
603 // Look for the Xform within the next 200 chars after refDes="C5"
604 std::string c5Region = xmlContent.substr( c5Pos, 200 );
605 BOOST_CHECK_MESSAGE( c5Region.find( "rotation=\"90.0\"" ) != std::string::npos
606 || c5Region.find( "rotation=\"90.00\"" ) != std::string::npos,
607 "C5 component rotation should be 90, not inverted. Region: "
608 + c5Region );
609}
610
611
615BOOST_AUTO_TEST_CASE( ContentBomRef )
616{
617 std::unique_ptr<BOARD> board = LoadBoard( "issue12609.kicad_pcb" );
618 BOOST_REQUIRE( board );
619
620 wxString tempPath = CreateTempFile();
621
622 std::map<std::string, UTF8> props;
623 props["units"] = "mm";
624 props["version"] = "C";
625 props["sigfig"] = "3";
626
627 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
628 BOOST_REQUIRE( wxFileExists( tempPath ) );
629
630 bool hasBom = FileContainsPattern( tempPath, wxT( "<Bom " ) );
631 bool hasBomRef = FileContainsPattern( tempPath, wxT( "<BomRef " ) );
632
633 if( hasBom )
634 {
635 BOOST_CHECK_MESSAGE( hasBomRef,
636 "Content should have BomRef when Bom section is present" );
637 }
638}
639
640
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:322
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
BOARD * LoadBoard(const wxString &aFileName, bool aSetActive)
Loads a board from file This function identifies the file type by extension and determines the correc...
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
BOOST_TEST_CONTEXT("Test Clearance")
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")