KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_ipc2581_export.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
28
32#include <qa_utils/file_utils.h>
34
40
41#include <board.h>
44#include <footprint.h>
45#include <netinfo.h>
46#include <pad.h>
47#include <pcb_shape.h>
48#include <pcb_textbox.h>
49#include <pcb_track.h>
50#include <base_units.h>
52#include <reporter.h>
53
54#include <wx/dir.h>
55#include <wx/file.h>
56#include <wx/filename.h>
57#include <wx/process.h>
58#include <wx/txtstrm.h>
59#include <wx/utils.h>
60#include <wx/xml/xml.h>
61#include <wx/wfstream.h>
62#include <wx/zipstrm.h>
63
64#include <cmath>
65#include <fstream>
66#include <regex>
67#include <set>
68#include <sstream>
69#include <tuple>
70
71
72namespace
73{
74
75void CollectXmlElements( wxXmlNode* aNode, const wxString& aName, std::vector<wxXmlNode*>& aResult )
76{
77 for( wxXmlNode* node = aNode; node; node = node->GetNext() )
78 {
79 if( node->GetName() == aName )
80 aResult.push_back( node );
81
82 CollectXmlElements( node->GetChildren(), aName, aResult );
83 }
84}
85
86
87wxXmlNode* FindXmlChild( wxXmlNode* aParent, const wxString& aName )
88{
89 for( wxXmlNode* child = aParent->GetChildren(); child; child = child->GetNext() )
90 {
91 if( child->GetName() == aName )
92 return child;
93 }
94
95 return nullptr;
96}
97
98
102bool IsXmllintAvailable()
103{
104 wxArrayString output;
105 wxArrayString errors;
106 int result = wxExecute( "xmllint --version", output, errors, wxEXEC_SYNC );
107 return result == 0;
108}
109
110
112bool XmllintRequired()
113{
114 wxString value;
115 return wxGetEnv( wxS( "KICAD_QA_REQUIRE_XMLLINT" ), &value ) && value == wxS( "1" );
116}
117
118
119wxString XmllintFailureMessage( int aExitCode, const wxArrayString& aErrors )
120{
121 if( aExitCode == 0 )
122 return wxEmptyString;
123
124 if( aErrors.IsEmpty() )
125 return wxString::Format( "xmllint exited with status %d", aExitCode );
126
127 wxString errorMsg;
128
129 for( const wxString& line : aErrors )
130 errorMsg += line + "\n";
131
132 return errorMsg;
133}
134
135
140wxString ValidateXmlWithXsd( const wxString& aXmlPath, const wxString& aXsdPath )
141{
142 wxString cmd = wxString::Format( "xmllint --noout --schema \"%s\" \"%s\"", aXsdPath, aXmlPath );
143
144 wxArrayString output;
145 wxArrayString errors;
146 int result = wxExecute( cmd, output, errors, wxEXEC_SYNC );
147
148 return XmllintFailureMessage( result, errors );
149}
150
151
155bool FileContainsPattern( const wxString& aFilePath, const wxString& aPattern )
156{
157 std::ifstream file( aFilePath.ToStdString() );
158
159 if( !file.is_open() )
160 return false;
161
162 std::stringstream buffer;
163 buffer << file.rdbuf();
164 std::string content = buffer.str();
165
166 return content.find( aPattern.ToStdString() ) != std::string::npos;
167}
168
169
170wxXmlNode* FindXmlEntry( wxXmlNode* aRoot, const wxString& aName, const wxString& aId )
171{
172 std::vector<wxXmlNode*> entries;
173 CollectXmlElements( aRoot, aName, entries );
174
175 for( wxXmlNode* entry : entries )
176 {
177 if( entry->GetAttribute( wxS( "id" ) ) == aId )
178 return entry;
179 }
180
181 return nullptr;
182}
183
184
185wxXmlNode* SecondaryDrillProperty( wxXmlNode* aSpec, const wxString& aType )
186{
187 for( wxXmlNode* child = aSpec->GetChildren(); child; child = child->GetNext() )
188 {
189 if( child->GetName() == wxS( "SecondaryDrill" ) && child->GetAttribute( wxS( "type" ) ) == aType )
190 return FindXmlChild( child, wxS( "Property" ) );
191 }
192
193 return nullptr;
194}
195
196
197bool XmlRegionHasCoordinateNear( const std::string& aRegion, char aAxis, double aExpected, double aTolerance )
198{
199 std::regex coordinateRegex( std::string( 1, aAxis ) + "=\"(-?[0-9]+(?:\\.[0-9]+)?)\"" );
200
201 for( std::sregex_iterator it( aRegion.begin(), aRegion.end(), coordinateRegex ), end; it != end; ++it )
202 {
203 if( std::abs( std::stod( ( *it )[1].str() ) - aExpected ) <= aTolerance )
204 return true;
205 }
206
207 return false;
208}
209
210
215static const std::vector<std::string> VALIDATION_TEST_BOARDS = {
216 "custom_pads.kicad_pcb",
217 "notched_zones.kicad_pcb",
218 "sliver.kicad_pcb",
219 "tracks_arcs_vias.kicad_pcb",
220 "issue7241.kicad_pcb",
221 "issue10906.kicad_pcb",
222 "issue22798.kicad_pcb",
223 "padstacks_complex.kicad_pcb",
224 "issue12609.kicad_pcb",
225 "issue22794.kicad_pcb",
226};
227
228} // anonymous namespace
229
230
232{
234 m_xmllintAvailable( IsXmllintAvailable() )
235 {
236 }
237
239 {
240 // Clean up temporary files
241 for( const wxString& path : m_tempFiles )
242 {
243 if( wxFileExists( path ) )
244 wxRemoveFile( path );
245 }
246 }
247
248 wxString CreateTempFile( const wxString& aSuffix = wxT( "" ) )
249 {
250 wxString path = wxFileName::CreateTempFileName( wxT( "kicad_ipc2581_test" ) );
251
252 if( !aSuffix.IsEmpty() )
253 path += aSuffix;
254 else
255 path += wxT( ".xml" );
256
257 m_tempFiles.push_back( path );
258 return path;
259 }
260
261 wxString GetXsdPath( char aVersion ) const
262 {
263 wxString filename = ( aVersion == 'C' ) ? wxT( "IPC-2581C.xsd" ) : wxT( "IPC-2581B1.xsd" );
264 return KI_TEST::GetPcbnewTestDataDir() + "ipc2581/" + filename;
265 }
266
267 bool SchemaCheckEnabled( const wxString& aXsdPath, wxString& aError ) const
268 {
269 aError.clear();
270
271 if( !m_xmllintAvailable )
272 aError = wxS( "xmllint is unavailable" );
273 else if( !wxFileExists( aXsdPath ) )
274 aError = wxS( "IPC-2581 schema is missing: " ) + aXsdPath;
275
276 return aError.IsEmpty();
277 }
278
279 bool SchemaCheckReady( char aVersion ) const
280 {
281 wxString error;
282
283 if( SchemaCheckEnabled( GetXsdPath( aVersion ), error ) )
284 return true;
285
286 if( XmllintRequired() )
287 BOOST_ERROR( "KICAD_QA_REQUIRE_XMLLINT=1: " << error.ToStdString() );
288 else
289 BOOST_WARN_MESSAGE( false, error.ToStdString() );
290
291 return false;
292 }
293
294 std::unique_ptr<BOARD> LoadBoard( const std::string& aRelativePath )
295 {
296 std::string fullPath = KI_TEST::GetPcbnewTestDataDir() + aRelativePath;
297
298 return m_kicadPlugin.LoadBoard( fullPath );
299 }
300
301 bool ExportAndValidate( BOARD& aBoard, char aVersion, wxString& aErrorMsg,
302 const std::string& aMode = std::string(),
303 const std::string& aRefDes = std::string(),
304 const std::string& aSections = std::string() )
305 {
306 wxString tempPath = CreateTempFile();
307
308 std::map<std::string, UTF8> props;
309 props["units"] = "mm";
310 props["version"] = std::string( 1, aVersion );
311 props["sigfig"] = "3";
312
313 if( !aMode.empty() )
314 props["mode"] = aMode;
315
316 if( !aRefDes.empty() )
317 {
318 props["refdes"] = aRefDes;
319 props["netnames"] = "anonymize";
320 }
321
322 if( !aSections.empty() )
323 props["sections"] = aSections;
324
325 try
326 {
327 m_ipc2581Plugin.SaveBoard( tempPath, aBoard, &props );
328 }
329 catch( const std::exception& e )
330 {
331 aErrorMsg = wxString::Format( "Export failed: %s", e.what() );
332 return false;
333 }
334
335 if( !wxFileExists( tempPath ) )
336 {
337 aErrorMsg = "Export file was not created";
338 return false;
339 }
340
341 wxString xsdPath = GetXsdPath( aVersion );
342 wxString unavailable;
343
344 if( !SchemaCheckEnabled( xsdPath, unavailable ) )
345 {
346 if( XmllintRequired() )
347 {
348 aErrorMsg = wxS( "KICAD_QA_REQUIRE_XMLLINT=1: " ) + unavailable;
349 return false;
350 }
351
352 return true;
353 }
354
355 aErrorMsg = ValidateXmlWithXsd( tempPath, xsdPath );
356 return aErrorMsg.IsEmpty();
357 }
358
360 std::vector<wxString> m_tempFiles;
363};
364
365
366BOOST_FIXTURE_TEST_SUITE( Ipc2581Export, IPC2581_EXPORT_FIXTURE )
367
368
369BOOST_AUTO_TEST_CASE( Ipc2581PadHoleMatchesNcDrill )
370{
371 std::unique_ptr<BOARD> board = LoadBoard( "odbpp/pad_roles.kicad_pcb" );
372 BOOST_REQUIRE( board );
373 using HOLE_KEY = std::tuple<int, int, int>;
374 std::multiset<HOLE_KEY> expected;
375 int slots = 0;
376
377 for( bool nonPlated : { false, true } )
378 {
379 DRILL_QUERY query;
380 query.m_Span = DRILL_SPAN( F_Cu, B_Cu, false, nonPlated );
381 query.m_NonPlatedOnly = nonPlated;
382
383 for( const DRILL_OPERATION& op : EnumerateDrillOperations( *board, query ) )
384 {
385 if( op.m_SourceItem->Type() != PCB_PAD_T )
386 continue;
387
388 if( op.m_IsSlot )
389 slots++;
390 else
391 expected.emplace( op.m_Position.x, -op.m_Position.y, op.m_Diameter );
392 }
393 }
394
395 BOOST_REQUIRE_GT( expected.size(), 0 );
396 BOOST_REQUIRE_GT( slots, 0 );
397
398 wxString output = CreateTempFile();
399 std::map<std::string, UTF8> props = { { "units", "mm" }, { "version", "C" }, { "sigfig", "6" } };
400 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( output, *board, &props ) );
401 wxXmlDocument document;
402 BOOST_REQUIRE( document.Load( output ) );
403 std::vector<wxXmlNode*> holes;
404 CollectXmlElements( document.GetRoot(), wxS( "Hole" ), holes );
405 std::multiset<HOLE_KEY> actual;
406
407 auto toIu = []( const wxString& aValue )
408 {
409 return static_cast<int>( std::llround( std::stod( aValue.ToStdString() ) * PCB_IU_PER_MM ) );
410 };
411
412 for( wxXmlNode* hole : holes )
413 {
414 wxString plating = hole->GetAttribute( wxS( "platingStatus" ) );
415
416 if( plating == wxS( "PLATED" ) || plating == wxS( "NONPLATED" ) )
417 {
418 actual.emplace( toIu( hole->GetAttribute( wxS( "x" ) ) ),
419 toIu( hole->GetAttribute( wxS( "y" ) ) ),
420 toIu( hole->GetAttribute( wxS( "diameter" ) ) ) );
421 }
422 }
423
424 BOOST_CHECK( actual == expected );
425}
426
427
428BOOST_AUTO_TEST_CASE( Ipc2581User10FeaturesHaveDeclaredLayer )
429{
430 std::unique_ptr<BOARD> board = LoadBoard( "ipc2581/user10-content.kicad_pcb" );
431 BOOST_REQUIRE( board );
432 BOOST_REQUIRE( board->IsLayerEnabled( User_10 ) );
433
434 bool hasUser10Drawing = false;
435
436 for( BOARD_ITEM* drawing : board->Drawings() )
437 hasUser10Drawing |= drawing->GetLayer() == User_10;
438
439 BOOST_REQUIRE( hasUser10Drawing );
440 wxString output = CreateTempFile();
441 std::map<std::string, UTF8> props = { { "units", "mm" }, { "version", "C" }, { "sigfig", "6" } };
442 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( output, *board, &props ) );
443 wxXmlDocument document;
444 BOOST_REQUIRE( document.Load( output ) );
445 std::vector<wxXmlNode*> layers;
446 std::vector<wxXmlNode*> features;
447 CollectXmlElements( document.GetRoot(), wxS( "Layer" ), layers );
448 CollectXmlElements( document.GetRoot(), wxS( "LayerFeature" ), features );
449 std::set<wxString> names;
450 bool user10Declared = false;
451 bool user10HasFeature = false;
452
453 for( wxXmlNode* layer : layers )
454 {
455 wxString name = layer->GetAttribute( wxS( "name" ) );
456 names.insert( name );
457
458 if( name == wxS( "User.10" ) )
459 {
460 user10Declared = true;
461 BOOST_CHECK_EQUAL( layer->GetAttribute( wxS( "layerFunction" ) ), wxString( wxS( "DOCUMENT" ) ) );
462 BOOST_CHECK_EQUAL( layer->GetAttribute( wxS( "polarity" ) ), wxString( wxS( "POSITIVE" ) ) );
463 BOOST_CHECK_EQUAL( layer->GetAttribute( wxS( "side" ) ), wxString( wxS( "NONE" ) ) );
464 }
465 }
466
467 for( wxXmlNode* feature : features )
468 {
469 wxString reference = feature->GetAttribute( wxS( "layerRef" ) );
470 BOOST_CHECK( !reference.IsEmpty() );
471 BOOST_CHECK( names.count( reference ) != 0 );
472
473 if( reference == wxS( "User.10" ) )
474 user10HasFeature = feature->GetChildren() != nullptr;
475 }
476
477 BOOST_CHECK( user10Declared );
478 BOOST_CHECK( user10HasFeature );
479
480 if( SchemaCheckReady( 'C' ) )
481 {
482 wxString schemaError = ValidateXmlWithXsd( output, GetXsdPath( 'C' ) );
483 BOOST_CHECK_MESSAGE( schemaError.IsEmpty(), schemaError.ToStdString() );
484 }
485}
486
487
488BOOST_AUTO_TEST_CASE( Ipc2581LogicalNetUsesPackagePinName )
489{
490 std::unique_ptr<BOARD> board = LoadBoard( "ipc2581/netted_unnumbered.kicad_pcb" );
491 BOOST_REQUIRE( board );
492 FOOTPRINT* footprint = nullptr;
493 PAD* target = nullptr;
494 size_t padIndex = 0;
495 size_t matches = 0;
496
497 for( FOOTPRINT* fp : board->Footprints() )
498 {
499 for( size_t i = 0; i < fp->Pads().size(); ++i )
500 {
501 PAD* pad = fp->Pads()[i];
502
503 if( pad->GetNumber().IsEmpty() && pad->GetNetCode() > 0 )
504 {
505 ++matches;
506 footprint = fp;
507 target = pad;
508 padIndex = i;
509 }
510 }
511 }
512
513 BOOST_REQUIRE_EQUAL( matches, 1u );
514 wxString expectedPin = MakeFabPin( *target, padIndex ).m_name;
515 BOOST_REQUIRE( expectedPin.StartsWith( wxS( "PAD" ) ) );
516 wxString component = footprint->GetReference();
517 wxString netName = target->GetNetname();
518 BOOST_REQUIRE( component == wxS( "SMD1" ) );
519 BOOST_REQUIRE( netName == wxS( "AVDD" ) );
520 wxString output = CreateTempFile();
521 std::map<std::string, UTF8> props = { { "units", "mm" }, { "version", "C" }, { "sigfig", "6" } };
523 sections.Set( IPC2581::SECTION::LOGICAL_NET );
524 props["sections"] = IPC2581::SectionKeyString( sections ).ToStdString();
525 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( output, *board, &props ) );
526 wxXmlDocument document;
527 BOOST_REQUIRE( document.Load( output ) );
528 std::vector<wxXmlNode*> pins;
529 std::vector<wxXmlNode*> pinRefs;
530 std::vector<wxXmlNode*> logicalNets;
531 CollectXmlElements( document.GetRoot(), wxS( "Pin" ), pins );
532 CollectXmlElements( document.GetRoot(), wxS( "PinRef" ), pinRefs );
533 CollectXmlElements( document.GetRoot(), wxS( "LogicalNet" ), logicalNets );
534 BOOST_REQUIRE_GT( logicalNets.size(), 0u );
535 bool hasPackagePin = false;
536 bool hasPadPinRef = false;
537 bool hasLogicalPinRef = false;
538
539 for( wxXmlNode* pin : pins )
540 hasPackagePin |= pin->GetAttribute( wxS( "number" ) ) == expectedPin;
541
542 for( wxXmlNode* ref : pinRefs )
543 {
544 if( ref->GetAttribute( wxS( "componentRef" ) ) != component
545 || ref->GetAttribute( wxS( "pin" ) ) != expectedPin )
546 {
547 continue;
548 }
549
550 wxXmlNode* parent = ref->GetParent();
551
552 if( parent->GetName() == wxS( "Pad" ) )
553 hasPadPinRef = true;
554 else if( parent->GetName() == wxS( "LogicalNet" ) && parent->GetAttribute( wxS( "name" ) ) == netName )
555 hasLogicalPinRef = true;
556 }
557
558 BOOST_CHECK( hasPackagePin );
559 BOOST_CHECK( hasPadPinRef );
560 BOOST_CHECK( hasLogicalPinRef );
561}
562
563
564BOOST_AUTO_TEST_CASE( Ipc2581ViaProtectionUsesBoardDefaults )
565{
566 std::unique_ptr<BOARD> board = LoadBoard( "drc_via_stack/FillFromBoardHonoursTheBoardDefault.kicad_pcb" );
567 BOOST_REQUIRE( board );
568 const BOARD_DESIGN_SETTINGS& settings = board->GetDesignSettings();
569 BOOST_REQUIRE( settings.m_FillVias );
570 BOOST_REQUIRE( settings.m_TentViasFront );
571 BOOST_REQUIRE( settings.m_TentViasBack );
572 BOOST_REQUIRE_EQUAL( board->Tracks().size(), 2u );
573
574 for( const PCB_TRACK* track : board->Tracks() )
575 {
576 BOOST_REQUIRE( track->Type() == PCB_VIA_T );
577 const PCB_VIA* via = static_cast<const PCB_VIA*>( track );
578 BOOST_REQUIRE( !via->Padstack().IsFilled().has_value() );
579 BOOST_REQUIRE( !via->Padstack().IsTented( F_Cu ).has_value() );
580 }
581
582 wxString output = CreateTempFile();
583 std::map<std::string, UTF8> props = { { "units", "mm" }, { "version", "C" }, { "sigfig", "6" } };
584 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( output, *board, &props ) );
585 wxXmlDocument document;
586 BOOST_REQUIRE( document.Load( output ) );
587 std::vector<wxXmlNode*> layers;
588 CollectXmlElements( document.GetRoot(), wxS( "Layer" ), layers );
589 std::set<wxString> fillingNames;
590 std::set<wxString> tentingNames;
591
592 for( wxXmlNode* layer : layers )
593 {
594 wxString function = layer->GetAttribute( wxS( "layerFunction" ) );
595
596 if( function == wxS( "HOLEFILL" ) && FindXmlChild( layer, wxS( "Span" ) ) )
597 fillingNames.insert( layer->GetAttribute( wxS( "name" ) ) );
598
599 if( function == wxS( "COATINGNONCOND" ) && !FindXmlChild( layer, wxS( "Span" ) ) )
600 tentingNames.insert( layer->GetAttribute( wxS( "name" ) ) );
601 }
602
603 BOOST_REQUIRE_EQUAL( fillingNames.size(), 2u );
604 BOOST_REQUIRE_EQUAL( tentingNames.size(), 1u );
605 std::vector<wxXmlNode*> padDefs;
606 CollectXmlElements( document.GetRoot(), wxS( "PadstackPadDef" ), padDefs );
607 std::set<wxString> padRefs;
608
609 for( wxXmlNode* padDef : padDefs )
610 padRefs.insert( padDef->GetAttribute( wxS( "layerRef" ) ) );
611
612 for( const wxString& name : fillingNames )
613 BOOST_CHECK( padRefs.contains( name ) );
614
615 for( const wxString& name : tentingNames )
616 BOOST_CHECK( padRefs.contains( name ) );
617}
618
619
620BOOST_AUTO_TEST_CASE( CompressedExportKeepsXmlAtArchiveRoot )
621{
622 std::unique_ptr<BOARD> board = LoadBoard( "issue14130.kicad_pcb" );
623 KI_TEST::SCOPED_TEMP_DIR dir( wxT( "ipc_archive" ) );
624 wxString output = wxString( ( dir.Path() / "ipc.zip" ).wstring() );
625 BOOST_REQUIRE( board );
626
628 job.m_compress = true;
629 job.SetConfiguredOutputPath( output );
630
632 BOOST_REQUIRE_MESSAGE( GenerateIpc2581File( job, board.get(), nullptr, &reporter ),
633 reporter.GetMessages().ToStdString() );
634
635 wxFFileInputStream input( output );
636 wxZipInputStream archive( input );
637 std::unique_ptr<wxArchiveEntry> entry( archive.GetNextEntry() );
638 BOOST_REQUIRE( entry );
639 BOOST_CHECK( entry->GetName() == wxS( "ipc.xml" ) );
640 std::unique_ptr<wxArchiveEntry> next( archive.GetNextEntry() );
641 BOOST_CHECK( !next );
642}
643
644
645BOOST_AUTO_TEST_CASE( EmptyIpcSectionSelectionFailsExport )
646{
647 std::unique_ptr<BOARD> board = LoadBoard( "issue14130.kicad_pcb" );
648 BOOST_REQUIRE( board );
649
650 KI_TEST::SCOPED_TEMP_DIR dir( wxT( "ipc_empty_sections" ) );
651 wxString output = wxString( ( dir.Path() / "empty.xml" ).wstring() );
653 job.FromJson( nlohmann::json{ { "mode", "USERDEF" }, { "sections", "" }, { "custom_sections", true } } );
654 job.SetConfiguredOutputPath( output );
655
657 BOOST_CHECK( !GenerateIpc2581File( job, board.get(), nullptr, &reporter ) );
658 BOOST_CHECK( reporter.GetMessages().Contains( wxS( "No IPC-2581 content selected" ) ) );
659 BOOST_CHECK( !wxFileExists( output ) );
660}
661
662
663BOOST_AUTO_TEST_CASE( Ipc2581VariantPropertySelectsPopulation )
664{
665 std::unique_ptr<BOARD> board = LoadBoard( "variant_test/variant_test.kicad_pcb" );
666 BOOST_REQUIRE( board );
667 BOOST_REQUIRE( board->HasVariant( wxS( "Variant A" ) ) );
668 wxString current = board->GetCurrentVariant();
669 KI_TEST::SCOPED_TEMP_DIR dir( wxT( "ipc_variant_property" ) );
670 wxString output = wxString( ( dir.Path() / "variant.xml" ).wstring() );
672 job.m_variantNames = { wxS( "Variant A" ) };
673 job.m_colInternalId = wxS( "Datasheet" );
674 job.SetConfiguredOutputPath( output );
676 BOOST_REQUIRE_MESSAGE( GenerateIpc2581File( job, board.get(), nullptr, &reporter ),
677 reporter.GetMessages().ToStdString() );
678 BOOST_CHECK_EQUAL( board->GetCurrentVariant(), current );
679 wxXmlDocument document;
680 BOOST_REQUIRE( document.Load( output ) );
681 std::vector<wxXmlNode*> refdes;
682 CollectXmlElements( document.GetRoot(), wxS( "RefDes" ), refdes );
683 bool found = false;
684
685 for( wxXmlNode* node : refdes )
686 {
687 if( node->GetAttribute( wxS( "name" ) ) == wxS( "R2" ) )
688 {
689 BOOST_CHECK_EQUAL( node->GetAttribute( wxS( "populate" ) ), wxString( wxS( "true" ) ) );
690 found = true;
691 }
692 }
693
694 BOOST_CHECK( found );
695
696 std::vector<wxXmlNode*> components;
697 CollectXmlElements( document.GetRoot(), wxS( "Component" ), components );
698 bool foundPart = false;
699
700 for( wxXmlNode* node : components )
701 {
702 if( node->GetAttribute( wxS( "refDes" ) ) == wxS( "R1" ) )
703 {
704 BOOST_CHECK_EQUAL( node->GetAttribute( wxS( "part" ) ), wxString( wxS( "test" ) ) );
705 foundPart = true;
706 }
707 }
708
709 BOOST_CHECK( foundPart );
710}
711
712
713BOOST_AUTO_TEST_CASE( IpcBackBlindDrillSpanUsesStackOrder )
714{
715 std::unique_ptr<BOARD> board = LoadBoard( "issue23451/3Rs_bv.kicad_pcb" );
716 BOOST_REQUIRE( board );
717
718 bool hasBackBlindVia = false;
719
720 for( PCB_TRACK* track : board->Tracks() )
721 {
722 if( track->Type() == PCB_VIA_T )
723 {
724 PCB_VIA* via = static_cast<PCB_VIA*>( track );
725 hasBackBlindVia |= via->TopLayer() == In2_Cu && via->BottomLayer() == B_Cu;
726 }
727 }
728
729 BOOST_REQUIRE( hasBackBlindVia );
730
731 wxString output = CreateTempFile();
732 std::map<std::string, UTF8> props = { { "units", "mm" }, { "version", "C" }, { "sigfig", "6" } };
733 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( output, *board, &props ) );
734
735 wxXmlDocument document;
736 BOOST_REQUIRE( document.Load( output ) );
737 std::vector<wxXmlNode*> layers;
738 CollectXmlElements( document.GetRoot(), wxS( "Layer" ), layers );
739 int matchingSpans = 0;
740
741 for( wxXmlNode* layer : layers )
742 {
743 if( layer->GetAttribute( wxS( "layerFunction" ) ) != wxS( "DRILL" ) )
744 continue;
745
746 wxXmlNode* span = FindXmlChild( layer, wxS( "Span" ) );
747
748 if( span && span->GetAttribute( wxS( "fromLayer" ) ) == board->GetLayerName( In2_Cu )
749 && span->GetAttribute( wxS( "toLayer" ) ) == board->GetLayerName( B_Cu ) )
750 {
751 ++matchingSpans;
752 }
753 }
754
755 BOOST_CHECK_EQUAL( matchingSpans, 1 );
756}
757
758
759BOOST_AUTO_TEST_CASE( IpcExportLeavesBoardStackupUntouched )
760{
761 std::unique_ptr<BOARD> board = LoadBoard( "custom_pads.kicad_pcb" );
762 BOOST_REQUIRE( board );
763
764 BOARD_DESIGN_SETTINGS& settings = board->GetDesignSettings();
765 BOARD_STACKUP before = settings.GetStackupDescriptor();
766 std::vector<BOARD_STACKUP_ITEM*> beforeItems = settings.GetStackupDescriptor().GetList();
767 BOOST_REQUIRE( !beforeItems.empty() );
768
769 std::map<std::string, UTF8> props;
770 props["units"] = "mm";
771 props["version"] = "C";
772 props["sigfig"] = "3";
773 m_ipc2581Plugin.SaveBoard( CreateTempFile(), *board, &props );
774
776 BOOST_CHECK( settings.GetStackupDescriptor() == before );
777 BOOST_CHECK( settings.GetStackupDescriptor().GetList() == beforeItems );
778}
779
780
789BOOST_AUTO_TEST_CASE( SurfaceFinishExport )
790{
791 // Load a board with ENIG surface finish (issue3812.kicad_pcb has ENIG)
792 std::unique_ptr<BOARD> board = LoadBoard( "issue3812.kicad_pcb" );
793
794 BOOST_REQUIRE( board );
795
796 // Verify the board has ENIG finish
797 const BOARD_STACKUP& stackup = board->GetDesignSettings().GetStackupDescriptor();
798 BOOST_CHECK_EQUAL( stackup.m_FinishType, wxT( "ENIG" ) );
799
800 // Export to IPC-2581 version C
801 wxString tempPath = CreateTempFile();
802
803 std::map<std::string, UTF8> props;
804 props["units"] = "mm";
805 props["version"] = "C";
806 props["sigfig"] = "3";
807
808 m_ipc2581Plugin.SaveBoard( tempPath, *board, &props );
809
810 BOOST_REQUIRE( wxFileExists( tempPath ) );
811
812 // Verify SurfaceFinish element is present with correct type attribute
813 // Schema requires: <SurfaceFinish type="ENIG-N"/>
814 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "<SurfaceFinish" ) ),
815 "SurfaceFinish element should be present" );
816 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "type=\"ENIG-N\"" ) ),
817 "SurfaceFinish type should be ENIG-N" );
818
819 // Verify coating layers are present
820 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "COATING_TOP" ) ),
821 "COATING_TOP layer should be present" );
822 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "COATING_BOTTOM" ) ),
823 "COATING_BOTTOM layer should be present" );
824 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "layerFunction=\"COATINGCOND\"" ) ),
825 "Coating layers should have layerFunction=COATINGCOND" );
826
827 // Note: XSD validation is done separately in SchemaValidation tests.
828 // This test focuses on verifying the surface finish elements are present.
829}
830
831
835BOOST_AUTO_TEST_CASE( NoSurfaceFinishExport )
836{
837 // Load a board without surface finish (vme-wren.kicad_pcb has "None")
838 std::unique_ptr<BOARD> board = LoadBoard( "vme-wren.kicad_pcb" );
839
840 BOOST_REQUIRE( board );
841
842 // Verify the board has no finish
843 const BOARD_STACKUP& stackup = board->GetDesignSettings().GetStackupDescriptor();
844 BOOST_CHECK( stackup.m_FinishType == wxT( "None" ) || stackup.m_FinishType.IsEmpty() );
845
846 // Export to IPC-2581 version C
847 wxString tempPath = CreateTempFile();
848
849 std::map<std::string, UTF8> props;
850 props["units"] = "mm";
851 props["version"] = "C";
852 props["sigfig"] = "3";
853
854 m_ipc2581Plugin.SaveBoard( tempPath, *board, &props );
855
856 BOOST_REQUIRE( wxFileExists( tempPath ) );
857
858 // Verify SurfaceFinish element is NOT present
859 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "<SurfaceFinish" ) ),
860 "SurfaceFinish element should not be present for 'None' finish" );
861
862 // Verify coating layers are NOT present
863 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "COATING_TOP" ) ),
864 "COATING_TOP layer should not be present" );
865 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "COATING_BOTTOM" ) ),
866 "COATING_BOTTOM layer should not be present" );
867
868 // Note: XSD validation is done separately in SchemaValidation tests.
869 // This test focuses on verifying coating layers are NOT present for "None" finish.
870}
871
872
873BOOST_AUTO_TEST_CASE( RequiredSchemaCheckRejectsMissingXmllint )
874{
875 std::unique_ptr<BOARD> board = LoadBoard( "padstacks_complex.kicad_pcb" );
876 BOOST_REQUIRE( board );
877
878 KI_TEST::SCOPED_PROCESS_ENV_VAR requireXmllint( wxS( "KICAD_QA_REQUIRE_XMLLINT" ), std::nullopt );
879 m_xmllintAvailable = false;
880 wxString errorMsg;
881 BOOST_CHECK( ExportAndValidate( *board, 'C', errorMsg ) );
882 BOOST_CHECK( errorMsg.IsEmpty() );
883
884 requireXmllint.SetValue( wxS( "1" ) );
885 bool valid = ExportAndValidate( *board, 'C', errorMsg );
886 BOOST_CHECK( !valid );
887 BOOST_CHECK( errorMsg.Contains( wxS( "xmllint" ) ) );
888 BOOST_CHECK( errorMsg.Contains( wxS( "KICAD_QA_REQUIRE_XMLLINT" ) ) );
889
890 m_xmllintAvailable = true;
891 wxString missingXsd = GetXsdPath( 'C' ) + wxS( ".missing" );
892 BOOST_CHECK( !SchemaCheckEnabled( missingXsd, errorMsg ) );
893 BOOST_CHECK( errorMsg.Contains( missingXsd ) );
894}
895
896
897BOOST_AUTO_TEST_CASE( SchemaValidationRejectsInvalidDocument )
898{
899 if( !SchemaCheckReady( 'C' ) )
900 return;
901
902 std::unique_ptr<BOARD> board = LoadBoard( "padstacks_complex.kicad_pcb" );
903 BOOST_REQUIRE( board );
904
905 wxString validPath = CreateTempFile();
906 std::map<std::string, UTF8> props = { { "units", "mm" }, { "version", "C" } };
907 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( validPath, *board, &props ) );
908 BOOST_CHECK( ValidateXmlWithXsd( validPath, GetXsdPath( 'C' ) ).IsEmpty() );
909
910 wxXmlDocument document;
911 BOOST_REQUIRE( document.Load( validPath ) );
912 wxXmlNode* content = FindXmlChild( document.GetRoot(), wxS( "Content" ) );
913 BOOST_REQUIRE( content );
914 content->AddChild( new wxXmlNode( wxXML_ELEMENT_NODE, wxS( "KiCadNotInSchema" ) ) );
915
916 wxString invalidPath = CreateTempFile();
917 BOOST_REQUIRE( document.Save( invalidPath ) );
918 wxString error = ValidateXmlWithXsd( invalidPath, GetXsdPath( 'C' ) );
919 BOOST_CHECK( error.Contains( wxS( "KiCadNotInSchema" ) ) );
920}
921
922
923BOOST_AUTO_TEST_CASE( XmllintFailureWithoutStderrIsReported )
924{
925 wxArrayString errors;
926 BOOST_CHECK( XmllintFailureMessage( 0, errors ).IsEmpty() );
927
928 wxString silentFailure = XmllintFailureMessage( -1, errors );
929 BOOST_CHECK( !silentFailure.IsEmpty() );
930 BOOST_CHECK( silentFailure.Contains( wxS( "-1" ) ) );
931
932 errors.Add( wxS( "broken.xml fails to validate" ) );
933 BOOST_CHECK_EQUAL( XmllintFailureMessage( 1, errors ), wxString( wxS( "broken.xml fails to validate\n" ) ) );
934}
935
936
943BOOST_AUTO_TEST_CASE( SchemaValidationVersionB )
944{
945 if( !SchemaCheckReady( 'B' ) )
946 return;
947
948 for( const std::string& boardFile : VALIDATION_TEST_BOARDS )
949 {
950 BOOST_TEST_CONTEXT( "Board: " << boardFile << " (Version B)" )
951 {
952 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
953
954 if( !board )
955 {
956 BOOST_WARN_MESSAGE( false, "Could not load board: " + boardFile );
957 continue;
958 }
959
960 wxString errorMsg;
961 bool valid = ExportAndValidate( *board, 'B', errorMsg );
962
963 BOOST_CHECK_MESSAGE( valid, "IPC-2581B validation failed for " + boardFile + ": " + errorMsg );
964 }
965 }
966}
967
968
975BOOST_AUTO_TEST_CASE( SchemaValidationVersionC )
976{
977 if( !SchemaCheckReady( 'C' ) )
978 return;
979
980 for( const std::string& boardFile : VALIDATION_TEST_BOARDS )
981 {
982 BOOST_TEST_CONTEXT( "Board: " << boardFile << " (Version C)" )
983 {
984 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
985
986 if( !board )
987 {
988 BOOST_WARN_MESSAGE( false, "Could not load board: " + boardFile );
989 continue;
990 }
991
992 wxString errorMsg;
993 bool valid = ExportAndValidate( *board, 'C', errorMsg );
994
995 BOOST_CHECK_MESSAGE( valid, "IPC-2581C validation failed for " + boardFile + ": " + errorMsg );
996 }
997 }
998}
999
1000
1011BOOST_AUTO_TEST_CASE( FunctionModeSchemaValidation )
1012{
1013 if( !SchemaCheckReady( 'C' ) )
1014 return;
1015
1016 static const std::vector<std::string> modes = { "userdef", "bom", "stackup", "fabrication",
1017 "assembly", "test", "stencil" };
1018
1019 // Boards chosen for differing content: inner copper, custom pad shapes, and vias
1020 static const std::vector<std::string> boards = { "padstacks_complex.kicad_pcb",
1021 "custom_pads.kicad_pcb",
1022 "tracks_arcs_vias.kicad_pcb" };
1023
1024 for( const std::string& boardFile : boards )
1025 {
1026 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
1027
1028 if( !board )
1029 {
1030 BOOST_WARN_MESSAGE( false, "Could not load board: " + boardFile );
1031 continue;
1032 }
1033
1034 for( const std::string& mode : modes )
1035 {
1036 for( char version : { 'B', 'C' } )
1037 {
1038 if( !SchemaCheckReady( version ) )
1039 continue;
1040
1041 for( const std::string& refdes : { std::string(), std::string( "omit" ) } )
1042 {
1043 BOOST_TEST_CONTEXT( "Board: " << boardFile << " Mode: " << mode
1044 << " Version: " << version
1045 << " RefDes: " << ( refdes.empty() ? "include" : refdes ) )
1046 {
1047 wxString errorMsg;
1048 bool valid = ExportAndValidate( *board, version, errorMsg, mode,
1049 refdes );
1050
1051 BOOST_CHECK_MESSAGE( valid, "validation failed: " + errorMsg );
1052 }
1053 }
1054 }
1055 }
1056 }
1057}
1058
1059
1064BOOST_AUTO_TEST_CASE( FunctionModeSuppressedReferences )
1065{
1066 if( !SchemaCheckReady( 'C' ) )
1067 return;
1068
1069 // Components without packages, without a BOM, and without padstacks respectively
1070 static const std::vector<std::string> sectionKeys = { "AOU", "ACOU", "KACOU", "ABOU" };
1071
1072 std::unique_ptr<BOARD> board = LoadBoard( "padstacks_complex.kicad_pcb" );
1073
1074 BOOST_REQUIRE( board );
1075
1076 for( const std::string& sections : sectionKeys )
1077 {
1078 for( char version : { 'B', 'C' } )
1079 {
1080 if( !SchemaCheckReady( version ) )
1081 continue;
1082
1083 BOOST_TEST_CONTEXT( "Sections: " << sections << " Version: " << version )
1084 {
1085 wxString errorMsg;
1086 bool valid = ExportAndValidate( *board, version, errorMsg, "userdef",
1087 std::string(), sections );
1088
1089 // Revision B cannot express components without packages and must say so
1090 if( version == 'B' && sections.find( 'C' ) == std::string::npos )
1091 BOOST_CHECK_MESSAGE( !valid, "IPC-2581B accepted components with no package" );
1092 else
1093 BOOST_CHECK_MESSAGE( valid, "validation failed: " + errorMsg );
1094 }
1095 }
1096 }
1097}
1098
1099
1100BOOST_AUTO_TEST_CASE( ComplexBoardExport )
1101{
1102 // Test boards with specific complex features
1103 static const std::vector<std::string> complexBoards = {
1104 "intersectingzones.kicad_pcb",
1105 "custom_pads.kicad_pcb",
1106 };
1107
1108 for( const std::string& boardFile : complexBoards )
1109 {
1110 BOOST_TEST_CONTEXT( "Complex board: " << boardFile )
1111 {
1112 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
1113
1114 if( !board )
1115 {
1116 BOOST_WARN_MESSAGE( false, "Could not load board: " + boardFile );
1117 continue;
1118 }
1119
1120 // Test both versions
1121 for( char version : { 'B', 'C' } )
1122 {
1123 BOOST_TEST_CONTEXT( "Version " << version )
1124 {
1125 wxString errorMsg;
1126 bool valid = ExportAndValidate( *board, version, errorMsg );
1127
1128 BOOST_CHECK_MESSAGE( valid,
1129 wxString::Format( "Export/validation failed for %s version %c: %s",
1130 boardFile, version, errorMsg ) );
1131 }
1132 }
1133 }
1134 }
1135}
1136
1137
1138BOOST_AUTO_TEST_CASE( DegenerateTrackArcExportsAsLine )
1139{
1140 BOARD board;
1141 board.SetCopperLayerCount( 2 );
1142
1143 NETINFO_ITEM* net = new NETINFO_ITEM( &board, wxT( "TestNet" ), 1 );
1144 board.Add( net );
1145
1146 PCB_ARC* arc = new PCB_ARC( &board );
1147 arc->SetStart( VECTOR2I( 110737101, 51206997 ) );
1148 arc->SetMid( VECTOR2I( 110737003, 51206898 ) );
1149 arc->SetEnd( VECTOR2I( 110736905, 51206799 ) );
1150 arc->SetWidth( pcbIUScale.mmToIU( 0.11684 ) );
1151 arc->SetLayer( F_Cu );
1152 arc->SetNet( net );
1153 board.Add( arc );
1154
1155 wxString tempPath = CreateTempFile();
1156
1157 std::map<std::string, UTF8> props;
1158 props["units"] = "mm";
1159 props["version"] = "C";
1160 props["sigfig"] = "6";
1161
1162 m_ipc2581Plugin.SaveBoard( tempPath, board, &props );
1163 BOOST_REQUIRE( wxFileExists( tempPath ) );
1164
1165 BOOST_CHECK( FileContainsPattern( tempPath, wxT( "<Line " ) ) );
1166 BOOST_CHECK( !FileContainsPattern( tempPath, wxT( "<Arc " ) ) );
1167}
1168
1169
1177BOOST_AUTO_TEST_CASE( TextBoxUsesDrawPosition )
1178{
1179 BOARD board;
1180
1181 PCB_TEXTBOX* textbox = new PCB_TEXTBOX( &board );
1182 textbox->SetLayer( F_SilkS );
1183 textbox->SetStart( { pcbIUScale.mmToIU( 100 ), pcbIUScale.mmToIU( 50 ) } );
1184 textbox->SetEnd( { pcbIUScale.mmToIU( 120 ), pcbIUScale.mmToIU( 60 ) } );
1185 textbox->SetText( wxT( "IPC textbox" ) );
1186 textbox->SetTextSize( { pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) } );
1187 textbox->SetTextThickness( pcbIUScale.mmToIU( 0.15 ) );
1190 textbox->SetMarginLeft( 0 );
1191 textbox->SetMarginTop( 0 );
1192 textbox->SetBorderEnabled( false );
1193 board.Add( textbox );
1194
1195 wxString tempPath = CreateTempFile();
1196
1197 std::map<std::string, UTF8> props;
1198 props["units"] = "mm";
1199 props["version"] = "C";
1200 props["sigfig"] = "6";
1201
1202 m_ipc2581Plugin.SaveBoard( tempPath, board, &props );
1203 BOOST_REQUIRE( wxFileExists( tempPath ) );
1204
1205 std::ifstream xmlFile( tempPath.ToStdString() );
1206 BOOST_REQUIRE( xmlFile.is_open() );
1207
1208 std::string xmlContent( ( std::istreambuf_iterator<char>( xmlFile ) ), std::istreambuf_iterator<char>() );
1209 size_t textStart = xmlContent.find( "value=\"IPC textbox\"" );
1210 BOOST_REQUIRE_MESSAGE( textStart != std::string::npos, "Export should contain the text box feature" );
1211
1212 size_t setEnd = xmlContent.find( "</Set>", textStart );
1213 BOOST_REQUIRE( setEnd != std::string::npos );
1214
1215 std::string textFeature = xmlContent.substr( textStart, setEnd - textStart );
1216
1217 BOOST_CHECK_MESSAGE( XmlRegionHasCoordinateNear( textFeature, 'x', 100.0, 5.0 ),
1218 "Text box geometry should use its X drawing position" );
1219 BOOST_CHECK_MESSAGE( XmlRegionHasCoordinateNear( textFeature, 'y', -50.0, 5.0 ),
1220 "Text box geometry should use its Y drawing position" );
1221}
1222
1223
1228BOOST_AUTO_TEST_CASE( SmdPadSolderMaskExport_Issue16658 )
1229{
1230 // Load a board with standard SMD components (capacitors using SMD footprints)
1231 std::unique_ptr<BOARD> board = LoadBoard( "issue16658/issue16658.kicad_pcb" );
1232
1233 BOOST_REQUIRE( board );
1234
1235 bool hasMaskedSmtPad = false;
1236
1237 for( FOOTPRINT* fp : board->Footprints() )
1238 {
1239 for( PAD* pad : fp->Pads() )
1240 {
1241 if( pad->GetAttribute() == PAD_ATTRIB::SMD && pad->IsOnLayer( F_Mask ) )
1242 hasMaskedSmtPad = true;
1243 }
1244 }
1245
1246 BOOST_REQUIRE_MESSAGE( hasMaskedSmtPad, "Test board should have an authored F.Mask SMD pad" );
1247
1248 // Export to IPC-2581 version C
1249 wxString tempPath = CreateTempFile();
1250
1251 std::map<std::string, UTF8> props;
1252 props["units"] = "mm";
1253 props["version"] = "C";
1254 props["sigfig"] = "3";
1255
1256 m_ipc2581Plugin.SaveBoard( tempPath, *board, &props );
1257
1258 BOOST_REQUIRE( wxFileExists( tempPath ) );
1259
1260 // Verify that F_Mask layer features are present in the export
1261 // (this was the bug - mask layers were empty for SMD pads)
1262 bool hasFMaskLayer = FileContainsPattern( tempPath, wxT( "layerRef=\"F.Mask\"" ) )
1263 || FileContainsPattern( tempPath, wxT( "layerRef=\"TSM\"" ) );
1264
1265 BOOST_CHECK_MESSAGE( hasFMaskLayer,
1266 "IPC-2581 export should contain F.Mask layer features for SMD pads" );
1267
1268 // Also check for LayerFeature element with mask layer reference
1269 bool hasLayerFeature = FileContainsPattern( tempPath, wxT( "<LayerFeature" ) );
1270 BOOST_CHECK_MESSAGE( hasLayerFeature, "IPC-2581 export should contain LayerFeature elements" );
1271}
1272
1273
1278static std::string LayerFeatureRegion( const std::string& aXml, const std::string& aLayerRef )
1279{
1280 const std::string open = "<LayerFeature layerRef=\"" + aLayerRef + "\"";
1281 size_t start = aXml.find( open );
1282
1283 if( start == std::string::npos )
1284 return std::string();
1285
1286 size_t end = aXml.find( "</LayerFeature>", start );
1287
1288 if( end == std::string::npos )
1289 return std::string();
1290
1291 return aXml.substr( start, end - start );
1292}
1293
1294
1295BOOST_AUTO_TEST_CASE( TentedThermalViaPadsDoNotOpenMask )
1296{
1297 std::unique_ptr<BOARD> board = LoadBoard( "connect/connect.kicad_pcb" );
1298 BOOST_REQUIRE( board );
1299
1300 wxString tempPath = CreateTempFile();
1301 std::map<std::string, UTF8> props = { { "units", "mm" }, { "version", "C" }, { "sigfig", "6" } };
1302 m_ipc2581Plugin.SaveBoard( tempPath, *board, &props );
1303
1304 std::ifstream stream( tempPath.ToStdString(), std::ios::binary );
1305 BOOST_REQUIRE( stream );
1306 std::string xml( std::istreambuf_iterator<char>{ stream }, std::istreambuf_iterator<char>{} );
1307 const std::string thermalPin = "<PinRef componentRef=\"U102\" pin=\"65\"";
1308 auto countThermalPins = [&]( const std::string& aLayer )
1309 {
1310 std::string mask = LayerFeatureRegion( xml, aLayer );
1311 BOOST_REQUIRE( !mask.empty() );
1312 size_t count = 0;
1313 size_t pos = 0;
1314
1315 while( ( pos = mask.find( thermalPin, pos ) ) != std::string::npos )
1316 {
1317 ++count;
1318 pos += thermalPin.size();
1319 }
1320
1321 return count;
1322 };
1323
1324 BOOST_CHECK_EQUAL( countThermalPins( "F.Mask" ), 1u );
1325 BOOST_CHECK_EQUAL( countThermalPins( "B.Mask" ), 0u );
1326}
1327
1328
1336BOOST_AUTO_TEST_CASE( EmptyRefDesProducesValidXml )
1337{
1338 std::unique_ptr<BOARD> board = LoadBoard( "padstacks_complex.kicad_pcb" );
1339 BOOST_REQUIRE( board );
1340
1341 for( char version : { 'B', 'C' } )
1342 {
1343 BOOST_TEST_CONTEXT( "Version " << version )
1344 {
1345 wxString tempPath = CreateTempFile();
1346
1347 std::map<std::string, UTF8> props;
1348 props["units"] = "mm";
1349 props["version"] = std::string( 1, version );
1350 props["sigfig"] = "3";
1351
1352 m_ipc2581Plugin.SaveBoard( tempPath, *board, &props );
1353 BOOST_REQUIRE( wxFileExists( tempPath ) );
1354
1355 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "refDes=\"\"" ) ),
1356 "Empty refDes attribute found" );
1357 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "<RefDes name=\"\"" ) ),
1358 "Empty RefDes/@name attribute found" );
1359 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "componentRef=\"\"" ) ),
1360 "Empty PinRef/@componentRef attribute found" );
1361 }
1362
1363 m_ipc2581Plugin = PCB_IO_IPC2581();
1364 }
1365}
1366
1367
1376BOOST_AUTO_TEST_CASE( FlippedComponentRotation )
1377{
1378 std::unique_ptr<BOARD> board = LoadBoard( "issue12609.kicad_pcb" );
1379 BOOST_REQUIRE( board );
1380
1381 wxString tempPath = CreateTempFile();
1382
1383 std::map<std::string, UTF8> props;
1384 props["units"] = "mm";
1385 props["version"] = "C";
1386 props["sigfig"] = "3";
1387
1388 m_ipc2581Plugin.SaveBoard( tempPath, *board, &props );
1389 BOOST_REQUIRE( wxFileExists( tempPath ) );
1390
1391 // C5 is on B.Cu at 90 degrees. Its Component Xform must be rotation="90.0",
1392 // not the inverted "270.0". Check by finding Component refDes="C5" and verifying
1393 // its Xform has rotation="90.0".
1394 std::ifstream xmlFile( tempPath.ToStdString() );
1395 BOOST_REQUIRE( xmlFile.is_open() );
1396
1397 std::string xmlContent( ( std::istreambuf_iterator<char>( xmlFile ) ),
1398 std::istreambuf_iterator<char>() );
1399
1400 // Find the C5 component and check its rotation
1401 size_t c5Pos = xmlContent.find( "refDes=\"C5\"" );
1402
1403 if( c5Pos == std::string::npos )
1404 c5Pos = xmlContent.find( "refDes=\"NOREF_" );
1405
1406 BOOST_REQUIRE_MESSAGE( c5Pos != std::string::npos,
1407 "C5 component should exist in export" );
1408
1409 // Look for the Xform within the next 200 chars after refDes="C5"
1410 std::string c5Region = xmlContent.substr( c5Pos, 200 );
1411 BOOST_CHECK_MESSAGE( c5Region.find( "rotation=\"90.0\"" ) != std::string::npos
1412 || c5Region.find( "rotation=\"90.00\"" ) != std::string::npos,
1413 "C5 component rotation should be 90, not inverted. Region: "
1414 + c5Region );
1415}
1416
1417
1421BOOST_AUTO_TEST_CASE( ContentBomRef )
1422{
1423 std::unique_ptr<BOARD> board = LoadBoard( "issue12609.kicad_pcb" );
1424 BOOST_REQUIRE( board );
1425
1426 wxString tempPath = CreateTempFile();
1427
1428 std::map<std::string, UTF8> props;
1429 props["units"] = "mm";
1430 props["version"] = "C";
1431 props["sigfig"] = "3";
1432
1433 m_ipc2581Plugin.SaveBoard( tempPath, *board, &props );
1434 BOOST_REQUIRE( wxFileExists( tempPath ) );
1435
1436 bool hasBom = FileContainsPattern( tempPath, wxT( "<Bom " ) );
1437 bool hasBomRef = FileContainsPattern( tempPath, wxT( "<BomRef " ) );
1438
1439 if( hasBom )
1440 {
1441 BOOST_CHECK_MESSAGE( hasBomRef,
1442 "Content should have BomRef when Bom section is present" );
1443 }
1444}
1445
1446
1460BOOST_AUTO_TEST_CASE( KnockoutTextMultiContour_Issue23968 )
1461{
1462 std::unique_ptr<BOARD> board = LoadBoard( "test_copper_graphics.kicad_pcb" );
1463 BOOST_REQUIRE( board );
1464
1465 for( char version : { 'B', 'C' } )
1466 {
1467 BOOST_TEST_CONTEXT( "Version " << version )
1468 {
1469 wxString errorMsg;
1470 bool valid = ExportAndValidate( *board, version, errorMsg );
1471
1472 BOOST_CHECK_MESSAGE( valid,
1473 wxString::Format( "Knockout text export should be schema-valid "
1474 "(version %c): %s", version, errorMsg ) );
1475 }
1476
1477 m_ipc2581Plugin = PCB_IO_IPC2581();
1478 }
1479}
1480
1481
1483BOOST_AUTO_TEST_CASE( SecondaryDrillPostMachiningSpecs )
1484{
1485 std::unique_ptr<BOARD> board = LoadBoard( "odbpp/pad_backdrill.kicad_pcb" );
1486 BOOST_REQUIRE( board );
1487 const PAD* machinedPad = dynamic_cast<const PAD*>(
1488 board->ResolveItem( KIID( wxS( "34428461-7652-43ea-869c-ef8abe86da6c" ) ), true ) );
1489
1490 BOOST_REQUIRE( machinedPad );
1493
1494 wxString output = CreateTempFile();
1495 std::map<std::string, UTF8> props = { { "units", "mm" }, { "version", "C" }, { "sigfig", "6" } };
1496 IPC2581::SECTION_SET sections{ IPC2581::SECTION::PADSTACKS, IPC2581::SECTION::DRILL_ROUT,
1497 IPC2581::SECTION::OUTER_COPPER, IPC2581::SECTION::INNER_COPPER };
1498 props["sections"] = IPC2581::SectionKeyString( sections ).ToStdString();
1499 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( output, *board, &props ) );
1500 wxXmlDocument document;
1501 BOOST_REQUIRE( document.Load( output ) );
1502 std::vector<wxXmlNode*> specs;
1503 std::vector<wxXmlNode*> holes;
1504 CollectXmlElements( document.GetRoot(), wxS( "Spec" ), specs );
1505 CollectXmlElements( document.GetRoot(), wxS( "Hole" ), holes );
1506 std::map<wxString, wxXmlNode*> specsByName;
1507
1508 for( wxXmlNode* spec : specs )
1509 specsByName.emplace( spec->GetAttribute( wxS( "name" ) ), spec );
1510
1511 int matchingHoles = 0;
1512
1513 for( wxXmlNode* hole : holes )
1514 {
1515 std::map<wxString, wxXmlNode*> bySide;
1516
1517 for( wxXmlNode* ref = hole->GetChildren(); ref; ref = ref->GetNext() )
1518 {
1519 if( ref->GetName() != wxS( "SpecRef" ) )
1520 continue;
1521
1522 auto spec = specsByName.find( ref->GetAttribute( wxS( "id" ) ) );
1523
1524 if( spec == specsByName.end() )
1525 continue;
1526
1527 wxXmlNode* side = SecondaryDrillProperty( spec->second, wxS( "SIDE" ) );
1528
1529 if( side )
1530 bySide.emplace( side->GetAttribute( wxS( "text" ) ), spec->second );
1531 }
1532
1533 if( bySide.count( wxS( "TOP" ) ) == 0 || bySide.count( wxS( "BOTTOM" ) ) == 0 )
1534 continue;
1535
1536 ++matchingHoles;
1537
1538 for( const auto& [side, spec] : bySide )
1539 {
1540 wxXmlNode* diameter = SecondaryDrillProperty( spec, wxS( "MAJOR_DIAMETER" ) );
1541 BOOST_REQUIRE( diameter );
1542 BOOST_CHECK_EQUAL( diameter->GetAttribute( wxS( "unit" ) ), wxString( wxS( "MM" ) ) );
1543 double value = 0;
1544 BOOST_REQUIRE( diameter->GetAttribute( wxS( "value" ) ).ToDouble( &value ) );
1545 BOOST_CHECK_SMALL( value - ( side == wxS( "TOP" ) ? 1.2 : 1.4 ), 0.000001 );
1546
1547 wxXmlNode* depth = SecondaryDrillProperty( spec, wxS( "DEPTH" ) );
1548 wxXmlNode* angle = SecondaryDrillProperty( spec, wxS( "ANGLE" ) );
1549
1550 if( side == wxS( "TOP" ) )
1551 {
1552 BOOST_CHECK( depth == nullptr );
1553 BOOST_REQUIRE( angle );
1554 BOOST_CHECK_EQUAL( angle->GetAttribute( wxS( "unit" ) ), wxString( wxS( "DEGREES" ) ) );
1555 BOOST_REQUIRE( angle->GetAttribute( wxS( "value" ) ).ToDouble( &value ) );
1556 BOOST_CHECK_SMALL( value - 90.0, 0.000001 );
1557 }
1558 else
1559 {
1560 BOOST_CHECK( angle == nullptr );
1561 BOOST_REQUIRE( depth );
1562 BOOST_CHECK_EQUAL( depth->GetAttribute( wxS( "unit" ) ), wxString( wxS( "MM" ) ) );
1563 BOOST_REQUIRE( depth->GetAttribute( wxS( "value" ) ).ToDouble( &value ) );
1564 BOOST_CHECK_SMALL( value - 0.3, 0.000001 );
1565 }
1566 }
1567 }
1568
1569 BOOST_CHECK_EQUAL( matchingHoles, 1 );
1570 BOOST_CHECK( !FileContainsPattern( output, wxT( "post-machining=" ) ) );
1571
1572 if( SchemaCheckReady( 'C' ) )
1573 {
1574 wxString schemaError = ValidateXmlWithXsd( output, GetXsdPath( 'C' ) );
1575 BOOST_CHECK_MESSAGE( schemaError.IsEmpty(), schemaError.ToStdString() );
1576 }
1577
1578 wxString bOutput = CreateTempFile();
1579 props["version"] = "B";
1580 props.erase( "sections" );
1581 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( bOutput, *board, &props ) );
1582 BOOST_CHECK( !FileContainsPattern( bOutput, wxT( "<SecondaryDrill" ) ) );
1583 BOOST_CHECK( !FileContainsPattern( bOutput, wxT( "post-machining=" ) ) );
1584}
1585
1586
1588BOOST_AUTO_TEST_CASE( SlotCavityUsesDrillPosition )
1589{
1590 std::unique_ptr<BOARD> board = LoadBoard( "issue25101.kicad_pcb" );
1591 BOOST_REQUIRE( board );
1592 const PAD* pad = dynamic_cast<const PAD*>(
1593 board->ResolveItem( KIID( wxS( "3fad97f9-a9d0-4388-8119-c4cdfce21fd9" ) ), true ) );
1594 BOOST_REQUIRE( pad );
1596 VECTOR2I offset = pad->GetOffset( pad->Padstack().EffectiveLayerFor( pad->GetLayer() ) );
1597 BOOST_REQUIRE( offset.x != 0 || offset.y != 0 );
1598
1599 wxString output = CreateTempFile();
1600 std::map<std::string, UTF8> props = { { "units", "mm" }, { "version", "C" }, { "sigfig", "6" } };
1601 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( output, *board, &props ) );
1602 wxXmlDocument document;
1603 BOOST_REQUIRE( document.Load( output ) );
1604 std::vector<wxXmlNode*> slots;
1605 CollectXmlElements( document.GetRoot(), wxS( "SlotCavity" ), slots );
1606 BOOST_REQUIRE_GE( slots.size(), 4u );
1607 VECTOR2I holeCenter = pad->GetEffectiveHoleShape()->GetCenter();
1608 double expectedX = pcbIUScale.IUTomm( holeCenter.x );
1609 double expectedY = -pcbIUScale.IUTomm( holeCenter.y );
1610 double nearest = 1e9;
1611
1612 for( wxXmlNode* slot : slots )
1613 {
1614 wxXmlNode* location = FindXmlChild( slot, wxS( "Location" ) );
1616 double x = 0;
1617 double y = 0;
1618 BOOST_REQUIRE( location->GetAttribute( wxS( "x" ) ).ToDouble( &x ) );
1619 BOOST_REQUIRE( location->GetAttribute( wxS( "y" ) ).ToDouble( &y ) );
1620 nearest = std::min( nearest, std::hypot( x - expectedX, y - expectedY ) );
1621 }
1622
1623 BOOST_CHECK_SMALL( nearest, 0.001 );
1624}
1625
1626
1628BOOST_AUTO_TEST_CASE( SlotCavityFullBoardSchema )
1629{
1630 std::unique_ptr<BOARD> board = LoadBoard( "odbpp/pad_backdrill.kicad_pcb" );
1631 BOOST_REQUIRE( board );
1632
1633 for( char version : { 'B', 'C' } )
1634 {
1635 wxString output = CreateTempFile();
1636 std::map<std::string, UTF8> props = { { "units", "mm" },
1637 { "version", version == 'B' ? "B" : "C" },
1638 { "sigfig", "6" } };
1639 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( output, *board, &props ) );
1640
1641 if( version == 'B' )
1642 {
1643 wxXmlDocument document;
1644 BOOST_REQUIRE( document.Load( output ) );
1645 std::vector<wxXmlNode*> slots;
1646 CollectXmlElements( document.GetRoot(), wxS( "SlotCavity" ), slots );
1647 BOOST_REQUIRE_GE( slots.size(), 1u );
1648
1649 for( wxXmlNode* slot : slots )
1650 {
1651 BOOST_REQUIRE( FindXmlChild( slot, wxS( "Outline" ) ) );
1652
1653 for( wxXmlNode* child = slot->GetChildren(); child; child = child->GetNext() )
1654 {
1655 if( child->GetType() == wxXML_ELEMENT_NODE )
1656 BOOST_CHECK( child->GetName() == wxS( "Outline" ) );
1657 }
1658 }
1659 }
1660
1661 if( !SchemaCheckReady( version ) )
1662 return;
1663
1664 wxString schemaError = ValidateXmlWithXsd( output, GetXsdPath( version ) );
1665 BOOST_CHECK_MESSAGE( schemaError.IsEmpty(), "Revision " << version << ": " << schemaError.ToStdString() );
1666 }
1667}
1668
1669
1681BOOST_AUTO_TEST_CASE( BackdrillSpecEncoding )
1682{
1683 // Build a minimal 6-layer synthetic board so we can place a via whose
1684 // backdrill targets a specific must-cut layer.
1685 BOARD board;
1686 board.SetCopperLayerCount( 6 );
1687
1690
1691 // Front-side backdrill: drill from F_Cu, must cut through In3_Cu. The
1692 // must-not-cut layer should therefore resolve to In4_Cu (the next signal
1693 // layer past must-cut going inward from the start surface).
1694 PCB_VIA* via = new PCB_VIA( &board );
1695 via->SetPadstackMode( PADSTACK::MODE::NORMAL );
1696 via->SetPosition( VECTOR2I( pcbIUScale.mmToIU( 5 ), pcbIUScale.mmToIU( 5 ) ) );
1697 via->SetLayerPair( F_Cu, B_Cu );
1698 via->SetDrill( pcbIUScale.mmToIU( 0.30 ) );
1699 via->SetWidth( PADSTACK::ALL_LAYERS, pcbIUScale.mmToIU( 0.60 ) );
1700 via->SetSecondaryDrillSize( pcbIUScale.mmToIU( 0.40 ) );
1701 via->SetSecondaryDrillStartLayer( F_Cu );
1702 via->SetSecondaryDrillEndLayer( In3_Cu );
1703 via->SetFrontPostMachiningMode( PAD_DRILL_POST_MACHINING_MODE::COUNTERSINK );
1704 board.Add( via );
1705
1706 wxString tempPath = CreateTempFile();
1707 std::map<std::string, UTF8> props;
1708 props["units"] = "mm";
1709 props["version"] = "C";
1710 props["sigfig"] = "4";
1711
1712 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, board, &props ) );
1713 BOOST_REQUIRE( wxFileExists( tempPath ) );
1714
1715 BOOST_CHECK_MESSAGE(
1716 FileContainsPattern( tempPath, wxT( "<Backdrill type=\"START_LAYER\"" ) ),
1717 "Backdrill spec should declare a START_LAYER child" );
1718 BOOST_CHECK_MESSAGE(
1719 FileContainsPattern( tempPath, wxT( "<Backdrill type=\"MUST_NOT_CUT_LAYER\"" ) ),
1720 "Backdrill spec should declare a MUST_NOT_CUT_LAYER child" );
1721 BOOST_CHECK_MESSAGE(
1722 FileContainsPattern( tempPath, wxT( "<Backdrill type=\"MAX_STUB_LENGTH\"" ) ),
1723 "Backdrill spec should declare a MAX_STUB_LENGTH child" );
1724
1725 // Schema requires layer references via Property layerOrGroupRef, not as
1726 // Backdrill attributes.
1727 BOOST_CHECK_MESSAGE( FileContainsPattern( tempPath, wxT( "layerOrGroupRef=" ) ),
1728 "Backdrill must convey layers through Property layerOrGroupRef" );
1729 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "startLayerRef=" ) ),
1730 "Backdrill should not use schema-invalid startLayerRef attribute" );
1731 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "mustNotCutLayerRef=" ) ),
1732 "Backdrill should not use schema-invalid mustNotCutLayerRef attribute" );
1733 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "maxStubLength=" ) ),
1734 "Backdrill should not use schema-invalid maxStubLength attribute" );
1735 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "postMachining=" ) ),
1736 "Backdrill should not use the non-standard postMachining attribute" );
1737
1738 // The must-not-cut layer for a backdrill from F_Cu through In3_Cu in a
1739 // 6-layer stack must resolve to In4_Cu, not the must-cut layer itself.
1740 BOOST_CHECK_MESSAGE(
1741 FileContainsPattern( tempPath, wxT( "layerOrGroupRef=\"In4.Cu\"" ) ),
1742 "Front backdrill must-not-cut layer should be In4.Cu" );
1743
1744 // The third (primary) backdrill spec slot has been removed; through-drills
1745 // must not be exported as backdrill specs.
1746 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "BD_1C" ) ),
1747 "Exporter should not emit a primary backdrill spec slot" );
1748
1749 // A mode without a positive diameter does not create a secondary drill
1750 BOOST_CHECK( !FileContainsPattern( tempPath, wxT( "<Backdrill type=\"OTHER\"" ) ) );
1751 BOOST_CHECK( !FileContainsPattern( tempPath, wxT( "<SecondaryDrill" ) ) );
1752}
1753
1754
1759BOOST_AUTO_TEST_CASE( ExposedPadPasteRespected_Issue24318 )
1760{
1761 std::unique_ptr<BOARD> board = LoadBoard( "connect/connect.kicad_pcb" );
1762 BOOST_REQUIRE( board );
1763 bool thermalMask = false;
1764 bool normalPaste = false;
1765 bool copperOnly = false;
1766 bool maskAperture = false;
1767
1768 for( FOOTPRINT* footprint : board->Footprints() )
1769 {
1770 for( PAD* pad : footprint->Pads() )
1771 {
1772 if( footprint->GetReference() == wxS( "U102" ) )
1773 {
1774 if( pad->GetNumber() == wxS( "65" ) && pad->IsOnLayer( F_Mask ) )
1775 {
1776 thermalMask = true;
1777 BOOST_CHECK( !pad->IsOnLayer( F_Paste ) );
1778 }
1779
1780 if( pad->GetNumber() == wxS( "1" ) && pad->IsOnLayer( F_Paste ) )
1781 normalPaste = true;
1782 }
1783 else if( footprint->GetReference() == wxS( "J301" ) )
1784 {
1785 if( pad->GetNumber() == wxS( "1" ) && pad->IsOnLayer( F_Cu )
1786 && !pad->IsOnLayer( F_Mask ) && !pad->IsOnLayer( F_Paste ) )
1787 {
1788 copperOnly = true;
1789 }
1790
1791 if( pad->IsOnLayer( F_Mask ) && !pad->IsOnLayer( F_Cu ) )
1792 maskAperture = true;
1793 }
1794 }
1795 }
1796
1797 BOOST_REQUIRE( thermalMask );
1798 BOOST_REQUIRE( normalPaste );
1799 BOOST_REQUIRE( copperOnly );
1800 BOOST_REQUIRE( maskAperture );
1801 wxString tempPath = CreateTempFile();
1802 std::map<std::string, UTF8> props = { { "units", "mm" }, { "version", "C" }, { "sigfig", "4" } };
1803 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, *board, &props ) );
1804
1805 std::ifstream stream( tempPath.ToStdString(), std::ios::binary );
1806 BOOST_REQUIRE( stream );
1807 std::string xml( std::istreambuf_iterator<char>{ stream }, std::istreambuf_iterator<char>{} );
1808 std::string paste = LayerFeatureRegion( xml, "F.Paste" );
1809 std::string mask = LayerFeatureRegion( xml, "F.Mask" );
1810 BOOST_REQUIRE( !paste.empty() );
1811 BOOST_REQUIRE( !mask.empty() );
1812
1813 BOOST_CHECK( paste.find( "<PinRef componentRef=\"U102\" pin=\"65\"" ) == std::string::npos );
1814 BOOST_CHECK( paste.find( "<PinRef componentRef=\"U102\" pin=\"1\"" ) != std::string::npos );
1815 BOOST_CHECK( paste.find( "<PinRef componentRef=\"J301\" pin=\"1\"" ) == std::string::npos );
1816 BOOST_CHECK( mask.find( "<PinRef componentRef=\"U102\" pin=\"65\"" ) != std::string::npos );
1817 BOOST_CHECK( mask.find( "<PinRef componentRef=\"J301\" pin=\"1\"" ) == std::string::npos );
1818}
1819
1820
1828BOOST_AUTO_TEST_CASE( GrRectCornerRadius_Issue24754 )
1829{
1830 BOARD board;
1831 board.SetCopperLayerCount( 2 );
1832
1833 PCB_SHAPE* rect = new PCB_SHAPE( &board, SHAPE_T::RECTANGLE );
1834 rect->SetLayer( Edge_Cuts );
1835 rect->SetStart( VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 10 ) ) );
1836 rect->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 11.5 ), pcbIUScale.mmToIU( 17 ) ) );
1837 rect->SetStroke( STROKE_PARAMS( pcbIUScale.mmToIU( 0.05 ), LINE_STYLE::SOLID ) );
1838 rect->SetFilled( false );
1839 rect->SetCornerRadius( pcbIUScale.mmToIU( 0.75 ) );
1840 board.Add( rect );
1841
1842 wxString tempPath = CreateTempFile();
1843 std::map<std::string, UTF8> props;
1844 props["units"] = "mm";
1845 props["version"] = "C";
1846 props["sigfig"] = "4";
1847
1848 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, board, &props ) );
1849 BOOST_REQUIRE( wxFileExists( tempPath ) );
1850
1851 std::string xml = KI_TEST::LoadStringData( tempPath );
1852
1853 // The RectRound must round its corners and carry the 0.75 mm radius, not stroke_width/2.
1854 size_t rectPos = xml.find( "<RectRound" );
1855 BOOST_REQUIRE_MESSAGE( rectPos != std::string::npos, "Export should contain a RectRound" );
1856
1857 std::string rectNode = xml.substr( rectPos, xml.find( '>', rectPos ) - rectPos );
1858
1859 BOOST_CHECK_MESSAGE( rectNode.find( "radius=\"0.750\"" ) != std::string::npos,
1860 "Rounded gr_rect must export its 0.75 mm corner radius. Node: "
1861 + rectNode );
1862 BOOST_CHECK_MESSAGE( rectNode.find( "upperRight=\"true\"" ) != std::string::npos
1863 && rectNode.find( "lowerLeft=\"true\"" ) != std::string::npos,
1864 "Rounded gr_rect must set its corner flags. Node: " + rectNode );
1865 BOOST_CHECK_MESSAGE( rectNode.find( "radius=\"0.025\"" ) == std::string::npos,
1866 "Corner radius must not collapse to stroke_width/2. Node: " + rectNode );
1867}
1868
1869
1876BOOST_AUTO_TEST_CASE( BackOnlyMaskNoFrontOpening_Issue24753 )
1877{
1878 BOARD board;
1879 board.SetCopperLayerCount( 2 );
1880
1881 FOOTPRINT* fp = new FOOTPRINT( &board );
1882 fp->SetReference( wxT( "J29" ) );
1883 fp->SetPosition( VECTOR2I( pcbIUScale.mmToIU( 50 ), pcbIUScale.mmToIU( 50 ) ) );
1884 board.Add( fp );
1885
1886 // Through-hole pad masked on the back only: *.Cu + B.Mask, deliberately no F.Mask.
1887 PAD* pad = new PAD( fp );
1888 pad->SetPadstackMode( PADSTACK::MODE::NORMAL );
1889 pad->SetNumber( wxT( "1" ) );
1890 pad->SetAttribute( PAD_ATTRIB::PTH );
1892 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 2.5 ), pcbIUScale.mmToIU( 4.0 ) ) );
1893 pad->SetDrillSize( VECTOR2I( pcbIUScale.mmToIU( 1.65 ), pcbIUScale.mmToIU( 1.65 ) ) );
1894 pad->SetLayerSet( LSET( { F_Cu, B_Cu, B_Mask } ) );
1895 fp->Add( pad );
1896
1897 wxString tempPath = CreateTempFile();
1898 std::map<std::string, UTF8> props;
1899 props["units"] = "mm";
1900 props["version"] = "C";
1901 props["sigfig"] = "4";
1902
1903 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, board, &props ) );
1904 BOOST_REQUIRE( wxFileExists( tempPath ) );
1905
1906 std::string xml = KI_TEST::LoadStringData( tempPath );
1907
1908 // The pad must appear on B.Mask (authored) but never on F.Mask.
1909 std::string fMask = LayerFeatureRegion( xml, "F.Mask" );
1910 BOOST_CHECK_MESSAGE( fMask.find( "<PinRef componentRef=\"J29\" pin=\"1\"" ) == std::string::npos,
1911 "Back-only masked pad must not appear on F.Mask layer feature" );
1912
1913 std::string bMask = LayerFeatureRegion( xml, "B.Mask" );
1914 BOOST_CHECK_MESSAGE( bMask.find( "<PinRef componentRef=\"J29\" pin=\"1\"" ) != std::string::npos,
1915 "Back-only masked pad should appear on B.Mask layer feature" );
1916}
1917
1918
1926BOOST_AUTO_TEST_CASE( RoundRectMaskRadius_Issue24751 )
1927{
1928 BOARD board;
1929 board.SetCopperLayerCount( 2 );
1930
1931 FOOTPRINT* fp = new FOOTPRINT( &board );
1932 fp->SetReference( wxT( "R1" ) );
1933 fp->SetPosition( VECTOR2I( pcbIUScale.mmToIU( 40 ), pcbIUScale.mmToIU( 40 ) ) );
1934 board.Add( fp );
1935
1936 PAD* pad = new PAD( fp );
1937 pad->SetPadstackMode( PADSTACK::MODE::NORMAL );
1938 pad->SetNumber( wxT( "1" ) );
1939 pad->SetAttribute( PAD_ATTRIB::SMD );
1941 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 0.45 ), pcbIUScale.mmToIU( 0.30 ) ) );
1942 pad->SetRoundRectRadiusRatio( PADSTACK::ALL_LAYERS, 0.3333333333 );
1943 pad->SetLayerSet( LSET( { F_Cu, F_Mask } ) );
1944 pad->SetLocalSolderMaskMargin( pcbIUScale.mmToIU( -0.05 ) );
1945 fp->Add( pad );
1946
1947 wxString tempPath = CreateTempFile();
1948 std::map<std::string, UTF8> props;
1949 props["units"] = "mm";
1950 props["version"] = "C";
1951 props["sigfig"] = "4";
1952
1953 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, board, &props ) );
1954 BOOST_REQUIRE( wxFileExists( tempPath ) );
1955
1956 std::string xml = KI_TEST::LoadStringData( tempPath );
1957
1958 // Copper roundrect radius is 0.10 mm; with a -0.05 mm per-side margin the mask aperture is
1959 // 0.35 x 0.20 with radius 0.05 mm. Both distinct primitives must be present.
1960 BOOST_CHECK_MESSAGE( xml.find( "radius=\"0.10\"" ) != std::string::npos,
1961 "Copper roundrect should keep its 0.10 mm radius" );
1962 BOOST_CHECK_MESSAGE( xml.find( "radius=\"0.050\"" ) != std::string::npos,
1963 "Mask roundrect aperture must shrink its radius to 0.05 mm" );
1964 BOOST_CHECK_MESSAGE( xml.find( "width=\"0.350\"" ) != std::string::npos,
1965 "Mask roundrect aperture should be 0.35 mm wide" );
1966}
1967
1968
1975BOOST_AUTO_TEST_CASE( MultiLayerFootprintGraphic_Issue24752 )
1976{
1977 BOARD board;
1978 board.SetCopperLayerCount( 2 );
1979
1980 FOOTPRINT* fp = new FOOTPRINT( &board );
1981 fp->SetReference( wxT( "G1" ) );
1982 fp->SetPosition( VECTOR2I( pcbIUScale.mmToIU( 30 ), pcbIUScale.mmToIU( 30 ) ) );
1983 board.Add( fp );
1984
1985 PCB_SHAPE* poly = new PCB_SHAPE( fp, SHAPE_T::POLY );
1986 poly->SetLayerSet( LSET( { F_Cu, F_Mask } ) );
1987 poly->SetFilled( true );
1989
1990 SHAPE_POLY_SET polySet;
1991 polySet.NewOutline();
1992 polySet.Append( pcbIUScale.mmToIU( 30 ), pcbIUScale.mmToIU( 30 ) );
1993 polySet.Append( pcbIUScale.mmToIU( 31 ), pcbIUScale.mmToIU( 30 ) );
1994 polySet.Append( pcbIUScale.mmToIU( 31 ), pcbIUScale.mmToIU( 31 ) );
1995 polySet.Append( pcbIUScale.mmToIU( 30 ), pcbIUScale.mmToIU( 31 ) );
1996 poly->SetPolyShape( polySet );
1997 fp->Add( poly );
1998
1999 wxString tempPath = CreateTempFile();
2000 std::map<std::string, UTF8> props;
2001 props["units"] = "mm";
2002 props["version"] = "C";
2003 props["sigfig"] = "4";
2004
2005 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, board, &props ) );
2006 BOOST_REQUIRE( wxFileExists( tempPath ) );
2007
2008 std::string xml = KI_TEST::LoadStringData( tempPath );
2009
2010 // The polygon graphic must show up under both F.Cu and F.Mask.
2011 std::string fCu = LayerFeatureRegion( xml, "F.Cu" );
2012 std::string fMask = LayerFeatureRegion( xml, "F.Mask" );
2013
2014 BOOST_CHECK_MESSAGE( fCu.find( "UserPrimitiveRef" ) != std::string::npos,
2015 "Footprint graphic should be present on F.Cu" );
2016 BOOST_CHECK_MESSAGE( fMask.find( "UserPrimitiveRef" ) != std::string::npos,
2017 "Multi-layer footprint graphic must also appear on F.Mask" );
2018}
2019
2020
2030BOOST_AUTO_TEST_CASE( SolderMaskMarginExpandsMaskPrimitive_Issue24749 )
2031{
2032 BOARD board;
2033
2034 FOOTPRINT* fp = new FOOTPRINT( &board );
2035 fp->SetReference( wxT( "FID4" ) );
2036 fp->SetPosition( VECTOR2I( pcbIUScale.mmToIU( 50 ), pcbIUScale.mmToIU( 50 ) ) );
2037 board.Add( fp );
2038
2039 PAD* pad = new PAD( fp );
2040 pad->SetPadstackMode( PADSTACK::MODE::NORMAL );
2041 pad->SetNumber( wxT( "1" ) );
2042 pad->SetAttribute( PAD_ATTRIB::SMD );
2044 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 1.0 ), pcbIUScale.mmToIU( 1.0 ) ) );
2045 pad->SetLayerSet( LSET( { F_Cu, F_Mask } ) );
2046 pad->SetLocalSolderMaskMargin( pcbIUScale.mmToIU( 0.5 ) );
2047 fp->Add( pad );
2048
2049 wxString tempPath = CreateTempFile();
2050 std::map<std::string, UTF8> props;
2051 props["units"] = "mm";
2052 props["version"] = "C";
2053 props["sigfig"] = "4";
2054
2055 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, board, &props ) );
2056 BOOST_REQUIRE( wxFileExists( tempPath ) );
2057
2058 std::ifstream xmlFile( tempPath.ToStdString() );
2059 BOOST_REQUIRE( xmlFile.is_open() );
2060
2061 std::string xml( ( std::istreambuf_iterator<char>( xmlFile ) ),
2062 std::istreambuf_iterator<char>() );
2063
2064 // Resolve the primitive id referenced by the F.Mask padstack entry, then confirm
2065 // that primitive's circle diameter is the mask-expanded 2.0 mm, not the 1.0 mm copper size.
2066 const std::string maskRef = "<PadstackPadDef layerRef=\"F.Mask\"";
2067 size_t maskDefPos = xml.find( maskRef );
2068 BOOST_REQUIRE_MESSAGE( maskDefPos != std::string::npos,
2069 "F.Mask padstack pad definition should be present" );
2070
2071 size_t refPos = xml.find( "StandardPrimitiveRef id=\"", maskDefPos );
2072 BOOST_REQUIRE( refPos != std::string::npos );
2073 refPos += std::string( "StandardPrimitiveRef id=\"" ).size();
2074 size_t refEnd = xml.find( '"', refPos );
2075 std::string maskPrimId = xml.substr( refPos, refEnd - refPos );
2076
2077 std::string entry = "<EntryStandard id=\"" + maskPrimId + "\"";
2078 size_t entryPos = xml.find( entry );
2079 BOOST_REQUIRE_MESSAGE( entryPos != std::string::npos,
2080 "Mask primitive EntryStandard should be defined" );
2081
2082 size_t diaPos = xml.find( "diameter=\"", entryPos );
2083 BOOST_REQUIRE( diaPos != std::string::npos );
2084 diaPos += std::string( "diameter=\"" ).size();
2085 size_t diaEnd = xml.find( '"', diaPos );
2086 double diameter = std::stod( xml.substr( diaPos, diaEnd - diaPos ) );
2087
2088 BOOST_CHECK_MESSAGE( std::abs( diameter - 2.0 ) < 1e-4,
2089 "F.Mask primitive diameter should be 2.0 mm (1.0 mm pad + 0.5 mm "
2090 "margin per side), got " << diameter );
2091}
2092
2093
2097BOOST_AUTO_TEST_CASE( SchemaValidation_Issue25149 )
2098{
2099 static const std::vector<std::string> boards = {
2100 "ipc2581/dielectric-sublayer.kicad_pcb",
2101 "ipc2581/edgecuts-circles-only.kicad_pcb",
2102 "ipc2581/filled-silk-circle.kicad_pcb",
2103 "ipc2581/filled-tented-via.kicad_pcb",
2104 "ipc2581/textbox-border.kicad_pcb",
2105 "ipc2581/thirty-copper-layers.kicad_pcb",
2106 "ipc2581/whitespace-text.kicad_pcb",
2107 };
2108
2109 for( const std::string& boardFile : boards )
2110 {
2111 BOOST_TEST_CONTEXT( "Board: " << boardFile )
2112 {
2113 std::unique_ptr<BOARD> board = LoadBoard( boardFile );
2114 BOOST_REQUIRE( board );
2115
2116 wxString errorMsg;
2117 bool valid = ExportAndValidate( *board, 'C', errorMsg );
2118
2119 BOOST_CHECK_MESSAGE( valid, "IPC-2581C validation failed for " + boardFile + ": " + errorMsg );
2120 }
2121 }
2122}
2123
2124
2129BOOST_AUTO_TEST_CASE( NoClosedOutline_Issue25149 )
2130{
2131 std::unique_ptr<BOARD> board = LoadBoard( "ipc2581/edgecuts-circles-only.kicad_pcb" );
2132 BOOST_REQUIRE( board );
2133
2134 wxString tempPath = CreateTempFile();
2135 std::map<std::string, UTF8> props;
2136 props["units"] = "mm";
2137 props["version"] = "C";
2138 props["sigfig"] = "3";
2139
2140 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, *board, &props ) );
2141 BOOST_REQUIRE( wxFileExists( tempPath ) );
2142
2143 BOOST_CHECK_MESSAGE( !FileContainsPattern( tempPath, wxT( "<Profile" ) ),
2144 "A board with no closed outline must not write a Profile" );
2145}
2146
2147
2151BOOST_AUTO_TEST_CASE( ProcessLayerViaPads_Issue25149 )
2152{
2153 std::unique_ptr<BOARD> board = LoadBoard( "ipc2581/filled-tented-via.kicad_pcb" );
2154 BOOST_REQUIRE( board );
2155
2156 wxString tempPath = CreateTempFile();
2157 std::map<std::string, UTF8> props;
2158 props["units"] = "mm";
2159 props["version"] = "C";
2160 props["sigfig"] = "3";
2161
2162 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, *board, &props ) );
2163 BOOST_REQUIRE( wxFileExists( tempPath ) );
2164
2165 std::string xml = KI_TEST::LoadStringData( tempPath );
2166
2167 std::string region = LayerFeatureRegion( xml, "F.Cu_2" );
2168 BOOST_REQUIRE_MESSAGE( !region.empty(), "Export should contain the F.Cu_2 process layer" );
2169
2170 BOOST_CHECK_MESSAGE( region.find( "<Set" ) != std::string::npos,
2171 "Process-layer via pads must be wrapped in a Set" );
2172 BOOST_CHECK_MESSAGE( region.find( "x=\"120.0\" y=\"-115.0\"" ) != std::string::npos,
2173 "Process-layer via pad must be at the via position" );
2174 BOOST_CHECK_MESSAGE( region.find( "x=\"0.0\" y=\"0.0\"" ) == std::string::npos,
2175 "Process-layer via pad must not be at (0,0)" );
2176}
2177
2178
2179BOOST_AUTO_TEST_CASE( RoundUnequalDrillHasPadstackHole )
2180{
2181 std::unique_ptr<BOARD> board = LoadBoard( "ipc2581/round_unequal_drill.kicad_pcb" );
2182 BOOST_REQUIRE( board );
2183
2184 const PAD* pad = dynamic_cast<const PAD*>(
2185 board->ResolveItem( KIID( wxS( "bd1ca0c2-74bb-4a01-94e6-11796af0cb7e" ) ), true ) );
2186 BOOST_REQUIRE( pad );
2187 BOOST_REQUIRE( pad->GetDrillShape() == PAD_DRILL_SHAPE::CIRCLE );
2188 BOOST_REQUIRE_NE( pad->GetDrillSizeX(), pad->GetDrillSizeY() );
2189
2190 wxString tempPath = CreateTempFile();
2191 std::map<std::string, UTF8> props = { { "units", "mm" }, { "version", "C" }, { "sigfig", "6" } };
2192 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( tempPath, *board, &props ) );
2193
2194 wxXmlDocument document;
2195 BOOST_REQUIRE( document.Load( tempPath ) );
2196 wxXmlNode* root = document.GetRoot();
2197 BOOST_REQUIRE( root );
2198
2199 double x = pcbIUScale.IUTomm( pad->GetPosition().x );
2200 double y = -pcbIUScale.IUTomm( pad->GetPosition().y );
2201
2202 auto isAtPad = [&]( wxXmlNode* aNode )
2203 {
2204 return aNode && std::abs( wxAtof( aNode->GetAttribute( wxS( "x" ) ) ) - x ) <= 0.001
2205 && std::abs( wxAtof( aNode->GetAttribute( wxS( "y" ) ) ) - y ) <= 0.001;
2206 };
2207
2208 std::vector<wxXmlNode*> holes;
2209 CollectXmlElements( root, wxS( "Hole" ), holes );
2210 double layerHole = 0;
2211
2212 for( wxXmlNode* hole : holes )
2213 {
2214 if( hole->GetAttribute( wxS( "platingStatus" ) ) == wxS( "PLATED" ) && isAtPad( hole ) )
2215 layerHole = wxAtof( hole->GetAttribute( wxS( "diameter" ) ) );
2216 }
2217
2218 BOOST_REQUIRE_GT( layerHole, 0 );
2219
2220 std::set<wxString> padstackNames;
2221 std::vector<wxXmlNode*> pads;
2222 CollectXmlElements( root, wxS( "Pad" ), pads );
2223
2224 for( wxXmlNode* padNode : pads )
2225 {
2226 if( padNode->HasAttribute( wxS( "padstackDefRef" ) ) && isAtPad( FindXmlChild( padNode, wxS( "Location" ) ) ) )
2227 padstackNames.insert( padNode->GetAttribute( wxS( "padstackDefRef" ) ) );
2228 }
2229
2230 BOOST_REQUIRE_EQUAL( padstackNames.size(), 1 );
2231
2232 std::vector<wxXmlNode*> padstacks;
2233 CollectXmlElements( root, wxS( "PadStackDef" ), padstacks );
2234 wxXmlNode* padstack = nullptr;
2235
2236 for( wxXmlNode* candidate : padstacks )
2237 {
2238 if( candidate->GetAttribute( wxS( "name" ) ) == *padstackNames.begin() )
2239 padstack = candidate;
2240 }
2241
2242 BOOST_REQUIRE( padstack );
2243 wxXmlNode* padstackHole = FindXmlChild( padstack, wxS( "PadstackHoleDef" ) );
2244 BOOST_REQUIRE_MESSAGE( padstackHole, "Round unequal drill padstack must define its hole" );
2245 BOOST_CHECK_EQUAL( padstackHole->GetAttribute( wxS( "platingStatus" ) ), wxS( "PLATED" ) );
2246 BOOST_CHECK_SMALL( wxAtof( padstackHole->GetAttribute( wxS( "diameter" ) ) ) - layerHole, 0.001 );
2247 BOOST_CHECK_SMALL( layerHole - pcbIUScale.IUTomm( std::min( pad->GetDrillSizeX(), pad->GetDrillSizeY() ) ),
2248 0.001 );
2249}
2250
2251
2252BOOST_AUTO_TEST_CASE( SlotPostMachiningSpecRefs )
2253{
2254 std::unique_ptr<BOARD> board = LoadBoard( "odbpp/slot_machining.kicad_pcb" );
2255 BOOST_REQUIRE( board );
2256
2257 const PAD* machinedSlot = dynamic_cast<const PAD*>(
2258 board->ResolveItem( KIID( wxS( "34428461-7652-43ea-869c-ef8abe86da6c" ) ), true ) );
2259 BOOST_REQUIRE( machinedSlot );
2260 BOOST_REQUIRE( IsDrillSlot( *machinedSlot ) );
2263
2264 wxString output = CreateTempFile();
2265 std::map<std::string, UTF8> props = { { "units", "mm" }, { "version", "C" }, { "sigfig", "6" } };
2266 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( output, *board, &props ) );
2267
2268 wxXmlDocument document;
2269 BOOST_REQUIRE( document.Load( output ) );
2270 std::vector<wxXmlNode*> specs;
2271 std::vector<wxXmlNode*> slots;
2272 std::map<wxString, wxXmlNode*> specsByName;
2273 CollectXmlElements( document.GetRoot(), wxS( "Spec" ), specs );
2274 CollectXmlElements( document.GetRoot(), wxS( "SlotCavity" ), slots );
2275
2276 for( wxXmlNode* spec : specs )
2277 specsByName.emplace( spec->GetAttribute( wxS( "name" ) ), spec );
2278
2279 int machinedSlots = 0;
2280
2281 for( wxXmlNode* slot : slots )
2282 {
2283 std::map<wxString, double> diameterBySide;
2284
2285 for( wxXmlNode* ref = slot->GetParent()->GetChildren(); ref; ref = ref->GetNext() )
2286 {
2287 if( ref->GetName() != wxS( "SpecRef" ) )
2288 continue;
2289
2290 auto spec = specsByName.find( ref->GetAttribute( wxS( "id" ) ) );
2291 BOOST_REQUIRE( spec != specsByName.end() );
2292 wxXmlNode* side = SecondaryDrillProperty( spec->second, wxS( "SIDE" ) );
2293 wxXmlNode* diameter = SecondaryDrillProperty( spec->second, wxS( "MAJOR_DIAMETER" ) );
2294 BOOST_REQUIRE( side && diameter );
2295 diameterBySide[side->GetAttribute( wxS( "text" ) )] = wxAtof( diameter->GetAttribute( wxS( "value" ) ) );
2296 }
2297
2298 if( diameterBySide.empty() )
2299 continue;
2300
2301 ++machinedSlots;
2302 BOOST_CHECK_SMALL( diameterBySide[wxS( "TOP" )] - 1.2, 0.000001 );
2303 BOOST_CHECK_SMALL( diameterBySide[wxS( "BOTTOM" )] - 1.4, 0.000001 );
2304 }
2305
2306 BOOST_CHECK_EQUAL( machinedSlots, 1 );
2307
2308 if( SchemaCheckReady( 'C' ) )
2309 {
2310 wxString schemaError = ValidateXmlWithXsd( output, GetXsdPath( 'C' ) );
2311 BOOST_CHECK_MESSAGE( schemaError.IsEmpty(), schemaError.ToStdString() );
2312 }
2313}
2314
2315
2316BOOST_AUTO_TEST_CASE( NpthMaskOnlyOpeningUsesAuthoredSize )
2317{
2318 std::unique_ptr<BOARD> board = LoadBoard( "issue5990.kicad_pcb" );
2319 BOOST_REQUIRE( board );
2320 const PAD* source = nullptr;
2321 size_t sourceCount = 0;
2322
2323 for( FOOTPRINT* footprint : board->Footprints() )
2324 {
2325 for( PAD* pad : footprint->Pads() )
2326 {
2327 if( pad->GetAttribute() == PAD_ATTRIB::NPTH && pad->IsOnLayer( F_Mask ) && !pad->IsOnLayer( F_Cu )
2328 && pad->GetDrillSizeX() == pcbIUScale.mmToIU( 2.7 )
2329 && pad->GetSize( F_Mask ).x == pcbIUScale.mmToIU( 5.0 ) )
2330 {
2331 if( !source )
2332 source = pad;
2333
2334 ++sourceCount;
2335 }
2336 }
2337 }
2338
2340 BOOST_REQUIRE_EQUAL( sourceCount, 4u );
2341 BOOST_REQUIRE( source->FlashLayer( F_Mask ) );
2342
2343 wxString output = CreateTempFile();
2344 std::map<std::string, UTF8> props = { { "units", "mm" }, { "version", "C" }, { "sigfig", "6" } };
2345 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( output, *board, &props ) );
2346 wxXmlDocument document;
2347 BOOST_REQUIRE( document.Load( output ) );
2348 wxXmlNode* root = document.GetRoot();
2349 BOOST_REQUIRE( root );
2350
2351 auto circleDiameter = [&]( wxXmlNode* aParent )
2352 {
2353 wxXmlNode* ref = FindXmlChild( aParent, wxS( "StandardPrimitiveRef" ) );
2354 wxXmlNode* entry =
2355 ref ? FindXmlEntry( root, wxS( "EntryStandard" ), ref->GetAttribute( wxS( "id" ) ) ) : nullptr;
2356 wxXmlNode* circle = entry ? FindXmlChild( entry, wxS( "Circle" ) ) : nullptr;
2357
2358 if( !circle )
2359 {
2360 ref = FindXmlChild( aParent, wxS( "UserPrimitiveRef" ) );
2361 entry = ref ? FindXmlEntry( root, wxS( "EntryUser" ), ref->GetAttribute( wxS( "id" ) ) ) : nullptr;
2362 wxXmlNode* special = entry ? FindXmlChild( entry, wxS( "UserSpecial" ) ) : nullptr;
2363 circle = special ? FindXmlChild( special, wxS( "Circle" ) ) : nullptr;
2364 }
2365
2366 return circle ? wxAtof( circle->GetAttribute( wxS( "diameter" ) ) ) : -1.0;
2367 };
2368
2369 std::vector<wxXmlNode*> padstacks;
2370 CollectXmlElements( root, wxS( "PadStackDef" ), padstacks );
2371 wxString targetName;
2372 double padstackDiameter = -1.0;
2373 size_t padstackCount = 0;
2374
2375 for( wxXmlNode* padstack : padstacks )
2376 {
2377 wxXmlNode* hole = FindXmlChild( padstack, wxS( "PadstackHoleDef" ) );
2378
2379 if( !hole || hole->GetAttribute( wxS( "platingStatus" ) ) != wxS( "NONPLATED" )
2380 || std::abs( wxAtof( hole->GetAttribute( wxS( "diameter" ) ) ) - 2.7 ) > 0.001 )
2381 {
2382 continue;
2383 }
2384
2385 for( wxXmlNode* paddef = padstack->GetChildren(); paddef; paddef = paddef->GetNext() )
2386 {
2387 if( paddef->GetName() == wxS( "PadstackPadDef" )
2388 && paddef->GetAttribute( wxS( "layerRef" ) ) == wxS( "F.Mask" ) )
2389 {
2390 targetName = padstack->GetAttribute( wxS( "name" ) );
2391 padstackDiameter = circleDiameter( paddef );
2392 ++padstackCount;
2393 }
2394 }
2395 }
2396
2397 BOOST_REQUIRE_EQUAL( padstackCount, 1u );
2398 BOOST_REQUIRE( !targetName.IsEmpty() );
2399 BOOST_CHECK_SMALL( padstackDiameter - 5.0, 0.001 );
2400 std::vector<wxXmlNode*> features;
2401 CollectXmlElements( root, wxS( "LayerFeature" ), features );
2402 size_t matched = 0;
2403
2404 for( wxXmlNode* feature : features )
2405 {
2406 if( feature->GetAttribute( wxS( "layerRef" ) ) != wxS( "F.Mask" ) )
2407 continue;
2408
2409 std::vector<wxXmlNode*> pads;
2410 CollectXmlElements( feature->GetChildren(), wxS( "Pad" ), pads );
2411
2412 for( wxXmlNode* pad : pads )
2413 {
2414 if( pad->GetAttribute( wxS( "padstackDefRef" ) ) == targetName )
2415 {
2416 BOOST_CHECK_SMALL( circleDiameter( pad ) - 5.0, 0.001 );
2417 ++matched;
2418 }
2419 }
2420 }
2421
2422 BOOST_CHECK_EQUAL( matched, sourceCount );
2423}
2424
2425
2426BOOST_AUTO_TEST_CASE( Ipc2581ComponentAndPartNamesStayInTheirOwnNamespaces )
2427{
2428 std::unique_ptr<BOARD> board = LoadBoard( "variant_test/variant_test.kicad_pcb" );
2429 BOOST_REQUIRE( board );
2430
2431 std::multiset<wxString> sourceRefs;
2432
2433 for( FOOTPRINT* footprint : board->Footprints() )
2434 sourceRefs.insert( footprint->GetReference() );
2435
2436 BOOST_REQUIRE_EQUAL( sourceRefs.count( wxS( "R2" ) ), 2 );
2437
2438 for( char version : { 'B', 'C' } )
2439 {
2440 BOOST_TEST_CONTEXT( "Revision " << version )
2441 {
2442 wxString path = CreateTempFile();
2443 std::map<std::string, UTF8> props = { { "units", "mm" },
2444 { "version", std::string( 1, version ) },
2445 { "OEMRef", "Reference" } };
2446 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( path, *board, &props ) );
2447
2448 wxXmlDocument document;
2449 BOOST_REQUIRE( document.Load( path ) );
2450 wxXmlNode* root = document.GetRoot();
2451 BOOST_REQUIRE( root );
2452
2453 std::vector<wxXmlNode*> components;
2454 std::vector<wxXmlNode*> bomItems;
2455 std::vector<wxXmlNode*> avlItems;
2456 std::vector<wxXmlNode*> pinRefs;
2457 std::vector<wxXmlNode*> logicalNetPins;
2458 CollectXmlElements( root, wxS( "Component" ), components );
2459 CollectXmlElements( root, wxS( "BomItem" ), bomItems );
2460 CollectXmlElements( root, wxS( "AvlItem" ), avlItems );
2461 CollectXmlElements( root, wxS( "PinRef" ), pinRefs );
2462 CollectXmlElements( root, wxS( "LogicalNetPin" ), logicalNetPins );
2463
2464 std::multiset<wxString> parts;
2465 std::set<wxString> componentRefs;
2466
2467 for( wxXmlNode* component : components )
2468 {
2469 parts.insert( component->GetAttribute( wxS( "part" ) ) );
2470 componentRefs.insert( component->GetAttribute( wxS( "refDes" ) ) );
2471 }
2472
2473 BOOST_CHECK( parts == sourceRefs );
2474 BOOST_CHECK_EQUAL( componentRefs.size(), components.size() );
2475
2476 std::map<wxString, wxString> partByRef;
2477 std::set<wxString> bomParts;
2478
2479 for( wxXmlNode* item : bomItems )
2480 {
2481 wxString part = item->GetAttribute( wxS( "OEMDesignNumberRef" ) );
2482 bomParts.insert( part );
2483 std::vector<wxXmlNode*> refs;
2484 CollectXmlElements( item->GetChildren(), wxS( "RefDes" ), refs );
2485
2486 if( part == wxS( "R2" ) )
2487 {
2488 BOOST_CHECK_EQUAL( item->GetAttribute( wxS( "quantity" ) ), wxString( wxS( "2" ) ) );
2489 BOOST_CHECK_EQUAL( refs.size(), 2 );
2490 }
2491
2492 for( wxXmlNode* ref : refs )
2493 BOOST_CHECK( partByRef.emplace( ref->GetAttribute( wxS( "name" ) ), part ).second );
2494 }
2495
2496 BOOST_CHECK_EQUAL( partByRef.size(), components.size() );
2497
2498 for( wxXmlNode* component : components )
2499 {
2500 wxString ref = component->GetAttribute( wxS( "refDes" ) );
2501 BOOST_CHECK_EQUAL( partByRef[ref], component->GetAttribute( wxS( "part" ) ) );
2502 }
2503
2504 std::set<wxString> avlParts;
2505
2506 for( wxXmlNode* item : avlItems )
2507 avlParts.insert( item->GetAttribute( wxS( "OEMDesignNumber" ) ) );
2508
2509 BOOST_CHECK( bomParts == avlParts );
2510 BOOST_CHECK( bomParts == std::set<wxString>( sourceRefs.begin(), sourceRefs.end() ) );
2511
2512 for( wxXmlNode* ref : pinRefs )
2513 BOOST_CHECK( componentRefs.count( ref->GetAttribute( wxS( "componentRef" ) ) ) );
2514
2515 for( wxXmlNode* ref : logicalNetPins )
2516 BOOST_CHECK( componentRefs.count( ref->GetAttribute( wxS( "componentRef" ) ) ) );
2517
2518 if( SchemaCheckReady( version ) )
2519 BOOST_CHECK( ValidateXmlWithXsd( path, GetXsdPath( version ) ).IsEmpty() );
2520 }
2521 }
2522
2523 for( char version : { 'B', 'C' } )
2524 {
2525 BOOST_TEST_CONTEXT( "Omitted reference, revision " << version )
2526 {
2527 wxString path = CreateTempFile();
2528 std::map<std::string, UTF8> props = { { "units", "mm" },
2529 { "version", std::string( 1, version ) },
2530 { "OEMRef", "Reference" },
2531 { "refdes", "omit" } };
2532 BOOST_REQUIRE_NO_THROW( m_ipc2581Plugin.SaveBoard( path, *board, &props ) );
2533
2534 wxXmlDocument document;
2535 BOOST_REQUIRE( document.Load( path ) );
2536
2537 std::vector<wxXmlNode*> components;
2538 std::vector<wxXmlNode*> bomRefs;
2539 std::vector<wxXmlNode*> pinRefs;
2540 CollectXmlElements( document.GetRoot(), wxS( "Component" ), components );
2541 CollectXmlElements( document.GetRoot(), wxS( "RefDes" ), bomRefs );
2542 CollectXmlElements( document.GetRoot(), wxS( "PinRef" ), pinRefs );
2543
2544 std::multiset<wxString> parts;
2545 std::set<wxString> anonRefs;
2546
2547 for( wxXmlNode* ref : bomRefs )
2548 {
2549 wxString name = ref->GetAttribute( wxS( "name" ) );
2550 BOOST_CHECK( name.StartsWith( wxS( "CMP_" ) ) );
2551 BOOST_CHECK( anonRefs.insert( name ).second );
2552 }
2553
2554 for( wxXmlNode* component : components )
2555 {
2556 parts.insert( component->GetAttribute( wxS( "part" ) ) );
2557 wxString ref = component->GetAttribute( wxS( "refDes" ) );
2558
2559 if( version == 'B' )
2560 BOOST_CHECK( anonRefs.count( ref ) );
2561 else
2562 BOOST_CHECK( ref.IsEmpty() );
2563 }
2564
2565 BOOST_CHECK( parts == sourceRefs );
2566 BOOST_CHECK_EQUAL( anonRefs.size(), components.size() );
2567
2568 for( wxXmlNode* ref : pinRefs )
2569 {
2570 if( version == 'B' )
2571 BOOST_CHECK( anonRefs.count( ref->GetAttribute( wxS( "componentRef" ) ) ) );
2572 else
2573 BOOST_CHECK( ref->GetAttribute( wxS( "componentRef" ) ).IsEmpty() );
2574 }
2575
2576 if( SchemaCheckReady( version ) )
2577 BOOST_CHECK( ValidateXmlWithXsd( path, GetXsdPath( version ) ).IsEmpty() );
2578 }
2579 }
2580}
2581
2582
const char * name
constexpr double PCB_IU_PER_MM
Pcbnew IU is 1 nanometer.
Definition base_units.h:68
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:128
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()
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:84
Manage layers needed to make a physical board.
const std::vector< BOARD_STACKUP_ITEM * > & GetList() const
int GetCount() const
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.
Definition board.h:410
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Removes an item from the container.
Definition board.cpp:1524
void SetCopperLayerCount(int aCount)
Definition board.cpp:1144
BOARD_DESIGN_SETTINGS & GetDesignSettings() const
Definition board.cpp:1301
virtual void SetFilled(bool aFlag)
Definition eda_shape.h:142
void SetVertJustify(GR_TEXT_V_ALIGN_T aType)
Definition eda_text.cpp:378
virtual void SetText(const wxString &aText)
Definition eda_text.cpp:236
void SetHorizJustify(GR_TEXT_H_ALIGN_T aType)
Definition eda_text.cpp:370
Set of schema sections.
SECTION_SET & Set(SECTION aSection, bool aOn=true)
void SetPosition(const VECTOR2I &aPos) override
void SetReference(const wxString &aReference)
Definition footprint.h:915
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Adds an item to the container.
const wxString & GetReference() const
Definition footprint.h:909
std::vector< wxString > m_variantNames
void FromJson(const nlohmann::json &aJson) override
void SetConfiguredOutputPath(const wxString &aPath)
Sets the configured output path for the job, this path is always saved to file.
Definition job.cpp:157
Definition kiid.h:46
RAII helper that sets a process-level environment variable for the lifetime of the object.
void SetValue(const wxString &aValue)
Set a new value for the environment variable (which will still only be in effect for the lifetime of ...
const std::filesystem::path & Path() const
Get the path to the temporary directory as a std::filesystem::path.
Definition file_utils.h:59
LSET is a set of PCB_LAYER_IDs.
Definition lset.h:37
Handle the data for a net.
Definition netinfo.h:50
@ NORMAL
Shape is the same on all layers.
Definition padstack.h:170
static constexpr PCB_LAYER_ID ALL_LAYERS
! The layer identifier to use for the single defintion on normal padstacks
Definition padstack.h:179
Definition pad.h:61
PAD_DRILL_POST_MACHINING_MODE GetBackPostMachiningMode() const
Definition pad.h:469
PAD_DRILL_POST_MACHINING_MODE GetFrontPostMachiningMode() const
Definition pad.h:449
void SetMid(const VECTOR2I &aMid)
Definition pcb_track.h:294
A PCB_IO derivation for saving and loading Pcbnew s-expression formatted files.
void SetCornerRadius(int aCornerRadius) override
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)
Disables the border, this is done by changing the stroke internally.
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)
Definition pcb_track.h:94
void SetStart(const VECTOR2I &aStart)
Definition pcb_track.h:97
virtual void SetWidth(int aWidth)
Definition pcb_track.h:91
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.
A wrapper for reporting to a wxString object.
Definition reporter.h:242
bool IsDrillSlot(const PAD &aPad)
Whether a pad's hole is a slot rather than a round drill.
std::vector< DRILL_OPERATION > EnumerateDrillOperations(const BOARD &aBoard, const DRILL_QUERY &aQuery)
The one place that decides what the board's holes are.
@ RECTANGLE
Use RECTANGLE instead of RECT to avoid collision in a Windows header.
Definition eda_shape.h:57
FAB_PIN MakeFabPin(const PAD &aPad, size_t aIndexInFootprint)
aIndexInFootprint is the pad's position in its footprint, used to name NPTH and unnumbered pads
Definition fab_pin.cpp:50
std::string source
bool GenerateIpc2581File(JOB_EXPORT_PCB_IPC2581 &aJob, BOARD *aBoard, PROGRESS_REPORTER *aProgressReporter, REPORTER *aReporter)
Write the IPC-2581 file or archive of aJob and record it as a job output.
@ Edge_Cuts
Definition layer_ids.h:108
@ F_Paste
Definition layer_ids.h:100
@ B_Mask
Definition layer_ids.h:94
@ B_Cu
Definition layer_ids.h:61
@ F_Mask
Definition layer_ids.h:93
@ In2_Cu
Definition layer_ids.h:63
@ User_10
Definition layer_ids.h:129
@ F_SilkS
Definition layer_ids.h:96
@ In3_Cu
Definition layer_ids.h:64
@ F_Cu
Definition layer_ids.h:60
wxString SectionKeyString(const SECTION_SET &aSet)
Give aSet as a sectionKey attribute value.
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.
std::string LoadStringData(const wxString &aPath)
Load the contents of a file into a string.
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:437
@ NPTH
like PAD_PTH, but not plated mechanical use only, no connection allowed
Definition padstack.h:102
@ SMD
Smd pad, appears on the solder paste layer (default)
Definition padstack.h:98
@ PTH
Plated through hole pad.
Definition padstack.h:97
@ ROUNDRECT
Definition padstack.h:56
CITER next(CITER it)
Definition ptree.cpp:120
One machining action, which is also one NC hit and one tool assignment.
Selects which operations EnumerateDrillOperations() returns.
bool m_NonPlatedOnly
Emit only non-plated holes.
DRILL_SPAN m_Span
wxString m_name
Definition fab_pin.h:58
wxString GetXsdPath(char aVersion) const
bool SchemaCheckEnabled(const wxString &aXsdPath, wxString &aError) const
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
wxString CreateTempFile(const wxString &aSuffix=wxT(""))
std::unique_ptr< BOARD > LoadBoard(const std::string &aRelativePath)
bool SchemaCheckReady(char aVersion) const
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
BOOST_AUTO_TEST_CASE(Ipc2581PadHoleMatchesNcDrill)
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.
std::string path
IbisParser parser & reporter
KIBIS_PIN * pin
VECTOR3I expected(15, 30, 45)
VECTOR2I end
BOOST_TEST_CONTEXT("Test Clearance")
SHAPE_CIRCLE circle(c.m_circle_center, c.m_circle_radius)
VECTOR2I location
int actual
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")
@ GR_TEXT_H_ALIGN_LEFT
@ GR_TEXT_V_ALIGN_TOP
@ PCB_VIA_T
class PCB_VIA, a via (like a track segment on a copper layer)
Definition typeinfo.h:89
@ PCB_PAD_T
class PAD, a pad in a footprint
Definition typeinfo.h:79
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:708
double ToDouble(KI_INT128 aValue)
Definition wide_int.h:94