39namespace fs = std::filesystem;
43using FIELDS = std::map<std::string, std::string>;
45std::vector<FIELDS> MatrixLayers(
const fs::path& aRoot )
47 std::vector<FIELDS> rows;
51 for(
const std::string& line :
ReadLines( aRoot /
"matrix" /
"matrix" ) )
53 if( line ==
"LAYER {" )
58 else if( inLayer && line ==
"}" )
60 rows.push_back( fields );
65 size_t equals = line.find(
'=' );
67 if(
equals != std::string::npos )
69 size_t start = line.find_first_not_of(
' ' );
70 fields[line.substr( start,
equals - start )] = line.substr(
equals + 1 );
79const FIELDS* FindRow(
const std::vector<FIELDS>& aRows,
const std::string& aName )
81 auto it = std::find_if( aRows.begin(), aRows.end(), [&](
const FIELDS& aRow )
82 { return aRow.at(
"NAME" ) == aName; } );
84 return it == aRows.end() ? nullptr : &*it;
90 std::pair<std::string, std::string> m_start;
91 std::pair<std::string, std::string> m_end;
95std::map<int, std::vector<ROUT_SPAN>> RoutChains(
const std::vector<std::string>& aLines )
97 std::map<int, std::vector<ROUT_SPAN>> chains;
98 std::string chainAttribute;
100 for(
const std::string& line : aLines )
102 if( line.rfind(
"@", 0 ) == 0 && line.find(
" .rout_chain" ) != std::string::npos )
103 chainAttribute = line.substr( 1, line.find(
' ' ) - 1 ) +
"=";
106 if( chainAttribute.empty() )
109 for(
const std::string& line : aLines )
111 if( line.rfind(
"L ", 0 ) != 0 && line.rfind(
"A ", 0 ) != 0 )
114 std::istringstream fields( line );
117 fields >> record >> span.m_start.first >> span.m_start.second >> span.m_end.first >> span.m_end.second;
119 size_t semicolon = line.find(
';' );
121 if( semicolon == std::string::npos )
124 std::istringstream attributes( line.substr( semicolon + 1 ) );
125 std::string attribute;
127 while( std::getline( attributes, attribute,
',' ) )
129 if( attribute.rfind( chainAttribute, 0 ) == 0 )
130 chains[std::stoi( attribute.substr( chainAttribute.size() ) )].push_back( span );
144 std::vector<FIELDS> rows = MatrixLayers( root );
145 const FIELDS* inner = FindRow( rows,
"in1.cu" );
152 rows = MatrixLayers( root );
153 const FIELDS* mixed = FindRow( rows,
"f.cu" );
163 BOOST_REQUIRE( board->GetBoardPolygonOutlines( outlines,
true ) );
166 std::vector<FIELDS> rows = MatrixLayers( root );
167 const FIELDS* rout = FindRow( rows,
"rout" );
169 BOOST_CHECK( !FindRow( rows,
"edge.cuts" ) );
174 std::vector<std::string> lines =
ReadLines( fs::path( product.
LayerFile(
"rout",
"features" ).ToStdWstring() ) );
175 size_t routes = std::count_if( lines.begin(), lines.end(), [](
const std::string& aLine )
176 { return aLine.rfind(
"L ", 0 ) == 0 || aLine.rfind(
"A ", 0 ) == 0; } );
177 BOOST_CHECK_GT( routes, 7u );
178 BOOST_CHECK( std::none_of( lines.begin(), lines.end(), [](
const std::string& aLine )
179 { return aLine.rfind(
"S ", 0 ) == 0; } ) );
180 BOOST_CHECK( std::any_of( lines.begin(), lines.end(), [](
const std::string& aLine )
181 { return aLine.rfind(
"$0 r0", 0 ) == 0; } ) );
186 std::map<int, std::vector<ROUT_SPAN>> chains = RoutChains( lines );
187 BOOST_REQUIRE_EQUAL( chains.size(), 2u );
189 for(
const auto& [number, spans] : chains )
191 BOOST_REQUIRE_GT( spans.size(), 2u );
193 for(
size_t i = 0; i < spans.size(); ++i )
195 const ROUT_SPAN& current = spans[i];
196 const ROUT_SPAN&
next = spans[( i + 1 ) % spans.size()];
197 BOOST_CHECK_MESSAGE( current.m_end ==
next.m_start,
"open rout chain " << number <<
" at " << i );
206 BOOST_REQUIRE( board->GetDesignSettings().GetStackupDescriptor().m_EdgePlating );
209 fs::path rout( product.
LayerFile(
"rout",
"features" ).ToStdWstring() );
213 std::vector<std::string> routLines =
ReadLines( rout );
214 BOOST_CHECK( std::any_of( routLines.begin(), routLines.end(), [](
const std::string& aLine )
215 { return aLine.rfind(
"A ", 0 ) == 0; } ) );
220 fs::path castRout( castellated.
LayerFile(
"rout",
"features" ).ToStdWstring() );
231 std::vector<FIELDS> rows = MatrixLayers( root );
233 for(
const std::string& side : {
"f",
"b" } )
235 const FIELDS* copper = FindRow( rows, side +
".cu" );
239 for(
const std::string& suffix : {
"mask",
"paste",
"silkscreen" } )
241 const FIELDS* row = FindRow( rows, side +
"." + suffix );
251 std::set<std::string> copperIds;
253 for(
const FIELDS& row : rows )
255 if( row.at(
"TYPE" ) ==
"SIGNAL" || row.at(
"TYPE" ) ==
"POWER_GROUND"
256 || row.at(
"TYPE" ) ==
"MIXED" )
258 copperIds.insert( row.at(
"ID" ) );
262 for(
const FIELDS& row : rows )
264 if( row.at(
"TYPE" ) !=
"DIELECTRIC" )
267 if( row.at(
"DIELECTRIC_TYPE" ) ==
"CORE" )
272 BOOST_CHECK( copperIds.count( row.at(
"CU_TOP" ) ) );
273 BOOST_CHECK( copperIds.count( row.at(
"CU_BOTTOM" ) ) );
274 BOOST_CHECK( !row.at(
"DIELECTRIC_NAME" ).empty() );
278 BOOST_CHECK( !row.count(
"CU_TOP" ) );
279 BOOST_CHECK( !row.count(
"CU_BOTTOM" ) );
292 {
Dwgs_User,
true, wxEmptyString, wxS(
"SOLDER_MASK" ) },
293 {
In1_Cu,
true, wxEmptyString, wxS(
"SIGNAL" ) },
294 {
B_Cu,
true, wxEmptyString, wxS(
"SILK_SCREEN" ) } };
295 BOOST_REQUIRE_NO_THROW( plugin.
Export( wxString::FromUTF8( dir.
Path().string() ), *board, options ) );
296 std::vector<FIELDS> rows = MatrixLayers( dir.
Path() );
298 const FIELDS* silk = FindRow( rows,
"f.silkscreen" );
301 BOOST_CHECK( !silk->contains(
"REF" ) );
303 const FIELDS* drawings = FindRow( rows,
"user.drawings" );
307 BOOST_CHECK( !drawings->contains(
"REF" ) );
309 const FIELDS* inner = FindRow( rows,
"in1.cu" );
314 const FIELDS* bottom = FindRow( rows,
"b.cu" );
324 std::vector<FIELDS> rows = MatrixLayers( root );
325 std::set<unsigned long> ids;
326 unsigned long maxId = 0;
328 for(
const FIELDS& row : rows )
333 for(
const std::string& line :
ReadLines( root /
"matrix" /
"matrix" ) )
335 if( line.rfind(
" ID=", 0 ) != 0 )
338 unsigned long id = std::stoul( line.substr( 7 ) );
340 BOOST_CHECK( ids.insert(
id ).second );
341 maxId = std::max( maxId,
id );
344 bool foundMax =
false;
346 for(
const std::string& line :
ReadLines( root /
"misc" /
"info" ) )
348 if( line.rfind(
"MAX_UID=", 0 ) == 0 )
351 BOOST_CHECK_GE( std::stoul( line.substr( 8 ) ), maxId );
355 BOOST_CHECK( foundMax );
364 fs::path rout( product.
LayerFile(
"rout",
"features" ).ToStdWstring() );
369 bool drillHasSlotLine =
false;
373 if( row.m_type != wxS(
"DRILL" ) )
377 wxS(
"features" ) ).ToStdWstring() ) ) )
379 if( line.rfind(
"L ", 0 ) == 0 )
380 drillHasSlotLine =
true;
384 BOOST_CHECK( drillHasSlotLine );
389 fs::path npthRout( npthProduct.
LayerFile(
"rout",
"features" ).ToStdWstring() );
401 std::set<wxString> ids;
404 ids.insert( row.m_id );
408 for(
const wxString& reference : { row.m_ref, row.m_cuTop, row.m_cuBottom } )
409 BOOST_CHECK_MESSAGE( reference.IsEmpty() || ids.count( reference ), row.m_name <<
" references " << reference );
418 BOOST_CHECK( !board->GetBoardPolygonOutlines( outline,
false ) );
422 std::vector<std::string> lines =
ReadLines( fs::path( product.
LayerFile(
"rout",
"features" ).ToStdWstring() ) );
423 BOOST_CHECK( std::none_of( lines.begin(), lines.end(), [](
const std::string& aLine )
424 { return aLine.rfind(
"L ", 0 ) == 0 || aLine.rfind(
"A ", 0 ) == 0; } ) );
425 BOOST_CHECK(
reporter.GetMessages().Contains( wxS(
"ODB++ rout layer has no board contour" ) ) );
432 BOOST_REQUIRE_EQUAL( board->GetLayerName(
F_Cu ), wxS(
"Signal" ) );
433 BOOST_REQUIRE_EQUAL( board->GetLayerName(
In1_Cu ), wxS(
"SiGnAl" ) );
434 BOOST_REQUIRE_EQUAL( board->GetLayerName(
B_Cu ), wxS(
"SIGNAL" ) );
435 BOOST_REQUIRE_EQUAL( board->GetLayerName(
User_1 ), wxS(
"notes" ) );
436 BOOST_REQUIRE_EQUAL( board->GetLayerName(
User_2 ), wxS(
"Notes" ) );
437 BOOST_REQUIRE_EQUAL( board->GetLayerName(
User_3 ), wxS(
"notes_1" ) );
438 BOOST_REQUIRE_EQUAL( board->GetLayerName(
User_4 ), wxS(
"COMP_+_BOT" ) );
441 for(
bool filtered : {
false,
true } )
443 fs::path root = dir.
Path() / ( filtered ?
"filtered" :
"default" );
447 fs::create_directories( root );
451 BOOST_REQUIRE_NO_THROW( plugin.
Export( wxString::FromUTF8( root.string() ), *board, options ) );
458 std::vector<FIELDS> rows = MatrixLayers( root );
460 std::set<std::string> names;
462 for(
const FIELDS& row : rows )
463 BOOST_CHECK( names.insert( row.at(
"NAME" ) ).second );
465 for(
const std::string&
name : {
"signal",
"signal_2",
"signal_3",
"notes",
"notes_2",
"notes_1",
469 const FIELDS* rout = FindRow( rows,
"rout" );
474 bool throughDrill =
false;
475 bool backdrill =
false;
477 for(
const FIELDS& row : rows )
479 if( row.at(
"TYPE" ) !=
"DRILL" )
482 if( row.contains(
"ADD_TYPE" ) && row.at(
"ADD_TYPE" ) ==
"BACKDRILL" )
484 if( row.at(
"START_NAME" ) ==
"in2.cu" && row.at(
"END_NAME" ) ==
"signal_3" )
487 fs::path attrs = root /
"steps" /
"pcb" /
"layers" / row.at(
"NAME" ) /
"attrlist";
488 std::vector<std::string> lines =
ReadLines( attrs );
489 BOOST_CHECK( std::find( lines.begin(), lines.end(),
490 ".backdrill_penetrate_stop_layer=signal_2" ) != lines.end() );
493 else if( row.at(
"START_NAME" ) ==
"signal" && row.at(
"END_NAME" ) ==
"signal_3" )
499 BOOST_CHECK( throughDrill );
500 BOOST_CHECK( backdrill );
502 const FIELDS* bottomComponents = FindRow( rows,
"comp_+_bot_2" );
505 fs::path components = root /
"steps" /
"pcb" /
"layers" /
"comp_+_bot_2" /
"components";
507 std::vector<std::string> lines =
ReadLines( components );
508 BOOST_CHECK( std::any_of( lines.begin(), lines.end(), [](
const std::string& aLine )
509 { 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::unique_ptr< BOARD > RequireBoardFromPcbnewQAData(const std::filesystem::path &aFilename)
Require a board from the path relative to the pcbnew QA data directory.
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")