50#include <wx/filename.h>
51#include <wx/process.h>
52#include <wx/txtstrm.h>
66bool IsXmllintAvailable()
70 int result = wxExecute(
"xmllint --version", output, errors, wxEXEC_SYNC );
79wxString ValidateXmlWithXsd(
const wxString& aXmlPath,
const wxString& aXsdPath )
81 wxString cmd = wxString::Format(
"xmllint --noout --schema \"%s\" \"%s\"",
86 int result = wxExecute( cmd, output, errors, wxEXEC_SYNC );
92 for(
const wxString& line : errors )
93 errorMsg += line +
"\n";
105bool FileContainsPattern(
const wxString& aFilePath,
const wxString& aPattern )
107 std::ifstream file( aFilePath.ToStdString() );
109 if( !file.is_open() )
112 std::stringstream buffer;
113 buffer << file.rdbuf();
114 std::string content = buffer.str();
116 return content.find( aPattern.ToStdString() ) != std::string::npos;
120bool XmlRegionHasCoordinateNear(
const std::string& aRegion,
char aAxis,
double aExpected,
double aTolerance )
122 std::regex coordinateRegex( std::string( 1, aAxis ) +
"=\"(-?[0-9]+(?:\\.[0-9]+)?)\"" );
124 for( std::sregex_iterator it( aRegion.begin(), aRegion.end(), coordinateRegex ),
end; it !=
end; ++it )
126 if(
std::abs( std::stod( ( *it )[1].str() ) - aExpected ) <= aTolerance )
138static const std::vector<std::string> VALIDATION_TEST_BOARDS = {
139 "custom_pads.kicad_pcb",
140 "notched_zones.kicad_pcb",
142 "tracks_arcs_vias.kicad_pcb",
143 "issue7241.kicad_pcb",
144 "issue10906.kicad_pcb",
145 "issue22798.kicad_pcb",
146 "padstacks_complex.kicad_pcb",
147 "issue12609.kicad_pcb",
148 "issue22794.kicad_pcb",
166 if( wxFileExists(
path ) )
167 wxRemoveFile(
path );
173 wxString
path = wxFileName::CreateTempFileName( wxT(
"kicad_ipc2581_test" ) );
175 if( !aSuffix.IsEmpty() )
178 path += wxT(
".xml" );
186 wxString filename = ( aVersion ==
'C' ) ? wxT(
"IPC-2581C.xsd" ) : wxT(
"IPC-2581B1.xsd" );
190 std::unique_ptr<BOARD>
LoadBoard(
const std::string& aRelativePath )
198 const std::string& aMode = std::string(),
199 const std::string& aRefDes = std::string(),
200 const std::string& aSections = std::string() )
204 std::map<std::string, UTF8> props;
205 props[
"units"] =
"mm";
206 props[
"version"] = std::string( 1, aVersion );
207 props[
"sigfig"] =
"3";
210 props[
"mode"] = aMode;
212 if( !aRefDes.empty() )
214 props[
"refdes"] = aRefDes;
215 props[
"netnames"] =
"anonymize";
218 if( !aSections.empty() )
219 props[
"sections"] = aSections;
225 catch(
const std::exception& e )
227 aErrorMsg = wxString::Format(
"Export failed: %s", e.what() );
231 if( !wxFileExists( tempPath ) )
233 aErrorMsg =
"Export file was not created";
241 if( wxFileExists( xsdPath ) )
243 aErrorMsg = ValidateXmlWithXsd( tempPath, xsdPath );
244 return aErrorMsg.IsEmpty();
273 std::unique_ptr<BOARD> board = LoadBoard(
"issue3812.kicad_pcb" );
278 const BOARD_STACKUP& stackup = board->GetDesignSettings().GetStackupDescriptor();
282 wxString tempPath = CreateTempFile();
284 std::map<std::string, UTF8> props;
285 props[
"units"] =
"mm";
286 props[
"version"] =
"C";
287 props[
"sigfig"] =
"3";
289 m_ipc2581Plugin.SaveBoard( tempPath, *board, &props );
295 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT(
"<SurfaceFinish" ) ),
296 "SurfaceFinish element should be present" );
297 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT(
"type=\"ENIG-N\"" ) ),
298 "SurfaceFinish type should be ENIG-N" );
301 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT(
"COATING_TOP" ) ),
302 "COATING_TOP layer should be present" );
303 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT(
"COATING_BOTTOM" ) ),
304 "COATING_BOTTOM layer should be present" );
305 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT(
"layerFunction=\"COATINGCOND\"" ) ),
306 "Coating layers should have layerFunction=COATINGCOND" );
319 std::unique_ptr<BOARD> board = LoadBoard(
"vme-wren.kicad_pcb" );
324 const BOARD_STACKUP& stackup = board->GetDesignSettings().GetStackupDescriptor();
328 wxString tempPath = CreateTempFile();
330 std::map<std::string, UTF8> props;
331 props[
"units"] =
"mm";
332 props[
"version"] =
"C";
333 props[
"sigfig"] =
"3";
335 m_ipc2581Plugin.SaveBoard( tempPath, *board, &props );
340 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT(
"<SurfaceFinish" ) ),
341 "SurfaceFinish element should not be present for 'None' finish" );
344 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT(
"COATING_TOP" ) ),
345 "COATING_TOP layer should not be present" );
346 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT(
"COATING_BOTTOM" ) ),
347 "COATING_BOTTOM layer should not be present" );
362 if( !m_xmllintAvailable )
364 BOOST_WARN_MESSAGE(
false,
"xmllint not available, skipping schema validation tests" );
368 wxString xsdPath = GetXsdPath(
'B' );
370 if( !wxFileExists( xsdPath ) )
372 BOOST_WARN_MESSAGE(
false,
"IPC-2581B1.xsd not found, skipping schema validation" );
376 for(
const std::string& boardFile : VALIDATION_TEST_BOARDS )
380 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
384 BOOST_WARN_MESSAGE(
false,
"Could not load board: " + boardFile );
389 bool valid = ExportAndValidate( *board,
'B', errorMsg );
391 BOOST_CHECK_MESSAGE( valid,
"IPC-2581B validation failed for " + boardFile +
": " + errorMsg );
405 if( !m_xmllintAvailable )
407 BOOST_WARN_MESSAGE(
false,
"xmllint not available, skipping schema validation tests" );
411 wxString xsdPath = GetXsdPath(
'C' );
413 if( !wxFileExists( xsdPath ) )
415 BOOST_WARN_MESSAGE(
false,
"IPC-2581C.xsd not found, skipping schema validation" );
419 for(
const std::string& boardFile : VALIDATION_TEST_BOARDS )
423 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
427 BOOST_WARN_MESSAGE(
false,
"Could not load board: " + boardFile );
432 bool valid = ExportAndValidate( *board,
'C', errorMsg );
434 BOOST_CHECK_MESSAGE( valid,
"IPC-2581C validation failed for " + boardFile +
": " + errorMsg );
452 if( !m_xmllintAvailable )
454 BOOST_WARN_MESSAGE(
false,
"xmllint not available, skipping schema validation tests" );
458 static const std::vector<std::string> modes = {
"userdef",
"bom",
"stackup",
"fabrication",
459 "assembly",
"test",
"stencil" };
462 static const std::vector<std::string> boards = {
"padstacks_complex.kicad_pcb",
463 "custom_pads.kicad_pcb",
464 "tracks_arcs_vias.kicad_pcb" };
466 for(
const std::string& boardFile : boards )
468 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
472 BOOST_WARN_MESSAGE(
false,
"Could not load board: " + boardFile );
476 for(
const std::string& mode : modes )
478 for(
char version : {
'B',
'C' } )
480 if( !wxFileExists( GetXsdPath( version ) ) )
483 for(
const std::string& refdes : { std::string(), std::string(
"omit" ) } )
486 <<
" Version: " << version
487 <<
" RefDes: " << ( refdes.empty() ?
"include" : refdes ) )
490 bool valid = ExportAndValidate( *board, version, errorMsg, mode,
493 BOOST_CHECK_MESSAGE( valid,
"validation failed: " + errorMsg );
508 if( !m_xmllintAvailable )
510 BOOST_WARN_MESSAGE(
false,
"xmllint not available, skipping schema validation tests" );
515 static const std::vector<std::string> sectionKeys = {
"AOU",
"ACOU",
"KACOU",
"ABOU" };
517 std::unique_ptr<BOARD> board = LoadBoard(
"padstacks_complex.kicad_pcb" );
521 for(
const std::string& sections : sectionKeys )
523 for(
char version : {
'B',
'C' } )
525 if( !wxFileExists( GetXsdPath( version ) ) )
531 bool valid = ExportAndValidate( *board, version, errorMsg,
"userdef",
532 std::string(), sections );
535 if( version ==
'B' && sections.find(
'C' ) == std::string::npos )
536 BOOST_CHECK_MESSAGE( !valid,
"IPC-2581B accepted components with no package" );
538 BOOST_CHECK_MESSAGE( valid,
"validation failed: " + errorMsg );
548 static const std::vector<std::string> complexBoards = {
549 "intersectingzones.kicad_pcb",
550 "custom_pads.kicad_pcb",
553 for(
const std::string& boardFile : complexBoards )
557 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
561 BOOST_WARN_MESSAGE(
false,
"Could not load board: " + boardFile );
566 for(
char version : {
'B',
'C' } )
571 bool valid = ExportAndValidate( *board, version, errorMsg );
573 BOOST_CHECK_MESSAGE( valid,
574 wxString::Format(
"Export/validation failed for %s version %c: %s",
575 boardFile, version, errorMsg ) );
600 wxString tempPath = CreateTempFile();
602 std::map<std::string, UTF8> props;
603 props[
"units"] =
"mm";
604 props[
"version"] =
"C";
605 props[
"sigfig"] =
"6";
607 m_ipc2581Plugin.SaveBoard( tempPath, board, &props );
610 BOOST_CHECK( FileContainsPattern( tempPath, wxT(
"<Line " ) ) );
611 BOOST_CHECK( !FileContainsPattern( tempPath, wxT(
"<Arc " ) ) );
630 textbox->
SetText( wxT(
"IPC textbox" ) );
638 board.
Add( textbox );
640 wxString tempPath = CreateTempFile();
642 std::map<std::string, UTF8> props;
643 props[
"units"] =
"mm";
644 props[
"version"] =
"C";
645 props[
"sigfig"] =
"6";
647 m_ipc2581Plugin.SaveBoard( tempPath, board, &props );
650 std::ifstream xmlFile( tempPath.ToStdString() );
653 std::string xmlContent( ( std::istreambuf_iterator<char>( xmlFile ) ), std::istreambuf_iterator<char>() );
654 size_t textStart = xmlContent.find(
"value=\"IPC textbox\"" );
655 BOOST_REQUIRE_MESSAGE( textStart != std::string::npos,
"Export should contain the text box feature" );
657 size_t setEnd = xmlContent.find(
"</Set>", textStart );
660 std::string textFeature = xmlContent.substr( textStart, setEnd - textStart );
662 BOOST_CHECK_MESSAGE( XmlRegionHasCoordinateNear( textFeature,
'x', 100.0, 5.0 ),
663 "Text box geometry should use its X drawing position" );
664 BOOST_CHECK_MESSAGE( XmlRegionHasCoordinateNear( textFeature,
'y', -50.0, 5.0 ),
665 "Text box geometry should use its Y drawing position" );
679 std::unique_ptr<BOARD> board = LoadBoard(
"issue16658/issue16658.kicad_pcb" );
684 bool hasSmtPad =
false;
686 for(
FOOTPRINT* fp : board->Footprints() )
688 for(
PAD*
pad : fp->Pads() )
709 BOOST_REQUIRE_MESSAGE( hasSmtPad,
"Test board should have SMD pads" );
712 wxString tempPath = CreateTempFile();
714 std::map<std::string, UTF8> props;
715 props[
"units"] =
"mm";
716 props[
"version"] =
"C";
717 props[
"sigfig"] =
"3";
719 m_ipc2581Plugin.SaveBoard( tempPath, *board, &props );
725 bool hasFMaskLayer = FileContainsPattern( tempPath, wxT(
"layerRef=\"F.Mask\"" ) )
726 || FileContainsPattern( tempPath, wxT(
"layerRef=\"TSM\"" ) );
728 BOOST_CHECK_MESSAGE( hasFMaskLayer,
729 "IPC-2581 export should contain F.Mask layer features for SMD pads" );
732 bool hasLayerFeature = FileContainsPattern( tempPath, wxT(
"<LayerFeature" ) );
733 BOOST_CHECK_MESSAGE( hasLayerFeature,
"IPC-2581 export should contain LayerFeature elements" );
746 std::unique_ptr<BOARD> board = LoadBoard(
"padstacks_complex.kicad_pcb" );
749 for(
char version : {
'B',
'C' } )
753 wxString tempPath = CreateTempFile();
755 std::map<std::string, UTF8> props;
756 props[
"units"] =
"mm";
757 props[
"version"] = std::string( 1, version );
758 props[
"sigfig"] =
"3";
760 m_ipc2581Plugin.SaveBoard( tempPath, *board, &props );
763 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT(
"refDes=\"\"" ) ),
764 "Empty refDes attribute found" );
765 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT(
"<RefDes name=\"\"" ) ),
766 "Empty RefDes/@name attribute found" );
767 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT(
"componentRef=\"\"" ) ),
768 "Empty PinRef/@componentRef attribute found" );
786 std::unique_ptr<BOARD> board = LoadBoard(
"issue12609.kicad_pcb" );
789 wxString tempPath = CreateTempFile();
791 std::map<std::string, UTF8> props;
792 props[
"units"] =
"mm";
793 props[
"version"] =
"C";
794 props[
"sigfig"] =
"3";
796 m_ipc2581Plugin.SaveBoard( tempPath, *board, &props );
802 std::ifstream xmlFile( tempPath.ToStdString() );
805 std::string xmlContent( ( std::istreambuf_iterator<char>( xmlFile ) ),
806 std::istreambuf_iterator<char>() );
809 size_t c5Pos = xmlContent.find(
"refDes=\"C5\"" );
811 if( c5Pos == std::string::npos )
812 c5Pos = xmlContent.find(
"refDes=\"NOREF_" );
814 BOOST_REQUIRE_MESSAGE( c5Pos != std::string::npos,
815 "C5 component should exist in export" );
818 std::string c5Region = xmlContent.substr( c5Pos, 200 );
819 BOOST_CHECK_MESSAGE( c5Region.find(
"rotation=\"90.0\"" ) != std::string::npos
820 || c5Region.find(
"rotation=\"90.00\"" ) != std::string::npos,
821 "C5 component rotation should be 90, not inverted. Region: "
831 std::unique_ptr<BOARD> board = LoadBoard(
"issue12609.kicad_pcb" );
834 wxString tempPath = CreateTempFile();
836 std::map<std::string, UTF8> props;
837 props[
"units"] =
"mm";
838 props[
"version"] =
"C";
839 props[
"sigfig"] =
"3";
841 m_ipc2581Plugin.SaveBoard( tempPath, *board, &props );
844 bool hasBom = FileContainsPattern( tempPath, wxT(
"<Bom " ) );
845 bool hasBomRef = FileContainsPattern( tempPath, wxT(
"<BomRef " ) );
849 BOOST_CHECK_MESSAGE( hasBomRef,
850 "Content should have BomRef when Bom section is present" );
870 std::unique_ptr<BOARD> board = LoadBoard(
"test_copper_graphics.kicad_pcb" );
873 for(
char version : {
'B',
'C' } )
878 bool valid = ExportAndValidate( *board, version, errorMsg );
880 BOOST_CHECK_MESSAGE( valid,
881 wxString::Format(
"Knockout text export should be schema-valid "
882 "(version %c): %s", version, errorMsg ) );
921 via->SetSecondaryDrillStartLayer(
F_Cu );
922 via->SetSecondaryDrillEndLayer(
In3_Cu );
926 wxString tempPath = CreateTempFile();
927 std::map<std::string, UTF8> props;
928 props[
"units"] =
"mm";
929 props[
"version"] =
"C";
930 props[
"sigfig"] =
"4";
932 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, board, &props ) );
936 FileContainsPattern( tempPath, wxT(
"<Backdrill type=\"START_LAYER\"" ) ),
937 "Backdrill spec should declare a START_LAYER child" );
939 FileContainsPattern( tempPath, wxT(
"<Backdrill type=\"MUST_NOT_CUT_LAYER\"" ) ),
940 "Backdrill spec should declare a MUST_NOT_CUT_LAYER child" );
942 FileContainsPattern( tempPath, wxT(
"<Backdrill type=\"MAX_STUB_LENGTH\"" ) ),
943 "Backdrill spec should declare a MAX_STUB_LENGTH child" );
947 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT(
"layerOrGroupRef=" ) ),
948 "Backdrill must convey layers through Property layerOrGroupRef" );
949 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT(
"startLayerRef=" ) ),
950 "Backdrill should not use schema-invalid startLayerRef attribute" );
951 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT(
"mustNotCutLayerRef=" ) ),
952 "Backdrill should not use schema-invalid mustNotCutLayerRef attribute" );
953 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT(
"maxStubLength=" ) ),
954 "Backdrill should not use schema-invalid maxStubLength attribute" );
955 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT(
"postMachining=" ) ),
956 "Backdrill should not use the non-standard postMachining attribute" );
961 FileContainsPattern( tempPath, wxT(
"layerOrGroupRef=\"In4.Cu\"" ) ),
962 "Front backdrill must-not-cut layer should be In4.Cu" );
966 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT(
"BD_1C" ) ),
967 "Exporter should not emit a primary backdrill spec slot" );
972 FileContainsPattern( tempPath, wxT(
"<Backdrill type=\"OTHER\"" ) ),
973 "Post-machining hint should produce a Backdrill type=OTHER child" );
975 FileContainsPattern( tempPath, wxT(
"comment=\"post-machining=COUNTERSINK\"" ) ),
976 "OTHER Backdrill should carry the post-machining comment" );
1001 PAD* thermalPad =
new PAD( fp );
1009 fp->
Add( thermalPad );
1012 PAD* pasteAperture =
new PAD( fp );
1014 pasteAperture->
SetNumber( wxEmptyString );
1022 fp->
Add( pasteAperture );
1031 PAD* normalSmd =
new PAD( fp2 );
1038 fp2->
Add( normalSmd );
1042 PAD* implicitMaskSmd =
new PAD( fp2 );
1044 implicitMaskSmd->
SetNumber( wxT(
"2" ) );
1050 fp2->
Add( implicitMaskSmd );
1052 wxString tempPath = CreateTempFile();
1053 std::map<std::string, UTF8> props;
1054 props[
"units"] =
"mm";
1055 props[
"version"] =
"C";
1056 props[
"sigfig"] =
"4";
1058 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, board, &props ) );
1061 std::ifstream xmlFile( tempPath.ToStdString() );
1064 std::string xml( ( std::istreambuf_iterator<char>( xmlFile ) ),
1065 std::istreambuf_iterator<char>() );
1068 const std::string pasteOpen =
"<LayerFeature layerRef=\"F.Paste\"";
1069 size_t pasteStart = xml.find( pasteOpen );
1071 if( pasteStart != std::string::npos )
1073 size_t pasteEnd = xml.find(
"</LayerFeature>", pasteStart );
1076 std::string pasteRegion = xml.substr( pasteStart, pasteEnd - pasteStart );
1079 BOOST_CHECK_MESSAGE(
1080 pasteRegion.find(
"<PinRef componentRef=\"U1\" pin=\"33\"" ) == std::string::npos,
1081 "Copper-only thermal pad U1.33 must not appear on F.Paste layer feature" );
1084 BOOST_CHECK_MESSAGE(
1085 pasteRegion.find(
"<PinRef componentRef=\"R1\" pin=\"1\"" ) != std::string::npos,
1086 "Normal SMD pad with explicit F.Paste in layer set should still emit a paste "
1090 BOOST_CHECK_MESSAGE(
1091 pasteRegion.find(
"<PinRef componentRef=\"R1\" pin=\"2\"" ) == std::string::npos,
1092 "Copper-only SMD pad R1.2 must not appear on F.Paste layer feature" );
1098 BOOST_FAIL(
"Expected an F.Paste LayerFeature for the explicitly-pasted control pad" );
1103 const std::string maskOpen =
"<LayerFeature layerRef=\"F.Mask\"";
1104 size_t maskStart = xml.find( maskOpen );
1105 BOOST_REQUIRE_MESSAGE( maskStart != std::string::npos,
1106 "F.Mask LayerFeature should still be emitted for SMD copper pads" );
1108 size_t maskEnd = xml.find(
"</LayerFeature>", maskStart );
1111 std::string maskRegion = xml.substr( maskStart, maskEnd - maskStart );
1113 BOOST_CHECK_MESSAGE(
1114 maskRegion.find(
"<PinRef componentRef=\"U1\" pin=\"33\"" ) != std::string::npos,
1115 "Thermal pad with explicit F.Mask should appear on F.Mask layer feature" );
1119 BOOST_CHECK_MESSAGE(
1120 maskRegion.find(
"<PinRef componentRef=\"R1\" pin=\"2\"" ) != std::string::npos,
1121 "SMD copper pad without explicit F.Mask must get an implicit F.Mask opening" );
1131 const std::string open =
"<LayerFeature layerRef=\"" + aLayerRef +
"\"";
1132 size_t start = aXml.find( open );
1134 if( start == std::string::npos )
1135 return std::string();
1137 size_t end = aXml.find(
"</LayerFeature>", start );
1139 if(
end == std::string::npos )
1140 return std::string();
1142 return aXml.substr( start,
end - start );
1167 wxString tempPath = CreateTempFile();
1168 std::map<std::string, UTF8> props;
1169 props[
"units"] =
"mm";
1170 props[
"version"] =
"C";
1171 props[
"sigfig"] =
"4";
1173 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, board, &props ) );
1179 size_t rectPos = xml.find(
"<RectRound" );
1180 BOOST_REQUIRE_MESSAGE( rectPos != std::string::npos,
"Export should contain a RectRound" );
1182 std::string rectNode = xml.substr( rectPos, xml.find(
'>', rectPos ) - rectPos );
1184 BOOST_CHECK_MESSAGE( rectNode.find(
"radius=\"0.750\"" ) != std::string::npos,
1185 "Rounded gr_rect must export its 0.75 mm corner radius. Node: "
1187 BOOST_CHECK_MESSAGE( rectNode.find(
"upperRight=\"true\"" ) != std::string::npos
1188 && rectNode.find(
"lowerLeft=\"true\"" ) != std::string::npos,
1189 "Rounded gr_rect must set its corner flags. Node: " + rectNode );
1190 BOOST_CHECK_MESSAGE( rectNode.find(
"radius=\"0.025\"" ) == std::string::npos,
1191 "Corner radius must not collapse to stroke_width/2. Node: " + rectNode );
1214 pad->SetNumber( wxT(
"1" ) );
1222 wxString tempPath = CreateTempFile();
1223 std::map<std::string, UTF8> props;
1224 props[
"units"] =
"mm";
1225 props[
"version"] =
"C";
1226 props[
"sigfig"] =
"4";
1228 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, board, &props ) );
1235 BOOST_CHECK_MESSAGE( fMask.find(
"<PinRef componentRef=\"J29\" pin=\"1\"" ) == std::string::npos,
1236 "Back-only masked pad must not appear on F.Mask layer feature" );
1239 BOOST_CHECK_MESSAGE( bMask.find(
"<PinRef componentRef=\"J29\" pin=\"1\"" ) != std::string::npos,
1240 "Back-only masked pad should appear on B.Mask layer feature" );
1263 pad->SetNumber( wxT(
"1" ) );
1269 pad->SetLocalSolderMaskMargin(
pcbIUScale.mmToIU( -0.05 ) );
1272 wxString tempPath = CreateTempFile();
1273 std::map<std::string, UTF8> props;
1274 props[
"units"] =
"mm";
1275 props[
"version"] =
"C";
1276 props[
"sigfig"] =
"4";
1278 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, board, &props ) );
1285 BOOST_CHECK_MESSAGE( xml.find(
"radius=\"0.10\"" ) != std::string::npos,
1286 "Copper roundrect should keep its 0.10 mm radius" );
1287 BOOST_CHECK_MESSAGE( xml.find(
"radius=\"0.050\"" ) != std::string::npos,
1288 "Mask roundrect aperture must shrink its radius to 0.05 mm" );
1289 BOOST_CHECK_MESSAGE( xml.find(
"width=\"0.350\"" ) != std::string::npos,
1290 "Mask roundrect aperture should be 0.35 mm wide" );
1324 wxString tempPath = CreateTempFile();
1325 std::map<std::string, UTF8> props;
1326 props[
"units"] =
"mm";
1327 props[
"version"] =
"C";
1328 props[
"sigfig"] =
"4";
1330 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, board, &props ) );
1339 BOOST_CHECK_MESSAGE( fCu.find(
"UserPrimitiveRef" ) != std::string::npos,
1340 "Footprint graphic should be present on F.Cu" );
1341 BOOST_CHECK_MESSAGE( fMask.find(
"UserPrimitiveRef" ) != std::string::npos,
1342 "Multi-layer footprint graphic must also appear on F.Mask" );
1366 pad->SetNumber( wxT(
"1" ) );
1374 wxString tempPath = CreateTempFile();
1375 std::map<std::string, UTF8> props;
1376 props[
"units"] =
"mm";
1377 props[
"version"] =
"C";
1378 props[
"sigfig"] =
"4";
1380 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, board, &props ) );
1383 std::ifstream xmlFile( tempPath.ToStdString() );
1386 std::string xml( ( std::istreambuf_iterator<char>( xmlFile ) ),
1387 std::istreambuf_iterator<char>() );
1391 const std::string maskRef =
"<PadstackPadDef layerRef=\"F.Mask\"";
1392 size_t maskDefPos = xml.find( maskRef );
1393 BOOST_REQUIRE_MESSAGE( maskDefPos != std::string::npos,
1394 "F.Mask padstack pad definition should be present" );
1396 size_t refPos = xml.find(
"StandardPrimitiveRef id=\"", maskDefPos );
1398 refPos += std::string(
"StandardPrimitiveRef id=\"" ).size();
1399 size_t refEnd = xml.find(
'"', refPos );
1400 std::string maskPrimId = xml.substr( refPos, refEnd - refPos );
1402 std::string entry =
"<EntryStandard id=\"" + maskPrimId +
"\"";
1403 size_t entryPos = xml.find( entry );
1404 BOOST_REQUIRE_MESSAGE( entryPos != std::string::npos,
1405 "Mask primitive EntryStandard should be defined" );
1407 size_t diaPos = xml.find(
"diameter=\"", entryPos );
1409 diaPos += std::string(
"diameter=\"" ).size();
1410 size_t diaEnd = xml.find(
'"', diaPos );
1411 double diameter = std::stod( xml.substr( diaPos, diaEnd - diaPos ) );
1413 BOOST_CHECK_MESSAGE(
std::abs( diameter - 2.0 ) < 1e-4,
1414 "F.Mask primitive diameter should be 2.0 mm (1.0 mm pad + 0.5 mm "
1415 "margin per side), got " << diameter );
1424 static const std::vector<std::string> boards = {
1425 "ipc2581/dielectric-sublayer.kicad_pcb",
1426 "ipc2581/edgecuts-circles-only.kicad_pcb",
1427 "ipc2581/filled-silk-circle.kicad_pcb",
1428 "ipc2581/filled-tented-via.kicad_pcb",
1429 "ipc2581/textbox-border.kicad_pcb",
1430 "ipc2581/thirty-copper-layers.kicad_pcb",
1431 "ipc2581/whitespace-text.kicad_pcb",
1434 for(
const std::string& boardFile : boards )
1438 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
1442 bool valid = ExportAndValidate( *board,
'C', errorMsg );
1444 BOOST_CHECK_MESSAGE( valid,
"IPC-2581C validation failed for " + boardFile +
": " + errorMsg );
1456 std::unique_ptr<BOARD> board = LoadBoard(
"ipc2581/edgecuts-circles-only.kicad_pcb" );
1459 wxString tempPath = CreateTempFile();
1460 std::map<std::string, UTF8> props;
1461 props[
"units"] =
"mm";
1462 props[
"version"] =
"C";
1463 props[
"sigfig"] =
"3";
1465 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, *board, &props ) );
1468 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT(
"<Profile" ) ),
1469 "A board with no closed outline must not write a Profile" );
1478 std::unique_ptr<BOARD> board = LoadBoard(
"ipc2581/filled-tented-via.kicad_pcb" );
1481 wxString tempPath = CreateTempFile();
1482 std::map<std::string, UTF8> props;
1483 props[
"units"] =
"mm";
1484 props[
"version"] =
"C";
1485 props[
"sigfig"] =
"3";
1487 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, *board, &props ) );
1493 BOOST_REQUIRE_MESSAGE( !region.empty(),
"Export should contain the F.Cu_2 process layer" );
1495 BOOST_CHECK_MESSAGE( region.find(
"<Set" ) != std::string::npos,
1496 "Process-layer via pads must be wrapped in a Set" );
1497 BOOST_CHECK_MESSAGE( region.find(
"x=\"120.0\" y=\"-115.0\"" ) != std::string::npos,
1498 "Process-layer via pad must be at the via position" );
1499 BOOST_CHECK_MESSAGE( region.find(
"x=\"0.0\" y=\"0.0\"" ) == std::string::npos,
1500 "Process-layer via pad must not be at (0,0)" );
constexpr EDA_IU_SCALE pcbIUScale
General utilities for PCB file IO for QA programs.
virtual void SetNet(NETINFO_ITEM *aNetInfo)
Set a NET_INFO object for the item.
void SetLayer(PCB_LAYER_ID aLayer) override
Set the layer this item is on.
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
void SetCornerRadius(int aRadius)
virtual void SetFilled(bool aFlag)
void SetVertJustify(GR_TEXT_V_ALIGN_T aType)
virtual void SetText(const wxString &aText)
void SetHorizJustify(GR_TEXT_H_ALIGN_T aType)
LSET is a set of PCB_LAYER_IDs.
Handle the data for a net.
@ NORMAL
Shape is the same on all layers.
static constexpr PCB_LAYER_ID ALL_LAYERS
! The layer identifier to use for the single defintion on normal padstacks
void SetAttribute(PAD_ATTRIB aAttribute)
void SetPadstackMode(PADSTACK::MODE aMode)
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
void SetMid(const VECTOR2I &aMid)
A #PLUGIN derivation for saving and loading Pcbnew s-expression formatted files.
virtual void SetLayerSet(const LSET &aLayers) override
void SetEnd(const VECTOR2I &aEnd) override
void SetPolyShape(const SHAPE_POLY_SET &aShape) override
void SetLayer(PCB_LAYER_ID aLayer) override
Set the layer this item is on.
void SetStart(const VECTOR2I &aStart) override
void SetStroke(const STROKE_PARAMS &aStroke) override
void SetBorderEnabled(bool enabled)
void SetMarginTop(int aTop)
void SetMarginLeft(int aLeft)
void SetTextThickness(int aWidth) override
The TextThickness is that set by the user.
void SetTextSize(VECTOR2I aNewSize, bool aEnforceMinTextSize=true) override
void SetEnd(const VECTOR2I &aEnd)
void SetStart(const VECTOR2I &aStart)
virtual void SetWidth(int aWidth)
Represent a set of closed polygons.
int Append(int x, int y, int aOutline=-1, int aHole=-1, bool aAllowDuplication=false)
Appends a vertex at the end of the given outline/hole (default: the last outline)
int NewOutline()
Creates a new empty polygon in the set and returns its index.
Simple container to manage line stroke parameters.
@ RECTANGLE
Use RECTANGLE instead of RECT to avoid collision in a Windows header.
std::string GetPcbnewTestDataDir()
Utility which returns a path to the data directory where the test board files are stored.
std::string LoadStringData(const wxString &aPath)
Load the contents of a file into a string.
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
@ SMD
Smd pad, appears on the solder paste layer (default)
@ PTH
Plated through hole pad.
@ 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, const std::string &aMode=std::string(), const std::string &aRefDes=std::string(), const std::string &aSections=std::string())
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)
static std::string LayerFeatureRegion(const std::string &aXml, const std::string &aLayerRef)
Extract the text of the first LayerFeature block for a given layer reference.
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