47#include <wx/filename.h>
48#include <wx/process.h>
49#include <wx/txtstrm.h>
62bool IsXmllintAvailable()
66 int result = wxExecute(
"xmllint --version",
output, errors, wxEXEC_SYNC );
75wxString ValidateXmlWithXsd(
const wxString& aXmlPath,
const wxString& aXsdPath )
77 wxString cmd = wxString::Format(
"xmllint --noout --schema \"%s\" \"%s\"",
82 int result = wxExecute( cmd,
output, errors, wxEXEC_SYNC );
88 for(
const wxString& line : errors )
89 errorMsg += line +
"\n";
101bool FileContainsPattern(
const wxString& aFilePath,
const wxString& aPattern )
103 std::ifstream file( aFilePath.ToStdString() );
105 if( !file.is_open() )
108 std::stringstream buffer;
109 buffer << file.rdbuf();
110 std::string content = buffer.str();
112 return content.find( aPattern.ToStdString() ) != std::string::npos;
120static const std::vector<std::string> VALIDATION_TEST_BOARDS = {
121 "custom_pads.kicad_pcb",
122 "notched_zones.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",
148 if( wxFileExists(
path ) )
149 wxRemoveFile(
path );
155 wxString
path = wxFileName::CreateTempFileName( wxT(
"kicad_ipc2581_test" ) );
157 if( !aSuffix.IsEmpty() )
160 path += wxT(
".xml" );
168 wxString filename = ( aVersion ==
'C' ) ? wxT(
"IPC-2581C.xsd" ) : wxT(
"IPC-2581B1.xsd" );
172 std::unique_ptr<BOARD>
LoadBoard(
const std::string& aRelativePath )
175 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
177 m_kicadPlugin.LoadBoard( fullPath, board.get(),
nullptr,
nullptr );
186 std::map<std::string, UTF8> props;
187 props[
"units"] =
"mm";
188 props[
"version"] = std::string( 1, aVersion );
189 props[
"sigfig"] =
"3";
195 catch(
const std::exception& e )
197 aErrorMsg = wxString::Format(
"Export failed: %s", e.what() );
201 if( !wxFileExists( tempPath ) )
203 aErrorMsg =
"Export file was not created";
211 if( wxFileExists( xsdPath ) )
213 aErrorMsg = ValidateXmlWithXsd( tempPath, xsdPath );
214 return aErrorMsg.IsEmpty();
243 std::unique_ptr<BOARD> board = LoadBoard(
"issue3812.kicad_pcb" );
248 const BOARD_STACKUP& stackup = board->GetDesignSettings().GetStackupDescriptor();
252 wxString tempPath = CreateTempFile();
254 std::map<std::string, UTF8> props;
255 props[
"units"] =
"mm";
256 props[
"version"] =
"C";
257 props[
"sigfig"] =
"3";
259 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
266 "SurfaceFinish element should be present" );
268 "SurfaceFinish type should be ENIG-N" );
272 "COATING_TOP layer should be present" );
274 "COATING_BOTTOM layer should be present" );
275 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT(
"layerFunction=\"COATINGCOND\"" ) ),
276 "Coating layers should have layerFunction=COATINGCOND" );
289 std::unique_ptr<BOARD> board = LoadBoard(
"vme-wren.kicad_pcb" );
294 const BOARD_STACKUP& stackup = board->GetDesignSettings().GetStackupDescriptor();
298 wxString tempPath = CreateTempFile();
300 std::map<std::string, UTF8> props;
301 props[
"units"] =
"mm";
302 props[
"version"] =
"C";
303 props[
"sigfig"] =
"3";
305 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
311 "SurfaceFinish element should not be present for 'None' finish" );
315 "COATING_TOP layer should not be present" );
317 "COATING_BOTTOM layer should not be present" );
332 if( !m_xmllintAvailable )
334 BOOST_WARN_MESSAGE(
false,
"xmllint not available, skipping schema validation tests" );
338 wxString xsdPath = GetXsdPath(
'B' );
340 if( !wxFileExists( xsdPath ) )
342 BOOST_WARN_MESSAGE(
false,
"IPC-2581B1.xsd not found, skipping schema validation" );
346 for(
const std::string& boardFile : VALIDATION_TEST_BOARDS )
350 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
354 BOOST_WARN_MESSAGE(
false,
"Could not load board: " + boardFile );
359 bool valid = ExportAndValidate( board.get(),
'B', errorMsg );
361 BOOST_CHECK_MESSAGE( valid,
"IPC-2581B validation failed for " + boardFile +
": " + errorMsg );
375 if( !m_xmllintAvailable )
377 BOOST_WARN_MESSAGE(
false,
"xmllint not available, skipping schema validation tests" );
381 wxString xsdPath = GetXsdPath(
'C' );
383 if( !wxFileExists( xsdPath ) )
385 BOOST_WARN_MESSAGE(
false,
"IPC-2581C.xsd not found, skipping schema validation" );
389 for(
const std::string& boardFile : VALIDATION_TEST_BOARDS )
393 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
397 BOOST_WARN_MESSAGE(
false,
"Could not load board: " + boardFile );
402 bool valid = ExportAndValidate( board.get(),
'C', errorMsg );
404 BOOST_CHECK_MESSAGE( valid,
"IPC-2581C validation failed for " + boardFile +
": " + errorMsg );
419 static const std::vector<std::string> complexBoards = {
420 "intersectingzones.kicad_pcb",
421 "custom_pads.kicad_pcb",
424 for(
const std::string& boardFile : complexBoards )
428 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
432 BOOST_WARN_MESSAGE(
false,
"Could not load board: " + boardFile );
437 for(
char version : {
'B',
'C' } )
442 bool valid = ExportAndValidate( board.get(), version, errorMsg );
445 wxString::Format(
"Export/validation failed for %s version %c: %s",
446 boardFile, version, errorMsg ) );
464 std::unique_ptr<BOARD> board = LoadBoard(
"issue16658/issue16658.kicad_pcb" );
469 bool hasSmtPad =
false;
471 for(
FOOTPRINT* fp : board->Footprints() )
473 for(
PAD*
pad : fp->Pads() )
494 BOOST_REQUIRE_MESSAGE( hasSmtPad,
"Test board should have SMD pads" );
497 wxString tempPath = CreateTempFile();
499 std::map<std::string, UTF8> props;
500 props[
"units"] =
"mm";
501 props[
"version"] =
"C";
502 props[
"sigfig"] =
"3";
504 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
510 bool hasFMaskLayer = FileContainsPattern( tempPath, wxT(
"layerRef=\"F.Mask\"" ) )
511 || FileContainsPattern( tempPath, wxT(
"layerRef=\"TSM\"" ) );
514 "IPC-2581 export should contain F.Mask layer features for SMD pads" );
517 bool hasLayerFeature = FileContainsPattern( tempPath, wxT(
"<LayerFeature" ) );
518 BOOST_CHECK_MESSAGE( hasLayerFeature,
"IPC-2581 export should contain LayerFeature elements" );
531 std::unique_ptr<BOARD> board = LoadBoard(
"padstacks_complex.kicad_pcb" );
534 for(
char version : {
'B',
'C' } )
538 wxString tempPath = CreateTempFile();
540 std::map<std::string, UTF8> props;
541 props[
"units"] =
"mm";
542 props[
"version"] = std::string( 1, version );
543 props[
"sigfig"] =
"3";
545 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
549 "Empty refDes attribute found" );
551 "Empty RefDes/@name attribute found" );
553 "Empty PinRef/@componentRef attribute found" );
571 std::unique_ptr<BOARD> board = LoadBoard(
"issue12609.kicad_pcb" );
574 wxString tempPath = CreateTempFile();
576 std::map<std::string, UTF8> props;
577 props[
"units"] =
"mm";
578 props[
"version"] =
"C";
579 props[
"sigfig"] =
"3";
581 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
587 std::ifstream xmlFile( tempPath.ToStdString() );
590 std::string xmlContent( ( std::istreambuf_iterator<char>( xmlFile ) ),
591 std::istreambuf_iterator<char>() );
594 size_t c5Pos = xmlContent.find(
"refDes=\"C5\"" );
596 if( c5Pos == std::string::npos )
597 c5Pos = xmlContent.find(
"refDes=\"NOREF_" );
599 BOOST_REQUIRE_MESSAGE( c5Pos != std::string::npos,
600 "C5 component should exist in export" );
603 std::string c5Region = xmlContent.substr( c5Pos, 200 );
605 || c5Region.find(
"rotation=\"90.00\"" ) != std::string::npos,
606 "C5 component rotation should be 90, not inverted. Region: "
616 std::unique_ptr<BOARD> board = LoadBoard(
"issue12609.kicad_pcb" );
619 wxString tempPath = CreateTempFile();
621 std::map<std::string, UTF8> props;
622 props[
"units"] =
"mm";
623 props[
"version"] =
"C";
624 props[
"sigfig"] =
"3";
626 m_ipc2581Plugin.SaveBoard( tempPath, board.get(), &props );
629 bool hasBom = FileContainsPattern( tempPath, wxT(
"<Bom " ) );
630 bool hasBomRef = FileContainsPattern( tempPath, wxT(
"<BomRef " ) );
635 "Content should have BomRef when Bom section is present" );
655 std::unique_ptr<BOARD> board = LoadBoard(
"test_copper_graphics.kicad_pcb" );
658 for(
char version : {
'B',
'C' } )
663 bool valid = ExportAndValidate( board.get(), version, errorMsg );
666 wxString::Format(
"Knockout text export should be schema-valid "
667 "(version %c): %s", version, errorMsg ) );
705 via->SetSecondaryDrillStartLayer(
F_Cu );
706 via->SetSecondaryDrillEndLayer(
In3_Cu );
710 wxString tempPath = CreateTempFile();
711 std::map<std::string, UTF8> props;
712 props[
"units"] =
"mm";
713 props[
"version"] =
"C";
714 props[
"sigfig"] =
"4";
716 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, &board, &props ) );
720 FileContainsPattern( tempPath, wxT(
"<Backdrill type=\"START_LAYER\"" ) ),
721 "Backdrill spec should declare a START_LAYER child" );
723 FileContainsPattern( tempPath, wxT(
"<Backdrill type=\"MUST_NOT_CUT_LAYER\"" ) ),
724 "Backdrill spec should declare a MUST_NOT_CUT_LAYER child" );
726 FileContainsPattern( tempPath, wxT(
"<Backdrill type=\"MAX_STUB_LENGTH\"" ) ),
727 "Backdrill spec should declare a MAX_STUB_LENGTH child" );
732 "Backdrill must convey layers through Property layerOrGroupRef" );
734 "Backdrill should not use schema-invalid startLayerRef attribute" );
736 "Backdrill should not use schema-invalid mustNotCutLayerRef attribute" );
738 "Backdrill should not use schema-invalid maxStubLength attribute" );
740 "Backdrill should not use the non-standard postMachining attribute" );
745 FileContainsPattern( tempPath, wxT(
"layerOrGroupRef=\"In4.Cu\"" ) ),
746 "Front backdrill must-not-cut layer should be In4.Cu" );
751 "Exporter should not emit a primary backdrill spec slot" );
756 FileContainsPattern( tempPath, wxT(
"<Backdrill type=\"OTHER\"" ) ),
757 "Post-machining hint should produce a Backdrill type=OTHER child" );
759 FileContainsPattern( tempPath, wxT(
"comment=\"post-machining=COUNTERSINK\"" ) ),
760 "OTHER Backdrill should carry the post-machining comment" );
785 PAD* thermalPad =
new PAD( fp );
793 fp->
Add( thermalPad );
796 PAD* pasteAperture =
new PAD( fp );
797 pasteAperture->
SetNumber( wxEmptyString );
807 fp->
Add( pasteAperture );
816 PAD* normalSmd =
new PAD( fp2 );
823 fp2->
Add( normalSmd );
827 PAD* implicitMaskSmd =
new PAD( fp2 );
828 implicitMaskSmd->
SetNumber( wxT(
"2" ) );
836 fp2->
Add( implicitMaskSmd );
838 wxString tempPath = CreateTempFile();
839 std::map<std::string, UTF8> props;
840 props[
"units"] =
"mm";
841 props[
"version"] =
"C";
842 props[
"sigfig"] =
"4";
844 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, &board, &props ) );
847 std::ifstream xmlFile( tempPath.ToStdString() );
850 std::string xml( ( std::istreambuf_iterator<char>( xmlFile ) ),
851 std::istreambuf_iterator<char>() );
854 const std::string pasteOpen =
"<LayerFeature layerRef=\"F.Paste\"";
855 size_t pasteStart = xml.find( pasteOpen );
857 if( pasteStart != std::string::npos )
859 size_t pasteEnd = xml.find(
"</LayerFeature>", pasteStart );
862 std::string pasteRegion = xml.substr( pasteStart, pasteEnd - pasteStart );
866 pasteRegion.find(
"<PinRef componentRef=\"U1\" pin=\"33\"" ) == std::string::npos,
867 "Copper-only thermal pad U1.33 must not appear on F.Paste layer feature" );
871 pasteRegion.find(
"<PinRef componentRef=\"R1\" pin=\"1\"" ) != std::string::npos,
872 "Normal SMD pad with explicit F.Paste in layer set should still emit a paste "
877 pasteRegion.find(
"<PinRef componentRef=\"R1\" pin=\"2\"" ) == std::string::npos,
878 "Copper-only SMD pad R1.2 must not appear on F.Paste layer feature" );
884 BOOST_FAIL(
"Expected an F.Paste LayerFeature for the explicitly-pasted control pad" );
889 const std::string maskOpen =
"<LayerFeature layerRef=\"F.Mask\"";
890 size_t maskStart = xml.find( maskOpen );
891 BOOST_REQUIRE_MESSAGE( maskStart != std::string::npos,
892 "F.Mask LayerFeature should still be emitted for SMD copper pads" );
894 size_t maskEnd = xml.find(
"</LayerFeature>", maskStart );
897 std::string maskRegion = xml.substr( maskStart, maskEnd - maskStart );
900 maskRegion.find(
"<PinRef componentRef=\"U1\" pin=\"33\"" ) != std::string::npos,
901 "Thermal pad with explicit F.Mask should appear on F.Mask layer feature" );
906 maskRegion.find(
"<PinRef componentRef=\"R1\" pin=\"2\"" ) != std::string::npos,
907 "SMD copper pad without explicit F.Mask must get an implicit F.Mask opening" );
914static std::string
ReadFile(
const wxString& aPath )
916 std::ifstream file( aPath.ToStdString() );
917 return std::string( ( std::istreambuf_iterator<char>( file ) ),
918 std::istreambuf_iterator<char>() );
928 const std::string open =
"<LayerFeature layerRef=\"" + aLayerRef +
"\"";
929 size_t start = aXml.find( open );
931 if( start == std::string::npos )
932 return std::string();
934 size_t end = aXml.find(
"</LayerFeature>", start );
936 if(
end == std::string::npos )
937 return std::string();
939 return aXml.substr( start,
end - start );
964 wxString tempPath = CreateTempFile();
965 std::map<std::string, UTF8> props;
966 props[
"units"] =
"mm";
967 props[
"version"] =
"C";
968 props[
"sigfig"] =
"4";
970 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, &board, &props ) );
973 std::string xml =
ReadFile( tempPath );
976 size_t rectPos = xml.find(
"<RectRound" );
977 BOOST_REQUIRE_MESSAGE( rectPos != std::string::npos,
"Export should contain a RectRound" );
979 std::string rectNode = xml.substr( rectPos, xml.find(
'>', rectPos ) - rectPos );
982 "Rounded gr_rect must export its 0.75 mm corner radius. Node: "
985 && rectNode.find(
"lowerLeft=\"true\"" ) != std::string::npos,
986 "Rounded gr_rect must set its corner flags. Node: " + rectNode );
988 "Corner radius must not collapse to stroke_width/2. Node: " + rectNode );
1010 pad->SetNumber( wxT(
"1" ) );
1019 wxString tempPath = CreateTempFile();
1020 std::map<std::string, UTF8> props;
1021 props[
"units"] =
"mm";
1022 props[
"version"] =
"C";
1023 props[
"sigfig"] =
"4";
1025 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, &board, &props ) );
1028 std::string xml =
ReadFile( tempPath );
1033 fMask.find(
"<PinRef componentRef=\"J29\" pin=\"1\"" ) == std::string::npos,
1034 "Back-only masked pad must not appear on F.Mask layer feature" );
1038 bMask.find(
"<PinRef componentRef=\"J29\" pin=\"1\"" ) != std::string::npos,
1039 "Back-only masked pad should appear on B.Mask layer feature" );
1061 pad->SetNumber( wxT(
"1" ) );
1068 pad->SetLocalSolderMaskMargin(
pcbIUScale.mmToIU( -0.05 ) );
1071 wxString tempPath = CreateTempFile();
1072 std::map<std::string, UTF8> props;
1073 props[
"units"] =
"mm";
1074 props[
"version"] =
"C";
1075 props[
"sigfig"] =
"4";
1077 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, &board, &props ) );
1080 std::string xml =
ReadFile( tempPath );
1085 "Copper roundrect should keep its 0.10 mm radius" );
1087 "Mask roundrect aperture must shrink its radius to 0.05 mm" );
1089 "Mask roundrect aperture should be 0.35 mm wide" );
1123 wxString tempPath = CreateTempFile();
1124 std::map<std::string, UTF8> props;
1125 props[
"units"] =
"mm";
1126 props[
"version"] =
"C";
1127 props[
"sigfig"] =
"4";
1129 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, &board, &props ) );
1132 std::string xml =
ReadFile( tempPath );
1139 "Footprint graphic should be present on F.Cu" );
1141 "Multi-layer footprint graphic must also appear on F.Mask" );
1164 pad->SetNumber( wxT(
"1" ) );
1173 wxString tempPath = CreateTempFile();
1174 std::map<std::string, UTF8> props;
1175 props[
"units"] =
"mm";
1176 props[
"version"] =
"C";
1177 props[
"sigfig"] =
"4";
1179 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, &board, &props ) );
1182 std::ifstream xmlFile( tempPath.ToStdString() );
1185 std::string xml( ( std::istreambuf_iterator<char>( xmlFile ) ),
1186 std::istreambuf_iterator<char>() );
1190 const std::string maskRef =
"<PadstackPadDef layerRef=\"F.Mask\"";
1191 size_t maskDefPos = xml.find( maskRef );
1192 BOOST_REQUIRE_MESSAGE( maskDefPos != std::string::npos,
1193 "F.Mask padstack pad definition should be present" );
1195 size_t refPos = xml.find(
"StandardPrimitiveRef id=\"", maskDefPos );
1197 refPos += std::string(
"StandardPrimitiveRef id=\"" ).size();
1198 size_t refEnd = xml.find(
'"', refPos );
1199 std::string maskPrimId = xml.substr( refPos, refEnd - refPos );
1201 std::string entry =
"<EntryStandard id=\"" + maskPrimId +
"\"";
1202 size_t entryPos = xml.find( entry );
1203 BOOST_REQUIRE_MESSAGE( entryPos != std::string::npos,
1204 "Mask primitive EntryStandard should be defined" );
1206 size_t diaPos = xml.find(
"diameter=\"", entryPos );
1208 diaPos += std::string(
"diameter=\"" ).size();
1209 size_t diaEnd = xml.find(
'"', diaPos );
1210 double diameter = std::stod( xml.substr( diaPos, diaEnd - diaPos ) );
1213 "F.Mask primitive diameter should be 2.0 mm (1.0 mm pad + 0.5 mm "
1214 "margin per side), got " << diameter );
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
void SetCornerRadius(int aRadius)
virtual void SetFilled(bool aFlag)
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.
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
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.
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)
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()
static std::string ReadFile(const wxString &aPath)
Read an exported file's full contents into a std::string.
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_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