40namespace fs = std::filesystem;
44using FIELDS = std::map<std::string, std::string>;
46std::vector<FIELDS> MatrixLayers(
const fs::path& aRoot )
48 std::vector<FIELDS> rows;
52 for(
const std::string& line :
ReadLines( aRoot /
"matrix" /
"matrix" ) )
54 if( line ==
"LAYER {" )
59 else if( inLayer && line ==
"}" )
61 rows.push_back( fields );
66 size_t equals = line.find(
'=' );
68 if(
equals != std::string::npos )
70 size_t start = line.find_first_not_of(
' ' );
71 fields[line.substr( start,
equals - start )] = line.substr(
equals + 1 );
80const FIELDS* FindRow(
const std::vector<FIELDS>& aRows,
const std::string& aName )
82 auto it = std::find_if( aRows.begin(), aRows.end(), [&](
const FIELDS& aRow )
83 { return aRow.at(
"NAME" ) == aName; } );
85 return it == aRows.end() ? nullptr : &*it;
89std::unique_ptr<BOARD>
LoadBoard(
const std::string& aPath )
97 std::pair<std::string, std::string> m_start;
98 std::pair<std::string, std::string> m_end;
102std::map<int, std::vector<ROUT_SPAN>> RoutChains(
const std::vector<std::string>& aLines )
104 std::map<int, std::vector<ROUT_SPAN>> chains;
105 std::string chainAttribute;
107 for(
const std::string& line : aLines )
109 if( line.rfind(
"@", 0 ) == 0 && line.find(
" .rout_chain" ) != std::string::npos )
110 chainAttribute = line.substr( 1, line.find(
' ' ) - 1 ) +
"=";
113 if( chainAttribute.empty() )
116 for(
const std::string& line : aLines )
118 if( line.rfind(
"L ", 0 ) != 0 && line.rfind(
"A ", 0 ) != 0 )
121 std::istringstream fields( line );
124 fields >> record >> span.m_start.first >> span.m_start.second >> span.m_end.first >> span.m_end.second;
126 size_t semicolon = line.find(
';' );
128 if( semicolon == std::string::npos )
131 std::istringstream attributes( line.substr( semicolon + 1 ) );
132 std::string attribute;
134 while( std::getline( attributes, attribute,
',' ) )
136 if( attribute.rfind( chainAttribute, 0 ) == 0 )
137 chains[std::stoi( attribute.substr( chainAttribute.size() ) )].push_back( span );
148 std::unique_ptr<BOARD> board = LoadBoard(
"connect/connect.kicad_pcb" );
152 std::vector<FIELDS> rows = MatrixLayers( root );
153 const FIELDS* inner = FindRow( rows,
"in1.cu" );
157 board = LoadBoard(
"issue24089/issue24089.kicad_pcb" );
161 rows = MatrixLayers( root );
162 const FIELDS* mixed = FindRow( rows,
"f.cu" );
170 std::unique_ptr<BOARD> board = LoadBoard(
"odbpp/two_board_islands.kicad_pcb" );
173 BOOST_REQUIRE( board->GetBoardPolygonOutlines( outlines,
true ) );
176 std::vector<FIELDS> rows = MatrixLayers( root );
177 const FIELDS* rout = FindRow( rows,
"rout" );
179 BOOST_CHECK( !FindRow( rows,
"edge.cuts" ) );
184 std::vector<std::string> lines =
ReadLines( fs::path( product.
LayerFile(
"rout",
"features" ).ToStdWstring() ) );
185 size_t routes = std::count_if( lines.begin(), lines.end(), [](
const std::string& aLine )
186 { return aLine.rfind(
"L ", 0 ) == 0 || aLine.rfind(
"A ", 0 ) == 0; } );
187 BOOST_CHECK_GT( routes, 7u );
188 BOOST_CHECK( std::none_of( lines.begin(), lines.end(), [](
const std::string& aLine )
189 { return aLine.rfind(
"S ", 0 ) == 0; } ) );
190 BOOST_CHECK( std::any_of( lines.begin(), lines.end(), [](
const std::string& aLine )
191 { return aLine.rfind(
"$0 r0", 0 ) == 0; } ) );
196 std::map<int, std::vector<ROUT_SPAN>> chains = RoutChains( lines );
197 BOOST_REQUIRE_EQUAL( chains.size(), 2u );
199 for(
const auto& [number, spans] : chains )
201 BOOST_REQUIRE_GT( spans.size(), 2u );
203 for(
size_t i = 0; i < spans.size(); ++i )
205 const ROUT_SPAN& current = spans[i];
206 const ROUT_SPAN&
next = spans[( i + 1 ) % spans.size()];
207 BOOST_CHECK_MESSAGE( current.m_end ==
next.m_start,
"open rout chain " << number <<
" at " << i );
215 std::unique_ptr<BOARD> board = LoadBoard(
"issue24089/issue24089.kicad_pcb" );
217 BOOST_REQUIRE( board->GetDesignSettings().GetStackupDescriptor().m_EdgePlating );
220 fs::path rout( product.
LayerFile(
"rout",
"features" ).ToStdWstring() );
224 std::vector<std::string> routLines =
ReadLines( rout );
225 BOOST_CHECK( std::any_of( routLines.begin(), routLines.end(), [](
const std::string& aLine )
226 { return aLine.rfind(
"A ", 0 ) == 0; } ) );
228 board = LoadBoard(
"issue19325/issue19325.kicad_pcb" );
232 fs::path castRout( castellated.
LayerFile(
"rout",
"features" ).ToStdWstring() );
241 std::unique_ptr<BOARD> board = LoadBoard(
"issue24089/issue24089.kicad_pcb" );
244 std::vector<FIELDS> rows = MatrixLayers( root );
246 for(
const std::string& side : {
"f",
"b" } )
248 const FIELDS* copper = FindRow( rows, side +
".cu" );
252 for(
const std::string& suffix : {
"mask",
"paste",
"silkscreen" } )
254 const FIELDS* row = FindRow( rows, side +
"." + suffix );
264 std::set<std::string> copperIds;
266 for(
const FIELDS& row : rows )
268 if( row.at(
"TYPE" ) ==
"SIGNAL" || row.at(
"TYPE" ) ==
"POWER_GROUND"
269 || row.at(
"TYPE" ) ==
"MIXED" )
271 copperIds.insert( row.at(
"ID" ) );
275 for(
const FIELDS& row : rows )
277 if( row.at(
"TYPE" ) !=
"DIELECTRIC" )
280 if( row.at(
"DIELECTRIC_TYPE" ) ==
"CORE" )
285 BOOST_CHECK( copperIds.count( row.at(
"CU_TOP" ) ) );
286 BOOST_CHECK( copperIds.count( row.at(
"CU_BOTTOM" ) ) );
287 BOOST_CHECK( !row.at(
"DIELECTRIC_NAME" ).empty() );
291 BOOST_CHECK( !row.count(
"CU_TOP" ) );
292 BOOST_CHECK( !row.count(
"CU_BOTTOM" ) );
300 std::unique_ptr<BOARD> board = LoadBoard(
"issue24089/issue24089.kicad_pcb" );
306 {
Dwgs_User,
true, wxEmptyString, wxS(
"SOLDER_MASK" ) },
307 {
In1_Cu,
true, wxEmptyString, wxS(
"SIGNAL" ) },
308 {
B_Cu,
true, wxEmptyString, wxS(
"SILK_SCREEN" ) } };
309 BOOST_REQUIRE_NO_THROW( plugin.
Export( wxString::FromUTF8( dir.
Path().string() ), *board, options ) );
310 std::vector<FIELDS> rows = MatrixLayers( dir.
Path() );
312 const FIELDS* silk = FindRow( rows,
"f.silkscreen" );
315 BOOST_CHECK( !silk->contains(
"REF" ) );
317 const FIELDS* drawings = FindRow( rows,
"user.drawings" );
321 BOOST_CHECK( !drawings->contains(
"REF" ) );
323 const FIELDS* inner = FindRow( rows,
"in1.cu" );
328 const FIELDS* bottom = FindRow( rows,
"b.cu" );
336 std::unique_ptr<BOARD> board = LoadBoard(
"issue24089/issue24089.kicad_pcb" );
339 std::vector<FIELDS> rows = MatrixLayers( root );
340 std::set<unsigned long> ids;
341 unsigned long maxId = 0;
343 for(
const FIELDS& row : rows )
348 for(
const std::string& line :
ReadLines( root /
"matrix" /
"matrix" ) )
350 if( line.rfind(
" ID=", 0 ) != 0 )
353 unsigned long id = std::stoul( line.substr( 7 ) );
355 BOOST_CHECK( ids.insert(
id ).second );
356 maxId = std::max( maxId,
id );
359 bool foundMax =
false;
361 for(
const std::string& line :
ReadLines( root /
"misc" /
"info" ) )
363 if( line.rfind(
"MAX_UID=", 0 ) == 0 )
366 BOOST_CHECK_GE( std::stoul( line.substr( 8 ) ), maxId );
370 BOOST_CHECK( foundMax );
376 std::unique_ptr<BOARD> board = LoadBoard(
"odbpp/pad_roles.kicad_pcb" );
380 fs::path rout( product.
LayerFile(
"rout",
"features" ).ToStdWstring() );
385 bool drillHasSlotLine =
false;
389 if( row.m_type != wxS(
"DRILL" ) )
393 wxS(
"features" ) ).ToStdWstring() ) ) )
395 if( line.rfind(
"L ", 0 ) == 0 )
396 drillHasSlotLine =
true;
400 BOOST_CHECK( drillHasSlotLine );
402 board = LoadBoard(
"odbpp/npth_pressfit_slot.kicad_pcb" );
406 fs::path npthRout( npthProduct.
LayerFile(
"rout",
"features" ).ToStdWstring() );
415 std::unique_ptr<BOARD> board = LoadBoard(
"api_kitchen_sink.kicad_pcb" );
419 std::set<wxString> ids;
422 ids.insert( row.m_id );
426 for(
const wxString& reference : { row.m_ref, row.m_cuTop, row.m_cuBottom } )
427 BOOST_CHECK_MESSAGE( reference.IsEmpty() || ids.count( reference ), row.m_name <<
" references " << reference );
434 std::unique_ptr<BOARD> board = LoadBoard(
"component_classes.kicad_pcb" );
437 BOOST_CHECK( !board->GetBoardPolygonOutlines( outline,
false ) );
441 std::vector<std::string> lines =
ReadLines( fs::path( product.
LayerFile(
"rout",
"features" ).ToStdWstring() ) );
442 BOOST_CHECK( std::none_of( lines.begin(), lines.end(), [](
const std::string& aLine )
443 { return aLine.rfind(
"L ", 0 ) == 0 || aLine.rfind(
"A ", 0 ) == 0; } ) );
444 BOOST_CHECK(
reporter.GetMessages().Contains( wxS(
"ODB++ rout layer has no board contour" ) ) );
450 std::unique_ptr<BOARD> board = LoadBoard(
"odbpp/layer_name_collisions.kicad_pcb" );
452 BOOST_REQUIRE_EQUAL( board->GetLayerName(
F_Cu ), wxS(
"Signal" ) );
453 BOOST_REQUIRE_EQUAL( board->GetLayerName(
In1_Cu ), wxS(
"SiGnAl" ) );
454 BOOST_REQUIRE_EQUAL( board->GetLayerName(
B_Cu ), wxS(
"SIGNAL" ) );
455 BOOST_REQUIRE_EQUAL( board->GetLayerName(
User_1 ), wxS(
"notes" ) );
456 BOOST_REQUIRE_EQUAL( board->GetLayerName(
User_2 ), wxS(
"Notes" ) );
457 BOOST_REQUIRE_EQUAL( board->GetLayerName(
User_3 ), wxS(
"notes_1" ) );
458 BOOST_REQUIRE_EQUAL( board->GetLayerName(
User_4 ), wxS(
"COMP_+_BOT" ) );
461 for(
bool filtered : {
false,
true } )
463 fs::path root = dir.
Path() / ( filtered ?
"filtered" :
"default" );
467 fs::create_directories( root );
471 BOOST_REQUIRE_NO_THROW( plugin.
Export( wxString::FromUTF8( root.string() ), *board, options ) );
478 std::vector<FIELDS> rows = MatrixLayers( root );
480 std::set<std::string> names;
482 for(
const FIELDS& row : rows )
483 BOOST_CHECK( names.insert( row.at(
"NAME" ) ).second );
485 for(
const std::string&
name : {
"signal",
"signal_2",
"signal_3",
"notes",
"notes_2",
"notes_1",
489 const FIELDS* rout = FindRow( rows,
"rout" );
494 bool throughDrill =
false;
495 bool backdrill =
false;
497 for(
const FIELDS& row : rows )
499 if( row.at(
"TYPE" ) !=
"DRILL" )
502 if( row.contains(
"ADD_TYPE" ) && row.at(
"ADD_TYPE" ) ==
"BACKDRILL" )
504 if( row.at(
"START_NAME" ) ==
"in2.cu" && row.at(
"END_NAME" ) ==
"signal_3" )
507 fs::path attrs = root /
"steps" /
"pcb" /
"layers" / row.at(
"NAME" ) /
"attrlist";
508 std::vector<std::string> lines =
ReadLines( attrs );
509 BOOST_CHECK( std::find( lines.begin(), lines.end(),
510 ".backdrill_penetrate_stop_layer=signal_2" ) != lines.end() );
513 else if( row.at(
"START_NAME" ) ==
"signal" && row.at(
"END_NAME" ) ==
"signal_3" )
519 BOOST_CHECK( throughDrill );
520 BOOST_CHECK( backdrill );
522 const FIELDS* bottomComponents = FindRow( rows,
"comp_+_bot_2" );
525 fs::path components = root /
"steps" /
"pcb" /
"layers" /
"comp_+_bot_2" /
"components";
527 std::vector<std::string> lines =
ReadLines( components );
528 BOOST_CHECK( std::any_of( lines.begin(), lines.end(), [](
const std::string& aLine )
529 { return aLine.rfind(
"CMP ", 0 ) == 0; } ) );
General utilities for PCB file IO for QA programs.
const std::filesystem::path & Path() const
Get the path to the temporary directory as a std::filesystem::path.
The matrix and layer directories of an exported ODB++ product model tree.
wxString LayerFile(const wxString &aLayer, const wxString &aFile) const
Path of a file in a layer directory, matched to the directory name without regard to case.
const std::vector< ODB_MATRIX_ROW > & Matrix() const
size_t CountRecordsWithInteger(const std::string &aName, int aValue) const
size_t CountRecordsWithFlag(const std::string &aName) const
void Export(const wxString &aFileName, BOARD &aBoard, const ODB_EXPORT_OPTIONS &aOptions)
Write the product model tree at aFileName Throw IO_ERROR when the write fails.
Represent a set of closed polygons.
int OutlineCount() const
Return the number of outlines in the set.
A wrapper for reporting to a wxString object.
SECTION_SET LegacySections()
Schema sections of the content that KiCad wrote before the function modes.
std::string GetPcbnewTestDataDir()
Utility which returns a path to the data directory where the test board files are stored.
void LoadBoard(SETTINGS_MANAGER &aSettingsManager, const wxString &aRelPath, std::unique_ptr< BOARD > &aBoard)
std::unique_ptr< BOARD > ReadBoardFromFileOrStream(const std::string &aFilename, std::istream &aFallback)
Read a board from a file, or another stream, as appropriate.
bool equals(glm::mat< L, C, T, Q > const &aFirst, glm::mat< L, C, T, Q > const &aSecond, T aEpsilon=static_cast< T >(FLT_EPSILON *10))
Template to compare two glm::mat<T> values for equality within a required epsilon.
fs::path ExportOdb(const BOARD &aBoard, const fs::path &aDir, const std::string &aUnits, const std::string &aSigfig, REPORTER *aReporter)
std::vector< std::string > ReadLines(const fs::path &aFile)
const KI_TEST::SCOPED_TEMP_DIR & TempDir()
What a caller asks of one ODB++ export.
std::optional< FAB::SECTION_SET > m_sections
Unset exports every section.
std::vector< ODB_LAYER_OVERRIDE > m_layerOverrides
One LAYER block of matrix/matrix.
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
IbisParser parser & reporter
BOOST_AUTO_TEST_CASE(OdbCopperLayerTypesFollowBoardSetup)
BOOST_CHECK_EQUAL(result, "25.4")