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};
129
130} // anonymous namespace
131
132
134{
136 m_xmllintAvailable( IsXmllintAvailable() )
137 {
138 }
139
141 {
142 // Clean up temporary files
143 for( const wxString& path : m_tempFiles )
144 {
145 if( wxFileExists( path ) )
146 wxRemoveFile( path );
147 }
148 }
149
150 wxString CreateTempFile( const wxString& aSuffix = wxT( "" ) )
151 {
152 wxString path = wxFileName::CreateTempFileName( wxT( "kicad_ipc2581_test" ) );
153
154 if( !aSuffix.IsEmpty() )
155 path += aSuffix;
156 else
157 path += wxT( ".xml" );
158
159 m_tempFiles.push_back( path );
160 return path;
161 }
162
163 wxString GetXsdPath( char aVersion )
164 {
165 wxString filename = ( aVersion == 'C' ) ? wxT( "IPC-2581C.xsd" ) : wxT( "IPC-2581B1.xsd" );
166 return KI_TEST::GetPcbnewTestDataDir() + "ipc2581/" + filename;
167 }
168
169 std::unique_ptr<BOARD> LoadBoard( const std::string& aRelativePath )
170 {
171 std::string fullPath = KI_TEST::GetPcbnewTestDataDir() + aRelativePath;
172 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
173
174 m_kicadPlugin.LoadBoard( fullPath, board.get(), nullptr, nullptr );
175
176 return board;
177 }
178
179 bool ExportAndValidate( BOARD* aBoard, char aVersion, wxString& aErrorMsg )
180 {
181 wxString tempPath = CreateTempFile();
182
183 std::map<std::string, UTF8> props;
184 props["units"] = "mm";
185 props["version"] = std::string( 1, aVersion );
186 props["sigfig"] = "3";
187
188 try
189 {
190 m_ipc2581Plugin.SaveBoard( tempPath, aBoard, &props );
191 }
192 catch( const std::exception& e )
193 {
194 aErrorMsg = wxString::Format( "Export failed: %s", e.what() );
195 return false;
196 }
197
198 if( !wxFileExists( tempPath ) )
199 {
200 aErrorMsg = "Export file was not created";
201 return false;
202 }
203
205 {
206 wxString xsdPath = GetXsdPath( aVersion );
207
208 if( wxFileExists( xsdPath ) )
209 {
210 aErrorMsg = ValidateXmlWithXsd( tempPath, xsdPath );
211 return aErrorMsg.IsEmpty();
212 }
213 }
214
215 // If xmllint not available, just check that export succeeded
216 return true;
217 }
218
220 std::vector<wxString> m_tempFiles;
223};
224
225
226BOOST_FIXTURE_TEST_SUITE( Ipc2581Export, IPC2581_EXPORT_FIXTURE )
227
228
229
237BOOST_AUTO_TEST_CASE( SurfaceFinishExport )
238{
239 // Load a board with ENIG surface finish (issue3812.kicad_pcb has ENIG)
240 std::unique_ptr<BOARD> board = LoadBoard( "issue3812.kicad_pcb" );
241
242 BOOST_REQUIRE( board );
243
244 // Verify the board has ENIG finish
245 const BOARD_STACKUP& stackup = board->GetDesignSettings().GetStackupDescriptor();
246 BOOST_CHECK_EQUAL( stackup.m_FinishType, wxT( "ENIG" ) );
247
248 // Export to IPC-2581 version C
249 wxString tempPath = CreateTempFile();
250
251 std::map<std::string, UTF8> props;
252 props["units"] = "mm";
253 props["version"] = "C";
254 props["sigfig"] = "3";
255
256 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
257
258 BOOST_REQUIRE( wxFileExists( tempPath ) );
259
260 // Verify SurfaceFinish element is present with correct type
261 // Upstream uses nested structure: <SurfaceFinish><Finish type="ENIG-N"/></SurfaceFinish>
262 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "<SurfaceFinish" ) ),
263 "SurfaceFinish element should be present" );
264 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "<Finish" ) ),
265 "Finish element should be present" );
266 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "type=\"ENIG-N\"" ) ),
267 "Finish type should be ENIG-N" );
268
269 // Verify coating layers are present
270 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "COATING_TOP" ) ),
271 "COATING_TOP layer should be present" );
272 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "COATING_BOTTOM" ) ),
273 "COATING_BOTTOM layer should be present" );
274 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "layerFunction=\"COATINGCOND\"" ) ),
275 "Coating layers should have layerFunction=COATINGCOND" );
276
277 // Note: XSD validation is done separately in SchemaValidation tests.
278 // This test focuses on verifying the surface finish elements are present.
279}
280
281
285BOOST_AUTO_TEST_CASE( NoSurfaceFinishExport )
286{
287 // Load a board without surface finish (vme-wren.kicad_pcb has "None")
288 std::unique_ptr<BOARD> board = LoadBoard( "vme-wren.kicad_pcb" );
289
290 BOOST_REQUIRE( board );
291
292 // Verify the board has no finish
293 const BOARD_STACKUP& stackup = board->GetDesignSettings().GetStackupDescriptor();
294 BOOST_CHECK( stackup.m_FinishType == wxT( "None" ) || stackup.m_FinishType.IsEmpty() );
295
296 // Export to IPC-2581 version C
297 wxString tempPath = CreateTempFile();
298
299 std::map<std::string, UTF8> props;
300 props["units"] = "mm";
301 props["version"] = "C";
302 props["sigfig"] = "3";
303
304 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
305
306 BOOST_REQUIRE( wxFileExists( tempPath ) );
307
308 // Verify SurfaceFinish element is NOT present
309 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "<SurfaceFinish" ) ),
310 "SurfaceFinish element should not be present for 'None' finish" );
311
312 // Verify coating layers are NOT present
313 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "COATING_TOP" ) ),
314 "COATING_TOP layer should not be present" );
315 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "COATING_BOTTOM" ) ),
316 "COATING_BOTTOM layer should not be present" );
317
318 // Note: XSD validation is done separately in SchemaValidation tests.
319 // This test focuses on verifying coating layers are NOT present for "None" finish.
320}
321
322
329BOOST_AUTO_TEST_CASE( SchemaValidationVersionB )
330{
331 if( !m_xmllintAvailable )
332 {
333 BOOST_WARN_MESSAGE( false, "xmllint not available, skipping schema validation tests" );
334 return;
335 }
336
337 wxString xsdPath = GetXsdPath( 'B' );
338
339 if( !wxFileExists( xsdPath ) )
340 {
341 BOOST_WARN_MESSAGE( false, "IPC-2581B1.xsd not found, skipping schema validation" );
342 return;
343 }
344
345 for( const std::string& boardFile : VALIDATION_TEST_BOARDS )
346 {
347 BOOST_TEST_CONTEXT( "Board: " << boardFile << " (Version B)" )
348 {
349 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
350
351 if( !board )
352 {
353 BOOST_WARN_MESSAGE( false, "Could not load board: " + boardFile );
354 continue;
355 }
356
357 wxString errorMsg;
358 bool valid = ExportAndValidate( board.get(), 'B', errorMsg );
359
360 BOOST_CHECK_MESSAGE( valid, "IPC-2581B validation failed for " + boardFile + ": " + errorMsg );
361 }
362 }
363}
364
365
372BOOST_AUTO_TEST_CASE( SchemaValidationVersionC )
373{
374 if( !m_xmllintAvailable )
375 {
376 BOOST_WARN_MESSAGE( false, "xmllint not available, skipping schema validation tests" );
377 return;
378 }
379
380 wxString xsdPath = GetXsdPath( 'C' );
381
382 if( !wxFileExists( xsdPath ) )
383 {
384 BOOST_WARN_MESSAGE( false, "IPC-2581C.xsd not found, skipping schema validation" );
385 return;
386 }
387
388 for( const std::string& boardFile : VALIDATION_TEST_BOARDS )
389 {
390 BOOST_TEST_CONTEXT( "Board: " << boardFile << " (Version C)" )
391 {
392 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
393
394 if( !board )
395 {
396 BOOST_WARN_MESSAGE( false, "Could not load board: " + boardFile );
397 continue;
398 }
399
400 wxString errorMsg;
401 bool valid = ExportAndValidate( board.get(), 'C', errorMsg );
402
403 BOOST_CHECK_MESSAGE( valid, "IPC-2581C validation failed for " + boardFile + ": " + errorMsg );
404 }
405 }
406}
407
408
415BOOST_AUTO_TEST_CASE( ComplexBoardExport )
416{
417 // Test boards with specific complex features
418 static const std::vector<std::string> complexBoards = {
419 "intersectingzones.kicad_pcb",
420 "custom_pads.kicad_pcb",
421 };
422
423 for( const std::string& boardFile : complexBoards )
424 {
425 BOOST_TEST_CONTEXT( "Complex board: " << boardFile )
426 {
427 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
428
429 if( !board )
430 {
431 BOOST_WARN_MESSAGE( false, "Could not load board: " + boardFile );
432 continue;
433 }
434
435 // Test both versions
436 for( char version : { 'B', 'C' } )
437 {
438 BOOST_TEST_CONTEXT( "Version " << version )
439 {
440 wxString errorMsg;
441 bool valid = ExportAndValidate( board.get(), version, errorMsg );
442
443 BOOST_CHECK_MESSAGE( valid,
444 wxString::Format( "Export/validation failed for %s version %c: %s",
445 boardFile, version, errorMsg ) );
446 }
447 }
448 }
449 }
450}
451
452
460BOOST_AUTO_TEST_CASE( SmdPadSolderMaskExport_Issue16658 )
461{
462 // Load a board with standard SMD components (capacitors using SMD footprints)
463 std::unique_ptr<BOARD> board = LoadBoard( "issue16658/issue16658.kicad_pcb" );
464
465 BOOST_REQUIRE( board );
466
467 // Verify the board has SMD pads with implicit mask openings
468 bool hasSmtPad = false;
469
470 for( FOOTPRINT* fp : board->Footprints() )
471 {
472 for( PAD* pad : fp->Pads() )
473 {
474 if( pad->GetAttribute() == PAD_ATTRIB::SMD )
475 {
476 hasSmtPad = true;
477
478 // Verify pad is on copper but NOT explicitly on mask layer
479 bool isOnCopperOnly = pad->IsOnLayer( F_Cu ) && !pad->IsOnLayer( F_Mask );
480
481 if( isOnCopperOnly )
482 {
483 // This is the condition we're testing
484 break;
485 }
486 }
487 }
488
489 if( hasSmtPad )
490 break;
491 }
492
493 BOOST_REQUIRE_MESSAGE( hasSmtPad, "Test board should have SMD pads" );
494
495 // Export to IPC-2581 version C
496 wxString tempPath = CreateTempFile();
497
498 std::map<std::string, UTF8> props;
499 props["units"] = "mm";
500 props["version"] = "C";
501 props["sigfig"] = "3";
502
503 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
504
505 BOOST_REQUIRE( wxFileExists( tempPath ) );
506
507 // Verify that F_Mask layer features are present in the export
508 // (this was the bug - mask layers were empty for SMD pads)
509 bool hasFMaskLayer = FileContainsPattern( tempPath, wxT( "layerRef=\"F.Mask\"" ) )
510 || FileContainsPattern( tempPath, wxT( "layerRef=\"TSM\"" ) );
511
512 BOOST_CHECK_MESSAGE( hasFMaskLayer,
513 "IPC-2581 export should contain F.Mask layer features for SMD pads" );
514
515 // Also check for LayerFeature element with mask layer reference
516 bool hasLayerFeature = FileContainsPattern( tempPath, wxT( "<LayerFeature" ) );
517 BOOST_CHECK_MESSAGE( hasLayerFeature, "IPC-2581 export should contain LayerFeature elements" );
518}
519
520
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")