46#include <wx/filename.h>
47#include <wx/process.h>
48#include <wx/txtstrm.h>
60bool IsXmllintAvailable()
64 int result = wxExecute(
"xmllint --version",
output, errors, wxEXEC_SYNC );
73wxString ValidateXmlWithXsd(
const wxString& aXmlPath,
const wxString& aXsdPath )
75 wxString cmd = wxString::Format(
"xmllint --noout --schema \"%s\" \"%s\"",
80 int result = wxExecute( cmd,
output, errors, wxEXEC_SYNC );
86 for(
const wxString& line : errors )
87 errorMsg += line +
"\n";
99bool FileContainsPattern(
const wxString& aFilePath,
const wxString& aPattern )
101 std::ifstream file( aFilePath.ToStdString() );
103 if( !file.is_open() )
106 std::stringstream buffer;
107 buffer << file.rdbuf();
108 std::string content = buffer.str();
110 return content.find( aPattern.ToStdString() ) != std::string::npos;
118static const std::vector<std::string> VALIDATION_TEST_BOARDS = {
119 "custom_pads.kicad_pcb",
120 "notched_zones.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",
146 if( wxFileExists(
path ) )
147 wxRemoveFile(
path );
153 wxString
path = wxFileName::CreateTempFileName( wxT(
"kicad_ipc2581_test" ) );
155 if( !aSuffix.IsEmpty() )
158 path += wxT(
".xml" );
166 wxString filename = ( aVersion ==
'C' ) ? wxT(
"IPC-2581C.xsd" ) : wxT(
"IPC-2581B1.xsd" );
170 std::unique_ptr<BOARD>
LoadBoard(
const std::string& aRelativePath )
173 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
175 m_kicadPlugin.LoadBoard( fullPath, board.get(),
nullptr,
nullptr );
184 std::map<std::string, UTF8> props;
185 props[
"units"] =
"mm";
186 props[
"version"] = std::string( 1, aVersion );
187 props[
"sigfig"] =
"3";
193 catch(
const std::exception& e )
195 aErrorMsg = wxString::Format(
"Export failed: %s", e.what() );
199 if( !wxFileExists( tempPath ) )
201 aErrorMsg =
"Export file was not created";
209 if( wxFileExists( xsdPath ) )
211 aErrorMsg = ValidateXmlWithXsd( tempPath, xsdPath );
212 return aErrorMsg.IsEmpty();
241 std::unique_ptr<BOARD> board = LoadBoard(
"issue3812.kicad_pcb" );
246 const BOARD_STACKUP& stackup = board->GetDesignSettings().GetStackupDescriptor();
250 wxString tempPath = CreateTempFile();
252 std::map<std::string, UTF8> props;
253 props[
"units"] =
"mm";
254 props[
"version"] =
"C";
255 props[
"sigfig"] =
"3";
257 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
264 "SurfaceFinish element should be present" );
266 "SurfaceFinish type should be ENIG-N" );
270 "COATING_TOP layer should be present" );
272 "COATING_BOTTOM layer should be present" );
273 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT(
"layerFunction=\"COATINGCOND\"" ) ),
274 "Coating layers should have layerFunction=COATINGCOND" );
287 std::unique_ptr<BOARD> board = LoadBoard(
"vme-wren.kicad_pcb" );
292 const BOARD_STACKUP& stackup = board->GetDesignSettings().GetStackupDescriptor();
296 wxString tempPath = CreateTempFile();
298 std::map<std::string, UTF8> props;
299 props[
"units"] =
"mm";
300 props[
"version"] =
"C";
301 props[
"sigfig"] =
"3";
303 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
309 "SurfaceFinish element should not be present for 'None' finish" );
313 "COATING_TOP layer should not be present" );
315 "COATING_BOTTOM layer should not be present" );
330 if( !m_xmllintAvailable )
332 BOOST_WARN_MESSAGE(
false,
"xmllint not available, skipping schema validation tests" );
336 wxString xsdPath = GetXsdPath(
'B' );
338 if( !wxFileExists( xsdPath ) )
340 BOOST_WARN_MESSAGE(
false,
"IPC-2581B1.xsd not found, skipping schema validation" );
344 for(
const std::string& boardFile : VALIDATION_TEST_BOARDS )
348 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
352 BOOST_WARN_MESSAGE(
false,
"Could not load board: " + boardFile );
357 bool valid = ExportAndValidate( board.get(),
'B', errorMsg );
359 BOOST_CHECK_MESSAGE( valid,
"IPC-2581B validation failed for " + boardFile +
": " + errorMsg );
373 if( !m_xmllintAvailable )
375 BOOST_WARN_MESSAGE(
false,
"xmllint not available, skipping schema validation tests" );
379 wxString xsdPath = GetXsdPath(
'C' );
381 if( !wxFileExists( xsdPath ) )
383 BOOST_WARN_MESSAGE(
false,
"IPC-2581C.xsd not found, skipping schema validation" );
387 for(
const std::string& boardFile : VALIDATION_TEST_BOARDS )
391 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
395 BOOST_WARN_MESSAGE(
false,
"Could not load board: " + boardFile );
400 bool valid = ExportAndValidate( board.get(),
'C', errorMsg );
402 BOOST_CHECK_MESSAGE( valid,
"IPC-2581C validation failed for " + boardFile +
": " + errorMsg );
417 static const std::vector<std::string> complexBoards = {
418 "intersectingzones.kicad_pcb",
419 "custom_pads.kicad_pcb",
422 for(
const std::string& boardFile : complexBoards )
426 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
430 BOOST_WARN_MESSAGE(
false,
"Could not load board: " + boardFile );
435 for(
char version : {
'B',
'C' } )
440 bool valid = ExportAndValidate( board.get(), version, errorMsg );
443 wxString::Format(
"Export/validation failed for %s version %c: %s",
444 boardFile, version, errorMsg ) );
462 std::unique_ptr<BOARD> board = LoadBoard(
"issue16658/issue16658.kicad_pcb" );
467 bool hasSmtPad =
false;
469 for(
FOOTPRINT* fp : board->Footprints() )
471 for(
PAD*
pad : fp->Pads() )
492 BOOST_REQUIRE_MESSAGE( hasSmtPad,
"Test board should have SMD pads" );
495 wxString tempPath = CreateTempFile();
497 std::map<std::string, UTF8> props;
498 props[
"units"] =
"mm";
499 props[
"version"] =
"C";
500 props[
"sigfig"] =
"3";
502 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
508 bool hasFMaskLayer = FileContainsPattern( tempPath, wxT(
"layerRef=\"F.Mask\"" ) )
509 || FileContainsPattern( tempPath, wxT(
"layerRef=\"TSM\"" ) );
512 "IPC-2581 export should contain F.Mask layer features for SMD pads" );
515 bool hasLayerFeature = FileContainsPattern( tempPath, wxT(
"<LayerFeature" ) );
516 BOOST_CHECK_MESSAGE( hasLayerFeature,
"IPC-2581 export should contain LayerFeature elements" );
529 std::unique_ptr<BOARD> board = LoadBoard(
"padstacks_complex.kicad_pcb" );
532 for(
char version : {
'B',
'C' } )
536 wxString tempPath = CreateTempFile();
538 std::map<std::string, UTF8> props;
539 props[
"units"] =
"mm";
540 props[
"version"] = std::string( 1, version );
541 props[
"sigfig"] =
"3";
543 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
547 "Empty refDes attribute found" );
549 "Empty RefDes/@name attribute found" );
551 "Empty PinRef/@componentRef attribute found" );
569 std::unique_ptr<BOARD> board = LoadBoard(
"issue12609.kicad_pcb" );
572 wxString tempPath = CreateTempFile();
574 std::map<std::string, UTF8> props;
575 props[
"units"] =
"mm";
576 props[
"version"] =
"C";
577 props[
"sigfig"] =
"3";
579 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
585 std::ifstream xmlFile( tempPath.ToStdString() );
588 std::string xmlContent( ( std::istreambuf_iterator<char>( xmlFile ) ),
589 std::istreambuf_iterator<char>() );
592 size_t c5Pos = xmlContent.find(
"refDes=\"C5\"" );
594 if( c5Pos == std::string::npos )
595 c5Pos = xmlContent.find(
"refDes=\"NOREF_" );
597 BOOST_REQUIRE_MESSAGE( c5Pos != std::string::npos,
598 "C5 component should exist in export" );
601 std::string c5Region = xmlContent.substr( c5Pos, 200 );
603 || c5Region.find(
"rotation=\"90.00\"" ) != std::string::npos,
604 "C5 component rotation should be 90, not inverted. Region: "
614 std::unique_ptr<BOARD> board = LoadBoard(
"issue12609.kicad_pcb" );
617 wxString tempPath = CreateTempFile();
619 std::map<std::string, UTF8> props;
620 props[
"units"] =
"mm";
621 props[
"version"] =
"C";
622 props[
"sigfig"] =
"3";
624 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
627 bool hasBom = FileContainsPattern( tempPath, wxT(
"<Bom " ) );
628 bool hasBomRef = FileContainsPattern( tempPath, wxT(
"<BomRef " ) );
633 "Content should have BomRef when Bom section is present" );
653 std::unique_ptr<BOARD> board = LoadBoard(
"test_copper_graphics.kicad_pcb" );
656 for(
char version : {
'B',
'C' } )
661 bool valid = ExportAndValidate( board.get(), version, errorMsg );
664 wxString::Format(
"Knockout text export should be schema-valid "
665 "(version %c): %s", version, errorMsg ) );
703 via->SetSecondaryDrillStartLayer(
F_Cu );
704 via->SetSecondaryDrillEndLayer(
In3_Cu );
708 wxString tempPath = CreateTempFile();
709 std::map<std::string, UTF8> props;
710 props[
"units"] =
"mm";
711 props[
"version"] =
"C";
712 props[
"sigfig"] =
"4";
714 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, &board, &props ) );
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" );
730 "Backdrill must convey layers through Property layerOrGroupRef" );
732 "Backdrill should not use schema-invalid startLayerRef attribute" );
734 "Backdrill should not use schema-invalid mustNotCutLayerRef attribute" );
736 "Backdrill should not use schema-invalid maxStubLength attribute" );
738 "Backdrill should not use the non-standard postMachining attribute" );
743 FileContainsPattern( tempPath, wxT(
"layerOrGroupRef=\"In4.Cu\"" ) ),
744 "Front backdrill must-not-cut layer should be In4.Cu" );
749 "Exporter should not emit a primary backdrill spec slot" );
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" );
783 PAD* thermalPad =
new PAD( fp );
791 fp->
Add( thermalPad );
794 PAD* pasteAperture =
new PAD( fp );
795 pasteAperture->
SetNumber( wxEmptyString );
805 fp->
Add( pasteAperture );
814 PAD* normalSmd =
new PAD( fp2 );
821 fp2->
Add( normalSmd );
825 PAD* implicitMaskSmd =
new PAD( fp2 );
826 implicitMaskSmd->
SetNumber( wxT(
"2" ) );
834 fp2->
Add( implicitMaskSmd );
836 wxString tempPath = CreateTempFile();
837 std::map<std::string, UTF8> props;
838 props[
"units"] =
"mm";
839 props[
"version"] =
"C";
840 props[
"sigfig"] =
"4";
842 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, &board, &props ) );
845 std::ifstream xmlFile( tempPath.ToStdString() );
848 std::string xml( ( std::istreambuf_iterator<char>( xmlFile ) ),
849 std::istreambuf_iterator<char>() );
852 const std::string pasteOpen =
"<LayerFeature layerRef=\"F.Paste\"";
853 size_t pasteStart = xml.find( pasteOpen );
855 if( pasteStart != std::string::npos )
857 size_t pasteEnd = xml.find(
"</LayerFeature>", pasteStart );
860 std::string pasteRegion = xml.substr( pasteStart, pasteEnd - pasteStart );
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" );
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 "
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" );
882 BOOST_FAIL(
"Expected an F.Paste LayerFeature for the explicitly-pasted control pad" );
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" );
892 size_t maskEnd = xml.find(
"</LayerFeature>", maskStart );
895 std::string maskRegion = xml.substr( maskStart, maskEnd - maskStart );
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" );
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" );
constexpr EDA_IU_SCALE pcbIUScale
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.
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Removes an item from the container.
void SetCopperLayerCount(int aCount)
BOARD_DESIGN_SETTINGS & GetDesignSettings() const
LSET is a set of PCB_LAYER_IDs.
static constexpr PCB_LAYER_ID ALL_LAYERS
! Temporary layer identifier to identify code that is not padstack-aware
void SetAttribute(PAD_ATTRIB aAttribute)
void SetShape(PCB_LAYER_ID aLayer, PAD_SHAPE aShape)
Set the new shape of this pad.
void SetProperty(PAD_PROP aProperty)
void SetNumber(const wxString &aNumber)
Set the pad number (note that it can be alphanumeric, such as the array reference "AA12").
void SetPosition(const VECTOR2I &aPos) override
void SetSize(PCB_LAYER_ID aLayer, const VECTOR2I &aSize)
void SetLayerSet(const LSET &aLayers) override
A #PLUGIN derivation for saving and loading Pcbnew s-expression formatted files.
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)
@ HEATSINK
a pad used as heat sink, usually in SMD footprints
wxString GetXsdPath(char aVersion)
PCB_IO_IPC2581 m_ipc2581Plugin
bool ExportAndValidate(BOARD *aBoard, char aVersion, wxString &aErrorMsg)
std::vector< wxString > m_tempFiles
PCB_IO_KICAD_SEXPR m_kicadPlugin
~IPC2581_EXPORT_FIXTURE()
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)
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