KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_cam_backdrill.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 3
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
20#include <boost/test/unit_test.hpp>
21
22#include <board.h>
25#include <footprint.h>
26#include <netinfo.h>
27#include <pad.h>
28#include <pcb_shape.h>
32#include <pcbnew/pcb_track.h>
35#include <qa_utils/file_utils.h>
37#include <base_units.h>
38
39#include <algorithm>
40#include <filesystem>
41#include <fstream>
42#include <iterator>
43#include <map>
44#include <memory>
45
46#include <core/utf8.h>
47
48#include <wx/dir.h>
49#include <wx/ffile.h>
50#include <wx/filename.h>
51#include <wx/tokenzr.h>
52#include <wx/utils.h>
53
54
55namespace
56{
57wxFileName MakeTempDir()
58{
59 wxFileName tempDir( wxFileName::GetTempDir(), wxEmptyString );
60 tempDir.AppendDir( wxString::Format( "kicad-backdrill-%llu", static_cast<unsigned long long>( wxGetUTCTime() ) ) );
61 BOOST_REQUIRE( tempDir.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
62
63 return tempDir;
64}
65
66
67std::vector<wxString> ExportGerberDrill( BOARD& aBoard, const KI_TEST::SCOPED_TEMP_DIR& aDir )
68{
69 GERBER_WRITER gerber( &aBoard );
70 gerber.SetOptions( VECTOR2I( 0, 0 ) );
71 gerber.SetFormat( 6 );
72 BOOST_REQUIRE( gerber.CreateDrillandMapFilesSet( aDir.PathStr(), true, false, true ) );
73 return gerber.GetCreatedFiles();
74}
75
76
77size_t CountContaining( const std::vector<wxString>& aFiles, const wxString& aPart )
78{
79 return std::count_if( aFiles.begin(), aFiles.end(), [&]( const wxString& aName )
80 {
81 return aName.Contains( aPart );
82 } );
83}
84} // anonymous namespace
85
86
87BOOST_AUTO_TEST_CASE( GerberDrillBoardDefaultsCreateProtectionFiles )
88{
89 std::unique_ptr<BOARD> board = KI_TEST::ReadBoardFromFileOrStream(
91 + "drc_via_stack/FillFromBoardHonoursTheBoardDefault.kicad_pcb" );
92 BOOST_REQUIRE( board );
93 BOOST_REQUIRE( board->GetDesignSettings().m_FillVias );
94 BOOST_REQUIRE( board->GetDesignSettings().m_TentViasFront );
95
96 KI_TEST::SCOPED_TEMP_DIR dir( wxT( "gerber_default_protection" ) );
97 std::vector<wxString> created = ExportGerberDrill( *board, dir );
98 auto front = std::find_if( created.begin(), created.end(), []( const wxString& aName )
99 {
100 return aName.EndsWith( wxT( "-tenting-front.gbr" ) );
101 } );
102 BOOST_REQUIRE( front != created.end() );
103 std::ifstream stream( std::filesystem::path( front->ToStdString() ), std::ios::binary );
104 std::string frontTenting( std::istreambuf_iterator<char>{ stream }, std::istreambuf_iterator<char>{} );
105
106 BOOST_CHECK_EQUAL( CountContaining( created, wxT( "-filling-" ) ), 2 );
107 BOOST_CHECK_EQUAL( CountContaining( created, wxT( "-tenting-" ) ), 1 );
108 BOOST_CHECK_EQUAL( CountContaining( created, wxT( "-tenting-front.gbr" ) ), 1 );
109 BOOST_CHECK( frontTenting.find( "TF,FileFunction,Other,Tenting-Front" ) != std::string::npos );
110}
111
112
113BOOST_AUTO_TEST_CASE( GerberBackdrillDoesNotInheritViaProtection )
114{
115 std::unique_ptr<BOARD> board = KI_TEST::ReadBoardFromFileOrStream(
116 KI_TEST::GetPcbnewTestDataDir() + "issue25021/backdrill.kicad_pcb" );
117 BOOST_REQUIRE( board );
118 BOOST_REQUIRE( board->GetDesignSettings().m_TentViasFront );
119 BOOST_REQUIRE( board->GetDesignSettings().m_TentViasBack );
120
121 for( const DRILL_SPAN& span : EnumerateDrillSpans( *board ) )
122 {
123 if( !span.m_IsBackdrill )
124 continue;
125
126 DRILL_QUERY query;
127 query.m_Span = span;
128
129 for( const DRILL_OPERATION& op : EnumerateDrillOperations( *board, query ) )
130 {
131 BOOST_CHECK( !op.m_TopTented );
132 BOOST_CHECK( !op.m_BottomTented );
133 BOOST_CHECK( !op.m_TopCovered );
134 BOOST_CHECK( !op.m_BottomCovered );
135 BOOST_CHECK( !op.m_TopPlugged );
136 BOOST_CHECK( !op.m_BottomPlugged );
137 }
138 }
139
140 KI_TEST::SCOPED_TEMP_DIR dir( wxT( "gerber_backdrill_protection" ) );
141 std::vector<wxString> created = ExportGerberDrill( *board, dir );
142 BOOST_CHECK_EQUAL( CountContaining( created, wxT( "-tenting-front.gbr" ) ), 1 );
143 BOOST_CHECK_EQUAL( CountContaining( created, wxT( "-tenting-back.gbr" ) ), 1 );
144}
145
146
147BOOST_AUTO_TEST_CASE( BackdrillCamOutputs )
148{
149 wxFileName tempDir = MakeTempDir();
150 wxFileName boardFile( tempDir.GetFullPath(), wxT( "backdrill_board.kicad_pcb" ) );
151
152 BOARD board;
153 board.SetCopperLayerCount( 6 );
154 board.SetFileName( boardFile.GetFullPath() );
155
156 PCB_VIA* via = new PCB_VIA( &board );
157 via->SetPadstackMode( PADSTACK::MODE::NORMAL );
158 via->SetPosition( VECTOR2I( 0, 0 ) );
159 via->SetLayerPair( F_Cu, B_Cu );
160 via->SetDrill( pcbIUScale.mmToIU( 0.30 ) );
161 via->SetWidth( PADSTACK::ALL_LAYERS, pcbIUScale.mmToIU( 0.60 ) );
162 via->SetSecondaryDrillSize( pcbIUScale.mmToIU( 0.20 ) );
163 via->SetSecondaryDrillStartLayer( F_Cu );
164 via->SetSecondaryDrillEndLayer( In3_Cu );
165 via->SetFrontPostMachiningMode( PAD_DRILL_POST_MACHINING_MODE::COUNTERSINK );
166 via->SetFrontPostMachiningSize( pcbIUScale.mmToIU( 0.60 ) );
167 via->SetFrontPostMachiningDepth( pcbIUScale.mmToIU( 0.15 ) );
168 via->SetFrontPostMachiningAngle( 900 );
169 board.Add( via );
170
171 EXCELLON_WRITER excellon( &board );
172 excellon.SetOptions( false, false, VECTOR2I( 0, 0 ), false );
173 excellon.SetFormat( true );
174 BOOST_REQUIRE( excellon.CreateDrillandMapFilesSet( tempDir.GetFullPath(), true, false, nullptr ) );
175
176 wxFileName excellonFile( tempDir.GetFullPath(), wxT( "backdrill_board_Backdrills_Drill_1_4.drl" ) );
177 BOOST_REQUIRE( excellonFile.FileExists() );
178
179 wxFFile excellonStream( excellonFile.GetFullPath(), wxT( "rb" ) );
180 wxString excellonContents;
181 BOOST_REQUIRE( excellonStream.ReadAll( &excellonContents ) );
182 BOOST_CHECK( excellonContents.Contains( wxT( "TF.FileFunction,NonPlated,1,4,Blind" ) ) );
183 BOOST_CHECK( excellonContents.Contains( wxT( "; Backdrill" ) ) );
184 BOOST_CHECK( excellonContents.Contains( wxT( "post-machining" ) ) );
185
186 wxFileName layerPairFile( tempDir.GetFullPath(), wxT( "backdrill_board-front-in3-backdrill.drl" ) );
187 BOOST_REQUIRE( layerPairFile.FileExists() );
188
189 wxFFile layerPairStream( layerPairFile.GetFullPath(), wxT( "rb" ) );
190 wxString layerPairContents;
191 BOOST_REQUIRE( layerPairStream.ReadAll( &layerPairContents ) );
192 BOOST_CHECK( layerPairContents.Contains( wxT( "; backdrill" ) ) );
193
194 wxFileName pthFile( tempDir.GetFullPath(), wxT( "backdrill_board-PTH.drl" ) );
195 BOOST_REQUIRE( pthFile.FileExists() );
196
197 wxFFile pthStream( pthFile.GetFullPath(), wxT( "rb" ) );
198 wxString pthContents;
199 BOOST_REQUIRE( pthStream.ReadAll( &pthContents ) );
200 BOOST_CHECK( pthContents.Contains( wxT( "; Post-machining front countersink dia 0.600mm depth 0.150mm angle 90deg" ) ) );
201
202 GERBER_WRITER gerber( &board );
203 gerber.SetOptions( VECTOR2I( 0, 0 ) );
204 gerber.SetFormat( 6 );
205 BOOST_REQUIRE( gerber.CreateDrillandMapFilesSet( tempDir.GetFullPath(), true, false, false, nullptr ) );
206
207 wxFileName gerberFile( tempDir.GetFullPath(), wxT( "backdrill_board_Backdrills_Drill_1_4-drl.gbr" ) );
208 BOOST_REQUIRE( gerberFile.FileExists() );
209
210 wxFFile gerberStream( gerberFile.GetFullPath(), wxT( "rb" ) );
211 wxString gerberContents;
212 BOOST_REQUIRE( gerberStream.ReadAll( &gerberContents ) );
213 BOOST_CHECK( gerberContents.Contains( wxT( "%TA.AperFunction,BackDrill*%" ) ) );
214 BOOST_CHECK( gerberContents.Contains( wxT( "%TF.FileFunction,NonPlated,1,4,Blind,Drill*%" ) ) );
215
216 wxFileName gerberLayerPairFile( tempDir.GetFullPath(),
217 wxT( "backdrill_board-front-in3-backdrill-drl.gbr" ) );
218 BOOST_REQUIRE( gerberLayerPairFile.FileExists() );
219
220 wxFFile gerberLayerPairStream( gerberLayerPairFile.GetFullPath(), wxT( "rb" ) );
221 wxString gerberLayerPairContents;
222 BOOST_REQUIRE( gerberLayerPairStream.ReadAll( &gerberLayerPairContents ) );
223 BOOST_CHECK( gerberLayerPairContents.Contains( wxT( "%TF.FileFunction,NonPlated,1,4,Blind,Drill*%" ) ) );
224 BOOST_CHECK( gerberLayerPairContents.Contains( wxT( "%TA.AperFunction,BackDrill*%" ) ) );
225
226 wxFileName odbRoot( tempDir.GetFullPath(), wxEmptyString );
227 odbRoot.AppendDir( wxT( "odb_out" ) );
228 BOOST_REQUIRE( odbRoot.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
229
230 PCB_IO_ODBPP odbExporter;
231 std::map<std::string, UTF8> props;
232 props["units"] = "mm";
233 props["sigfig"] = "4";
234 BOOST_REQUIRE_NO_THROW( odbExporter.SaveBoard( odbRoot.GetFullPath(), board, &props ) );
235
236 wxFileName drill1Dir( odbRoot.GetFullPath(), wxEmptyString );
237 drill1Dir.AppendDir( wxT( "steps" ) );
238 drill1Dir.AppendDir( wxT( "pcb" ) );
239 drill1Dir.AppendDir( wxT( "layers" ) );
240 drill1Dir.AppendDir( wxT( "drill1" ) );
241 BOOST_REQUIRE( drill1Dir.DirExists() );
242
243 wxFileName toolsFile( drill1Dir.GetFullPath(), wxT( "tools" ) );
244 BOOST_REQUIRE( toolsFile.FileExists() );
245
246 wxFFile toolsStream( toolsFile.GetFullPath(), wxT( "rb" ) );
247 wxString toolsContents;
248 BOOST_REQUIRE( toolsStream.ReadAll( &toolsContents ) );
249 BOOST_CHECK( toolsContents.Contains( wxT( "TYPE=NON_PLATED" ) ) );
250 BOOST_CHECK( toolsContents.Contains( wxT( "TYPE2=BLIND" ) ) );
251
252 wxFileName matrixFile( odbRoot.GetFullPath(), wxEmptyString );
253 matrixFile.AppendDir( wxT( "matrix" ) );
254 matrixFile.SetFullName( wxT( "matrix" ) );
255 BOOST_REQUIRE( matrixFile.FileExists() );
256
257 wxFFile matrixStream( matrixFile.GetFullPath(), wxT( "rb" ) );
258 wxString matrixContents;
259 BOOST_REQUIRE( matrixStream.ReadAll( &matrixContents ) );
260 BOOST_CHECK( matrixContents.Contains( wxT( "ADD_TYPE=BACKDRILL" ) ) );
261
262 matrixStream.Close();
263 toolsStream.Close();
264 gerberStream.Close();
265 gerberLayerPairStream.Close();
266 excellonStream.Close();
267 layerPairStream.Close();
268 pthStream.Close();
269
270 wxFileName::Rmdir( odbRoot.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
271 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
272}
273
274
275// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/23914
276// Only front-side (secondary) backdrill holes were exported; back-side (tertiary)
277// backdrill operations never produced a drill file.
278BOOST_AUTO_TEST_CASE( FrontAndBackBackdrillCamOutputs )
279{
280 wxFileName tempDir = MakeTempDir();
281 wxFileName boardFile( tempDir.GetFullPath(), wxT( "backdrill_pair_board.kicad_pcb" ) );
282
283 BOARD board;
284 board.SetCopperLayerCount( 6 );
285 board.SetFileName( boardFile.GetFullPath() );
286
287 PCB_VIA* topVia = new PCB_VIA( &board );
289 topVia->SetPosition( VECTOR2I( 0, 0 ) );
290 topVia->SetLayerPair( F_Cu, B_Cu );
291 topVia->SetDrill( pcbIUScale.mmToIU( 0.30 ) );
292 topVia->SetWidth( PADSTACK::ALL_LAYERS, pcbIUScale.mmToIU( 0.60 ) );
293 topVia->SetSecondaryDrillSize( pcbIUScale.mmToIU( 0.40 ) );
296 board.Add( topVia );
297
298 PCB_VIA* bottomVia = new PCB_VIA( &board );
300 bottomVia->SetPosition( VECTOR2I( pcbIUScale.mmToIU( 5.0 ), 0 ) );
301 bottomVia->SetLayerPair( F_Cu, B_Cu );
302 bottomVia->SetDrill( pcbIUScale.mmToIU( 0.30 ) );
303 bottomVia->SetWidth( PADSTACK::ALL_LAYERS, pcbIUScale.mmToIU( 0.60 ) );
304 bottomVia->SetTertiaryDrillSize( pcbIUScale.mmToIU( 0.40 ) );
305 bottomVia->SetTertiaryDrillStartLayer( B_Cu );
306 bottomVia->SetTertiaryDrillEndLayer( In3_Cu );
307 board.Add( bottomVia );
308
309 EXCELLON_WRITER excellon( &board );
310 excellon.SetOptions( false, false, VECTOR2I( 0, 0 ), false );
311 excellon.SetFormat( true );
312 BOOST_REQUIRE( excellon.CreateDrillandMapFilesSet( tempDir.GetFullPath(), true, false, nullptr ) );
313
314 wxFileName topBackdrillFile( tempDir.GetFullPath(),
315 wxT( "backdrill_pair_board_Backdrills_Drill_1_2.drl" ) );
316 BOOST_CHECK_MESSAGE( topBackdrillFile.FileExists(),
317 "Front-side backdrill drill file should be produced" );
318
319 // Start=B_Cu (UI index 6) drilled toward In3_Cu (UI index 4) in a 6-layer board
320 wxFileName bottomBackdrillFile( tempDir.GetFullPath(),
321 wxT( "backdrill_pair_board_Backdrills_Drill_6_4.drl" ) );
322 BOOST_CHECK_MESSAGE( bottomBackdrillFile.FileExists(),
323 "Back-side (tertiary) backdrill drill file should be produced" );
324
325 if( bottomBackdrillFile.FileExists() )
326 {
327 wxFFile stream( bottomBackdrillFile.GetFullPath(), wxT( "rb" ) );
328 wxString contents;
329 BOOST_REQUIRE( stream.ReadAll( &contents ) );
330 BOOST_CHECK( contents.Contains( wxT( "; Backdrill" ) ) );
331 stream.Close();
332 }
333
334 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
335}
336
337
338// Stronger coverage for https://gitlab.com/kicad/code/kicad/-/issues/23914
339// A single via can carry both a front-side (secondary) and a back-side
340// (tertiary) backdrill. Both drill files must be produced.
341BOOST_AUTO_TEST_CASE( DualBackdrillSameViaCamOutputs )
342{
343 wxFileName tempDir = MakeTempDir();
344 wxFileName boardFile( tempDir.GetFullPath(), wxT( "dual_backdrill_board.kicad_pcb" ) );
345
346 BOARD board;
347 board.SetCopperLayerCount( 6 );
348 board.SetFileName( boardFile.GetFullPath() );
349
350 PCB_VIA* via = new PCB_VIA( &board );
351 via->SetPadstackMode( PADSTACK::MODE::NORMAL );
352 via->SetPosition( VECTOR2I( 0, 0 ) );
353 via->SetLayerPair( F_Cu, B_Cu );
354 via->SetDrill( pcbIUScale.mmToIU( 0.30 ) );
355 via->SetWidth( PADSTACK::ALL_LAYERS, pcbIUScale.mmToIU( 0.60 ) );
356 via->SetSecondaryDrillSize( pcbIUScale.mmToIU( 0.40 ) );
357 via->SetSecondaryDrillStartLayer( F_Cu );
358 via->SetSecondaryDrillEndLayer( In1_Cu );
359 via->SetTertiaryDrillSize( pcbIUScale.mmToIU( 0.40 ) );
360 via->SetTertiaryDrillStartLayer( B_Cu );
361 via->SetTertiaryDrillEndLayer( In3_Cu );
362 board.Add( via );
363
364 EXCELLON_WRITER excellon( &board );
365 excellon.SetOptions( false, false, VECTOR2I( 0, 0 ), false );
366 excellon.SetFormat( true );
367 BOOST_REQUIRE( excellon.CreateDrillandMapFilesSet( tempDir.GetFullPath(), true, false,
368 nullptr ) );
369
370 wxFileName topBackdrillFile( tempDir.GetFullPath(),
371 wxT( "dual_backdrill_board_Backdrills_Drill_1_2.drl" ) );
372 BOOST_CHECK( topBackdrillFile.FileExists() );
373
374 wxFileName bottomBackdrillFile( tempDir.GetFullPath(),
375 wxT( "dual_backdrill_board_Backdrills_Drill_6_4.drl" ) );
376 BOOST_CHECK( bottomBackdrillFile.FileExists() );
377
378 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
379}
380
381
382// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/23451
383// GERBER_WRITER::SetFormat() precision was not passed to the plotter; output always used 4.6.
384BOOST_AUTO_TEST_CASE( GerberDrillPrecision )
385{
386 wxFileName tempDir = MakeTempDir();
387 wxFileName boardFile( tempDir.GetFullPath(), wxT( "precision_board.kicad_pcb" ) );
388
389 BOARD board;
390 board.SetCopperLayerCount( 2 );
391 board.SetFileName( boardFile.GetFullPath() );
392
393 PCB_VIA* via = new PCB_VIA( &board );
394 via->SetPadstackMode( PADSTACK::MODE::NORMAL );
395 via->SetPosition( VECTOR2I( 0, 0 ) );
396 via->SetLayerPair( F_Cu, B_Cu );
397 via->SetDrill( pcbIUScale.mmToIU( 0.30 ) );
398 via->SetWidth( PADSTACK::ALL_LAYERS, pcbIUScale.mmToIU( 0.60 ) );
399 board.Add( via );
400
401 // Verify precision 5 produces "Fmt 4.5" in the file header
402 GERBER_WRITER gerber5( &board );
403 gerber5.SetOptions( VECTOR2I( 0, 0 ) );
404 gerber5.SetFormat( 5 );
405 BOOST_REQUIRE( gerber5.CreateDrillandMapFilesSet( tempDir.GetFullPath(), true, false, false, nullptr ) );
406
407 wxFileName gerberFile5( tempDir.GetFullPath(), wxT( "precision_board-PTH-drl.gbr" ) );
408 BOOST_REQUIRE( gerberFile5.FileExists() );
409
410 wxFFile gerberStream5( gerberFile5.GetFullPath(), wxT( "rb" ) );
411 wxString gerberContents5;
412 BOOST_REQUIRE( gerberStream5.ReadAll( &gerberContents5 ) );
413 BOOST_CHECK_MESSAGE( gerberContents5.Contains( wxT( "Fmt 4.5" ) ),
414 "Expected 'Fmt 4.5' in gerber header with precision=5" );
415 BOOST_CHECK( !gerberContents5.Contains( wxT( "Fmt 4.6" ) ) );
416 gerberStream5.Close();
417
418 // Verify precision 6 produces "Fmt 4.6" in the file header
419 GERBER_WRITER gerber6( &board );
420 gerber6.SetOptions( VECTOR2I( 0, 0 ) );
421 gerber6.SetFormat( 6 );
422 BOOST_REQUIRE( gerber6.CreateDrillandMapFilesSet( tempDir.GetFullPath(), true, false, false, nullptr ) );
423
424 wxFFile gerberStream6( gerberFile5.GetFullPath(), wxT( "rb" ) );
425 wxString gerberContents6;
426 BOOST_REQUIRE( gerberStream6.ReadAll( &gerberContents6 ) );
427 BOOST_CHECK_MESSAGE( gerberContents6.Contains( wxT( "Fmt 4.6" ) ),
428 "Expected 'Fmt 4.6' in gerber header with precision=6" );
429 BOOST_CHECK( !gerberContents6.Contains( wxT( "Fmt 4.5" ) ) );
430 gerberStream6.Close();
431
432 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
433}
434
435
436// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/23452
437// Drill files for vias spanning inner-to-back or back-to-inner layers produced
438// reversed layer order in the generated file name (e.g. "back-in2" instead of "in2-back").
439BOOST_AUTO_TEST_CASE( DrillFileLayerOrderInFilename )
440{
441 wxFileName tempDir = MakeTempDir();
442 wxFileName boardFile( tempDir.GetFullPath(), wxT( "layer_order_board.kicad_pcb" ) );
443
444 BOARD board;
445 board.SetCopperLayerCount( 4 );
446 board.SetFileName( boardFile.GetFullPath() );
447
448 // Via spanning In2 (bottom inner) to B_Cu: file must be named "in2-back", not "back-in2".
449 PCB_VIA* via = new PCB_VIA( &board );
450 via->SetPadstackMode( PADSTACK::MODE::NORMAL );
451 via->SetPosition( VECTOR2I( 0, 0 ) );
452 via->SetViaType( VIATYPE::BURIED );
453 via->SetLayerPair( In2_Cu, B_Cu );
454 via->SetDrill( pcbIUScale.mmToIU( 0.30 ) );
455 via->SetWidth( PADSTACK::ALL_LAYERS, pcbIUScale.mmToIU( 0.60 ) );
456 board.Add( via );
457
458 EXCELLON_WRITER excellon( &board );
459 excellon.SetOptions( false, false, VECTOR2I( 0, 0 ), false );
460 excellon.SetFormat( true );
461 BOOST_REQUIRE( excellon.CreateDrillandMapFilesSet( tempDir.GetFullPath(), true, false, nullptr ) );
462
463 // Correct order: top inner layer first, then back layer.
464 wxFileName correctFile( tempDir.GetFullPath(), wxT( "layer_order_board-in2-back.drl" ) );
465 wxFileName reversedFile( tempDir.GetFullPath(), wxT( "layer_order_board-back-in2.drl" ) );
466 BOOST_CHECK_MESSAGE( correctFile.FileExists(), "Expected drill file 'in2-back' not found" );
467 BOOST_CHECK_MESSAGE( !reversedFile.FileExists(), "Incorrectly named drill file 'back-in2' found" );
468
469 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
470}
471
472
473// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/23005
474// GenDrillReportFile crashed when aReporter was null (the default)
475BOOST_AUTO_TEST_CASE( DrillReportNullReporter )
476{
477 wxFileName tempDir = MakeTempDir();
478 wxFileName boardFile( tempDir.GetFullPath(), wxT( "test_board.kicad_pcb" ) );
479
480 BOARD board;
481 board.SetCopperLayerCount( 2 );
482 board.SetFileName( boardFile.GetFullPath() );
483
484 wxFileName reportFile( tempDir.GetFullPath(), wxT( "test_board-drl.rpt" ) );
485
486 // Valid path with null reporter should succeed without crashing
487 EXCELLON_WRITER excellon( &board );
488 BOOST_CHECK( excellon.GenDrillReportFile( reportFile.GetFullPath() ) );
489 BOOST_CHECK( reportFile.FileExists() );
490
491 GERBER_WRITER gerber( &board );
492 BOOST_CHECK( gerber.GenDrillReportFile( reportFile.GetFullPath() ) );
493
494 // Invalid path with null reporter should return false without crashing
495 EXCELLON_WRITER excellon2( &board );
496 BOOST_CHECK( !excellon2.GenDrillReportFile( wxT( "/nonexistent/path/report.rpt" ) ) );
497
498 GERBER_WRITER gerber2( &board );
499 BOOST_CHECK( !gerber2.GenDrillReportFile( wxT( "/nonexistent/path/report.rpt" ) ) );
500
501 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
502}
503
504
505// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/23289
506// GenDrillReportFile crashed with SIGSEGV when the board had drills because
507// printToolSummary() passed integer literal 0 instead of the FILE* to fmt::print()
508BOOST_AUTO_TEST_CASE( DrillReportWithTools )
509{
510 wxFileName tempDir = MakeTempDir();
511 wxFileName boardFile( tempDir.GetFullPath(), wxT( "test_board_with_drills.kicad_pcb" ) );
512
513 BOARD board;
514 board.SetCopperLayerCount( 2 );
515 board.SetFileName( boardFile.GetFullPath() );
516
517 PCB_VIA* via = new PCB_VIA( &board );
518 via->SetPadstackMode( PADSTACK::MODE::NORMAL );
519 via->SetPosition( VECTOR2I( 0, 0 ) );
520 via->SetLayerPair( F_Cu, B_Cu );
521 via->SetDrill( pcbIUScale.mmToIU( 0.30 ) );
522 via->SetWidth( PADSTACK::ALL_LAYERS, pcbIUScale.mmToIU( 0.60 ) );
523 board.Add( via );
524
525 wxFileName reportFile( tempDir.GetFullPath(), wxT( "test_board_with_drills-drl.rpt" ) );
526
527 EXCELLON_WRITER excellon( &board );
528 excellon.SetOptions( false, false, VECTOR2I( 0, 0 ), false );
529 excellon.SetFormat( true );
530 BOOST_CHECK_NO_THROW( excellon.GenDrillReportFile( reportFile.GetFullPath() ) );
531 BOOST_CHECK( reportFile.FileExists() );
532
533 wxFFile reportStream( reportFile.GetFullPath(), wxT( "rb" ) );
534 wxString reportContents;
535 BOOST_REQUIRE( reportStream.ReadAll( &reportContents ) );
536 BOOST_CHECK( reportContents.Contains( wxT( "T1" ) ) );
537 BOOST_CHECK( reportContents.Contains( wxT( "0.300mm" ) ) );
538 reportStream.Close();
539
540 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
541}
542
543
544// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/24014
545// A non-filled PCB_SHAPE rectangle on F.SilkS was emitted as a donut_rc symbol with a
546// corner radius smaller than half the line width. Many ODB++ viewers reject the
547// resulting degenerate symbol, so the outline appeared to be missing. The exporter now
548// emits the four outline edges as line features, matching how a rectangle built from
549// individual line segments is exported.
550BOOST_AUTO_TEST_CASE( OdbPpUnfilledRectangleOnSilk )
551{
552 wxFileName tempDir = MakeTempDir();
553 wxFileName boardFile( tempDir.GetFullPath(), wxT( "silk_rect_board.kicad_pcb" ) );
554
555 BOARD board;
556 board.SetCopperLayerCount( 2 );
557 board.SetFileName( boardFile.GetFullPath() );
558
559 // Non-filled rectangle on F.SilkS
560 PCB_SHAPE* rect = new PCB_SHAPE( &board, SHAPE_T::RECTANGLE );
561 rect->SetStart( VECTOR2I( pcbIUScale.mmToIU( 10.0 ), pcbIUScale.mmToIU( 10.0 ) ) );
562 rect->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 30.0 ), pcbIUScale.mmToIU( 20.0 ) ) );
563 rect->SetLayer( F_SilkS );
564 rect->SetFilled( false );
565 rect->SetWidth( pcbIUScale.mmToIU( 0.15 ) );
566 board.Add( rect );
567
568 wxFileName odbRoot( tempDir.GetFullPath(), wxEmptyString );
569 odbRoot.AppendDir( wxT( "odb_out" ) );
570 BOOST_REQUIRE( odbRoot.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
571
572 PCB_IO_ODBPP odbExporter;
573 std::map<std::string, UTF8> props;
574 props["units"] = "mm";
575 props["sigfig"] = "4";
576 BOOST_REQUIRE_NO_THROW( odbExporter.SaveBoard( odbRoot.GetFullPath(), board, &props ) );
577
578 wxFileName silkFeatures( odbRoot.GetFullPath(), wxT( "features" ) );
579 silkFeatures.AppendDir( wxT( "steps" ) );
580 silkFeatures.AppendDir( wxT( "pcb" ) );
581 silkFeatures.AppendDir( wxT( "layers" ) );
582 silkFeatures.AppendDir( wxT( "f.silkscreen" ) );
583 BOOST_REQUIRE( silkFeatures.FileExists() );
584
585 wxFFile silkStream( silkFeatures.GetFullPath(), wxT( "rb" ) );
586 wxString silkContents;
587 BOOST_REQUIRE( silkStream.ReadAll( &silkContents ) );
588 silkStream.Close();
589
590 // Four ODB++ line ("L ...") features describe the rectangle outline.
591 int lineCount = 0;
592 wxStringTokenizer lines( silkContents, wxT( "\n" ) );
593
594 while( lines.HasMoreTokens() )
595 {
596 if( lines.GetNextToken().StartsWith( wxT( "L " ) ) )
597 lineCount++;
598 }
599
600 BOOST_CHECK_EQUAL( lineCount, 4 );
601
602 // The degenerate donut_rc symbol should not appear anymore.
603 BOOST_CHECK( !silkContents.Contains( wxT( "donut_rc" ) ) );
604
605 wxFileName::Rmdir( odbRoot.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
606 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
607}
608
609
610BOOST_AUTO_TEST_CASE( OdbPpDegenerateTrackArc )
611{
612 wxFileName tempDir = MakeTempDir();
613
614 BOARD board;
615 board.SetCopperLayerCount( 2 );
616
617 NETINFO_ITEM* net = new NETINFO_ITEM( &board, wxT( "TestNet" ), 1 );
618 board.Add( net );
619
620 PCB_ARC* arc = new PCB_ARC( &board );
621 arc->SetStart( VECTOR2I( 110737101, 51206997 ) );
622 arc->SetMid( VECTOR2I( 110737003, 51206898 ) );
623 arc->SetEnd( VECTOR2I( 110736905, 51206799 ) );
624 arc->SetWidth( pcbIUScale.mmToIU( 0.11684 ) );
625 arc->SetLayer( F_Cu );
626 arc->SetNet( net );
627
628 board.Add( arc );
629
630 wxFileName odbRoot( tempDir.GetFullPath(), wxEmptyString );
631 odbRoot.AppendDir( wxT( "odb_out" ) );
632 BOOST_REQUIRE( odbRoot.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
633
634 PCB_IO_ODBPP odbExporter;
635 std::map<std::string, UTF8> props;
636 props["units"] = "mm";
637 props["sigfig"] = "4";
638 BOOST_REQUIRE_NO_THROW( odbExporter.SaveBoard( odbRoot.GetFullPath(), board, &props ) );
639
640 wxFileName copperFeatures( odbRoot.GetFullPath(), wxT( "features" ) );
641 copperFeatures.AppendDir( wxT( "steps" ) );
642 copperFeatures.AppendDir( wxT( "pcb" ) );
643 copperFeatures.AppendDir( wxT( "layers" ) );
644 copperFeatures.AppendDir( wxT( "f.cu" ) );
645 BOOST_REQUIRE( copperFeatures.FileExists() );
646
647 wxFFile copperStream( copperFeatures.GetFullPath(), wxT( "rb" ) );
648 wxString copperContents;
649 BOOST_REQUIRE( copperStream.ReadAll( &copperContents ) );
650 copperStream.Close();
651
652 int lineCount = 0;
653 int arcCount = 0;
654 wxStringTokenizer lines( copperContents, wxT( "\n" ) );
655
656 while( lines.HasMoreTokens() )
657 {
658 wxString line = lines.GetNextToken();
659
660 if( line.StartsWith( wxT( "L " ) ) )
661 lineCount++;
662 else if( line.StartsWith( wxT( "A " ) ) )
663 arcCount++;
664 }
665
666 BOOST_CHECK_EQUAL( lineCount, 1 );
667 BOOST_CHECK_EQUAL( arcCount, 0 );
668
669 wxFileName::Rmdir( odbRoot.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
670 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
671}
672
673
674// Export aBoard to a fresh ODB++ tree below aTempDir and return the root of that tree. Symbol
675// dimensions come out in micrometres because the file units are millimetres.
676static wxFileName ExportOdbTree( BOARD* aBoard, const wxFileName& aTempDir )
677{
678 wxFileName odbRoot( aTempDir.GetFullPath(), wxEmptyString );
679 odbRoot.AppendDir( wxT( "odb_out" ) );
680 BOOST_REQUIRE( odbRoot.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
681
682 PCB_IO_ODBPP odbExporter;
683 std::map<std::string, UTF8> props;
684 props["units"] = "mm";
685 props["sigfig"] = "4";
686 BOOST_REQUIRE_NO_THROW( odbExporter.SaveBoard( odbRoot.GetFullPath(), *aBoard, &props ) );
687
688 return odbRoot;
689}
690
691
692static wxString ReadOdbLayerFeatures( const wxFileName& aOdbRoot, const wxString& aLayerDir )
693{
694 wxFileName features( aOdbRoot.GetFullPath(), wxT( "features" ) );
695 features.AppendDir( wxT( "steps" ) );
696 features.AppendDir( wxT( "pcb" ) );
697 features.AppendDir( wxT( "layers" ) );
698 features.AppendDir( aLayerDir );
699 BOOST_REQUIRE( features.FileExists() );
700
701 wxFFile stream( features.GetFullPath(), wxT( "rb" ) );
702 wxString contents;
703 BOOST_REQUIRE( stream.ReadAll( &contents ) );
704 stream.Close();
705
706 return contents;
707}
708
709
710// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/25089
711// The inner diameter of the donut_r symbol standing in for an unfilled circle subtracted only
712// half the line width from the diameter, so the exported annulus was a quarter width too thin
713// and sat off-centre from the circle it came from. The board is the reporter's own project.
714BOOST_AUTO_TEST_CASE( OdbPpUnfilledCircleAnnulus )
715{
716 SETTINGS_MANAGER settingsManager;
717 std::unique_ptr<BOARD> board;
718
719 KI_TEST::LoadBoard( settingsManager, wxT( "issue25089/odb_circles" ), board );
720 BOOST_REQUIRE( board );
721
722 wxFileName tempDir = MakeTempDir();
723 wxFileName odbRoot = ExportOdbTree( board.get(), tempDir );
724
725 // Each copper circle is 2 mm across with a 0.1 mm stroke, so the ring spans radius
726 // 0.95 mm to 1.05 mm
727 wxString contents = ReadOdbLayerFeatures( odbRoot, wxT( "f.cu" ) );
728 BOOST_CHECK_MESSAGE( contents.Contains( wxT( "donut_r2100.0x1900.0" ) ),
729 "Wrong copper annulus, features file holds:\n" + contents );
730
731 wxFileName::Rmdir( odbRoot.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
732 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
733}
734
735
736// Companion to OdbPpUnfilledCircleAnnulus covering the two circles that have no donut_r spelling,
737// derived from the same project. On F.Cu the stroke is as wide as the circle and closes the hole;
738// on B.Cu the radius sits at the EDA_SHAPE clamp of INT_MAX / 2, where the doubled diameter used to
739// overflow a signed int and emit a negative dimension.
740BOOST_AUTO_TEST_CASE( OdbPpUnfilledCircleWithoutHole )
741{
742 SETTINGS_MANAGER settingsManager;
743 std::unique_ptr<BOARD> board;
744
745 KI_TEST::LoadBoard( settingsManager, wxT( "issue25089/odb_circle_edge_cases" ), board );
746 BOOST_REQUIRE( board );
747
748 wxFileName tempDir = MakeTempDir();
749 wxFileName odbRoot = ExportOdbTree( board.get(), tempDir );
750
751 wxString frontContents = ReadOdbLayerFeatures( odbRoot, wxT( "f.cu" ) );
752
753 BOOST_CHECK_MESSAGE( frontContents.Contains( wxT( "r400.0" ) )
754 && !frontContents.Contains( wxT( "donut" ) ),
755 "Circle with a hole-closing stroke should export as a solid pad, "
756 "features file holds:\n"
757 + frontContents );
758
759 // Feature records carry signed Y coordinates, so only the symbol definitions can be checked
760 wxString backContents = ReadOdbLayerFeatures( odbRoot, wxT( "b.cu" ) );
761 wxStringTokenizer backLines( backContents, wxT( "\n" ) );
762
763 while( backLines.HasMoreTokens() )
764 {
765 wxString line = backLines.GetNextToken();
766
767 if( line.StartsWith( wxT( "$" ) ) )
768 {
769 BOOST_CHECK_MESSAGE( !line.Contains( wxT( "-" ) ),
770 "Oversized circle overflowed to a negative symbol dimension: "
771 + line );
772 }
773 }
774
775 wxFileName::Rmdir( odbRoot.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
776 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
777}
778
779
780namespace
781{
782PAD* AddSlotPad( FOOTPRINT* aFootprint, const VECTOR2I& aPos, PAD_ATTRIB aAttribute )
783{
784 PAD* pad = new PAD( aFootprint );
785 pad->SetPadstackMode( PADSTACK::MODE::NORMAL );
786 pad->SetAttribute( aAttribute );
787 pad->SetLayerSet( aAttribute == PAD_ATTRIB::NPTH ? PAD::UnplatedHoleMask()
788 : PAD::PTHMask() );
789 pad->SetPosition( aPos );
791 pad->SetSize( PADSTACK::ALL_LAYERS, VECTOR2I( pcbIUScale.mmToIU( 2.0 ), pcbIUScale.mmToIU( 1.0 ) ) );
792 pad->SetDrillShape( PAD_DRILL_SHAPE::OBLONG );
793 pad->SetDrillSize( VECTOR2I( pcbIUScale.mmToIU( 1.7 ), pcbIUScale.mmToIU( 0.6 ) ) );
794 aFootprint->Add( pad );
795
796 return pad;
797}
798} // anonymous namespace
799
800
801// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/24677
802// Plated through-hole pads with a slotted (oval) drill were dropped entirely from the
803// ODB++ drill output. Round holes and non-plated slots exported fine, but plated slots
804// were never written to the plated drill layer. The exporter now emits PTH slots on the
805// plated drill layer just like NPTH slots on the non-plated layer.
806BOOST_AUTO_TEST_CASE( OdbPpPlatedSlotDrill )
807{
808 wxFileName tempDir = MakeTempDir();
809 wxFileName boardFile( tempDir.GetFullPath(), wxT( "plated_slot_board.kicad_pcb" ) );
810
811 BOARD board;
812 board.SetCopperLayerCount( 2 );
813 board.SetFileName( boardFile.GetFullPath() );
814
815 FOOTPRINT* fp = new FOOTPRINT( &board );
816 fp->SetPosition( VECTOR2I( pcbIUScale.mmToIU( 50.0 ), pcbIUScale.mmToIU( 50.0 ) ) );
817 board.Add( fp );
818
819 // A plated slot and a non-plated slot in the same design.
820 AddSlotPad( fp, VECTOR2I( pcbIUScale.mmToIU( 50.0 ), pcbIUScale.mmToIU( 50.0 ) ),
822 AddSlotPad( fp, VECTOR2I( pcbIUScale.mmToIU( 60.0 ), pcbIUScale.mmToIU( 50.0 ) ),
824
825 wxFileName odbRoot( tempDir.GetFullPath(), wxEmptyString );
826 odbRoot.AppendDir( wxT( "odb_out" ) );
827 BOOST_REQUIRE( odbRoot.Mkdir( wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
828
829 PCB_IO_ODBPP odbExporter;
830 std::map<std::string, UTF8> props;
831 props["units"] = "mm";
832 props["sigfig"] = "4";
833 BOOST_REQUIRE_NO_THROW( odbExporter.SaveBoard( odbRoot.GetFullPath(), board, &props ) );
834
835 auto layerDir = [&]( const wxString& aLayer )
836 {
837 wxFileName dir( odbRoot.GetFullPath(), wxEmptyString );
838 dir.AppendDir( wxT( "steps" ) );
839 dir.AppendDir( wxT( "pcb" ) );
840 dir.AppendDir( wxT( "layers" ) );
841 dir.AppendDir( aLayer );
842
843 return dir;
844 };
845
846 auto readFile = []( const wxFileName& aFile )
847 {
848 wxFFile stream( aFile.GetFullPath(), wxT( "rb" ) );
849 wxString contents;
850 BOOST_REQUIRE( stream.ReadAll( &contents ) );
851 stream.Close();
852
853 return contents;
854 };
855
856 // ODB++ array members (e.g. drill tools) are indented, so trim before matching.
857 auto countLinesStartingWith = []( const wxString& aContents, const wxString& aPrefix )
858 {
859 int count = 0;
860 wxStringTokenizer lines( aContents, wxT( "\n" ) );
861
862 while( lines.HasMoreTokens() )
863 {
864 wxString line = lines.GetNextToken();
865 line.Trim( false );
866
867 if( line.StartsWith( aPrefix ) )
868 count++;
869 }
870
871 return count;
872 };
873
874 auto containsOvalSymbol = []( const wxString& aContents )
875 {
876 wxStringTokenizer lines( aContents, wxT( "\n" ) );
877
878 while( lines.HasMoreTokens() )
879 {
880 wxString line = lines.GetNextToken();
881
882 // Symbol definition lines look like "$0 oval<w>x<h>".
883 if( line.StartsWith( wxT( "$" ) ) && line.Contains( wxT( "oval" ) ) )
884 return true;
885 }
886
887 return false;
888 };
889
890 // The plated slot must appear on the plated drill layer as exactly one line feature (drill
891 // layers hold only lines and circular pads), and must NOT leak onto this layer as a
892 // non-plated hole.
893 wxFileName platedDir = layerDir( wxT( "drill_plated_f.cu-b.cu" ) );
894 BOOST_REQUIRE_MESSAGE( platedDir.DirExists(), "Plated drill layer should exist" );
895
896 wxFileName platedFeatures( platedDir.GetFullPath(), wxT( "features" ) );
897 BOOST_REQUIRE( platedFeatures.FileExists() );
898 wxString platedContents = readFile( platedFeatures );
899
900 BOOST_CHECK_EQUAL( countLinesStartingWith( platedContents, wxT( "L " ) ), 1 );
901 BOOST_CHECK_MESSAGE( !containsOvalSymbol( platedContents ),
902 "Plated slot should be a line, not an oval pad" );
903
904 wxFileName platedTools( platedDir.GetFullPath(), wxT( "tools" ) );
905 BOOST_REQUIRE( platedTools.FileExists() );
906 wxString platedToolsContents = readFile( platedTools );
907 BOOST_CHECK_EQUAL( countLinesStartingWith( platedToolsContents, wxT( "TYPE=PLATED" ) ), 1 );
908 BOOST_CHECK_EQUAL( countLinesStartingWith( platedToolsContents, wxT( "TYPE=NON_PLATED" ) ), 0 );
909
910 // The non-plated slot must still appear on the non-plated drill layer (unchanged), and
911 // the plated slot must NOT leak onto it.
912 wxFileName nonPlatedDir = layerDir( wxT( "drill_non-plated_f.cu-b.cu" ) );
913 BOOST_REQUIRE_MESSAGE( nonPlatedDir.DirExists(), "Non-plated drill layer should exist" );
914
915 wxFileName nonPlatedFeatures( nonPlatedDir.GetFullPath(), wxT( "features" ) );
916 BOOST_REQUIRE( nonPlatedFeatures.FileExists() );
917 wxString nonPlatedContents = readFile( nonPlatedFeatures );
918
919 BOOST_CHECK_EQUAL( countLinesStartingWith( nonPlatedContents, wxT( "L " ) ), 1 );
920 BOOST_CHECK_MESSAGE( !containsOvalSymbol( nonPlatedContents ),
921 "Non-plated slot should be a line, not an oval pad" );
922
923 wxFileName nonPlatedTools( nonPlatedDir.GetFullPath(), wxT( "tools" ) );
924 BOOST_REQUIRE( nonPlatedTools.FileExists() );
925 wxString nonPlatedToolsContents = readFile( nonPlatedTools );
926 BOOST_CHECK_EQUAL( countLinesStartingWith( nonPlatedToolsContents, wxT( "TYPE=NON_PLATED" ) ),
927 1 );
928 BOOST_CHECK_EQUAL( countLinesStartingWith( nonPlatedToolsContents, wxT( "TYPE=PLATED" ) ), 0 );
929
930 wxFileName::Rmdir( odbRoot.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
931 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
932}
933
934
935// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/25021
936// Backdrill holes are recorded as non-plated, so the report's backdrill sections asked
937// printToolSummary() for plated tools only and always claimed zero holes, even though
938// the matching Excellon files carried them.
939BOOST_AUTO_TEST_CASE( DrillReportBackdrillHoleCount )
940{
941 SETTINGS_MANAGER settingsManager;
942 std::unique_ptr<BOARD> board;
943
944 KI_TEST::LoadBoard( settingsManager, wxT( "issue25021/backdrill" ), board );
945 BOOST_REQUIRE( board );
946
947 wxFileName tempDir = MakeTempDir();
948 wxFileName boardFile( tempDir.GetFullPath(), wxT( "backdrill.kicad_pcb" ) );
949
950 // Keep the derived drill file names out of the source tree
951 board->SetFileName( boardFile.GetFullPath() );
952
953 wxFileName reportFile( tempDir.GetFullPath(), wxT( "backdrill-drl.rpt" ) );
954
955 EXCELLON_WRITER excellon( board.get() );
956 excellon.SetOptions( false, false, VECTOR2I( 0, 0 ), false );
957 excellon.SetFormat( true );
958 BOOST_REQUIRE( excellon.GenDrillReportFile( reportFile.GetFullPath() ) );
959
960 wxFFile reportStream( reportFile.GetFullPath(), wxT( "rb" ) );
961 wxString reportContents;
962 BOOST_REQUIRE( reportStream.ReadAll( &reportContents ) );
963 reportStream.Close();
964
965 struct BACKDRILL_SECTION
966 {
967 long holes = -1;
968 int toolLines = 0;
969 };
970
971 std::map<wxString, BACKDRILL_SECTION> sections;
972 wxString currentFile;
973 wxStringTokenizer lines( reportContents, wxT( "\n" ) );
974
975 while( lines.HasMoreTokens() )
976 {
977 wxString line = lines.GetNextToken();
978 line.Trim( true ).Trim( false );
979
980 if( line.StartsWith( wxT( "Drill file '" ) ) )
981 currentFile = line.AfterFirst( '\'' ).BeforeFirst( '\'' );
982
983 if( line.StartsWith( wxT( "T" ) ) && line.Contains( wxT( "0.330mm" ) ) )
984 sections[currentFile].toolLines++;
985
986 wxString count;
987
988 if( line.StartsWith( wxT( "Total backdrilled holes count " ), &count ) )
989 {
990 long value = -1;
991 BOOST_REQUIRE( count.ToLong( &value ) );
992 sections[currentFile].holes = value;
993 }
994 }
995
996 // The board backdrills two vias from F.Cu down to In3.Cu, and one via each from
997 // B.Cu up to In3.Cu and to In6.Cu
998 const std::map<wxString, long> expected = {
999 { wxT( "backdrill_Backdrills_Drill_1_4.drl" ), 2 },
1000 { wxT( "backdrill_Backdrills_Drill_10_4.drl" ), 1 },
1001 { wxT( "backdrill_Backdrills_Drill_10_7.drl" ), 1 }
1002 };
1003
1004 // No backdrill section beyond the three expected ones
1005 BOOST_CHECK_EQUAL( sections.size(), expected.size() );
1006
1007 for( const auto& [file, holes] : expected )
1008 {
1009 auto section = sections.find( file );
1010
1011 if( section == sections.end() )
1012 {
1013 BOOST_ERROR( "No backdrill section for " << file );
1014 continue;
1015 }
1016
1017 BOOST_CHECK_EQUAL( section->second.holes, holes );
1018
1019 // The section must also list the 0.33mm backdrill tool it counted
1020 BOOST_CHECK_EQUAL( section->second.toolLines, 1 );
1021 }
1022
1023 // Plated through holes and the unplated summary must be unaffected
1024 BOOST_CHECK( reportContents.Contains( wxT( "Total plated holes count 4" ) ) );
1025 BOOST_CHECK( reportContents.Contains( wxT( "Total unplated holes count 0" ) ) );
1026
1027 wxFileName::Rmdir( tempDir.GetFullPath(), wxPATH_RMDIR_RECURSIVE );
1028}
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.
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 SetFileName(const wxString &aFileName)
Definition board.h:451
void SetCopperLayerCount(int aCount)
Definition board.cpp:1144
virtual void SetFilled(bool aFlag)
Definition eda_shape.h:142
Create Excellon drill, drill map, and drill report files.
void SetFormat(bool aMetric, ZEROS_FMT aZerosFmt=DECIMAL_FORMAT, int aLeftDigits=0, int aRightDigits=0)
Initialize internal parameters to match the given format.
bool CreateDrillandMapFilesSet(const wxString &aPlotDirectory, bool aGenDrill, bool aGenMap, REPORTER *aReporter=nullptr)
Create the full set of Excellon drill file for the board.
void SetOptions(bool aMirror, bool aMinimalHeader, const VECTOR2I &aOffset, bool aMerge_PTH_NPTH)
Initialize internal parameters to match drill options.
void SetPosition(const VECTOR2I &aPos) override
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Adds an item to the container.
bool GenDrillReportFile(const wxString &aFullFileName, REPORTER *aReporter=nullptr)
Create a plain text report file giving a list of drill values and drill count for through holes,...
Used to create Gerber drill files.
bool CreateDrillandMapFilesSet(const wxString &aPlotDirectory, bool aGenDrill, bool aGenMap, bool aGenTenting, REPORTER *aReporter=nullptr)
Create the full set of Excellon drill file for the board filenames are computed from the board name,...
void SetOptions(const VECTOR2I &aOffset)
Initialize internal parameters to match drill options.
void SetFormat(int aRightDigits=6)
Initialize internal parameters to match the given format.
wxString PathStr() const
Get the path to the temporary directory as a wxString.
Definition file_utils.h:62
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
static LSET PTHMask()
layer set for a through hole pad
Definition pad.cpp:616
static LSET UnplatedHoleMask()
layer set for a mechanical unplated through hole pad
Definition pad.cpp:637
void SetMid(const VECTOR2I &aMid)
Definition pcb_track.h:294
void SaveBoard(const wxString &aFileName, BOARD &aBoard, const std::map< std::string, UTF8 > *aProperties=nullptr) override
Write aBoard to a storage file in a format that this PCB_IO implementation knows about or it can be u...
void SetWidth(int aWidth) override
void SetEnd(const VECTOR2I &aEnd) override
void SetLayer(PCB_LAYER_ID aLayer) override
Set the layer this item is on.
void SetStart(const VECTOR2I &aStart) 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
void SetSecondaryDrillStartLayer(PCB_LAYER_ID aLayer)
void SetTertiaryDrillEndLayer(PCB_LAYER_ID aLayer)
void SetDrill(int aDrill)
Definition pcb_track.h:798
void SetPadstackMode(PADSTACK::MODE aMode)
Definition pcb_track.h:491
void SetSecondaryDrillEndLayer(PCB_LAYER_ID aLayer)
void SetPosition(const VECTOR2I &aPoint) override
Definition pcb_track.h:600
void SetLayerPair(PCB_LAYER_ID aTopLayer, PCB_LAYER_ID aBottomLayer)
For a via m_layer contains the top layer, the other layer is in m_bottomLayer/.
void SetTertiaryDrillStartLayer(PCB_LAYER_ID aLayer)
void SetTertiaryDrillSize(const VECTOR2I &aSize)
void SetSecondaryDrillSize(const VECTOR2I &aSize)
void SetWidth(int aWidth) override
std::vector< DRILL_SPAN > EnumerateDrillSpans(const BOARD &aBoard, bool aPadBackdrills)
Every drill span present on the board, through-holes first.
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
Classes used in drill files, map files and report files generation.
Classes used in drill files, map files and report files generation.
@ B_Cu
Definition layer_ids.h:61
@ In2_Cu
Definition layer_ids.h:63
@ F_SilkS
Definition layer_ids.h:96
@ In1_Cu
Definition layer_ids.h:62
@ In3_Cu
Definition layer_ids.h:64
@ F_Cu
Definition layer_ids.h:60
std::string GetPcbnewTestDataDir()
Utility which returns a path to the data directory where the test board files are stored.
void LoadBoard(SETTINGS_MANAGER &aSettingsManager, const wxString &aRelPath, std::unique_ptr< BOARD > &aBoard)
std::unique_ptr< BOARD > ReadBoardFromFileOrStream(const std::string &aFilename, std::istream &aFallback)
Read a board from a file, or another stream, as appropriate.
PAD_ATTRIB
The set of pad shapes, used with PAD::{Set,Get}Attribute().
Definition padstack.h:96
@ NPTH
like PAD_PTH, but not plated mechanical use only, no connection allowed
Definition padstack.h:102
@ PTH
Plated through hole pad.
Definition padstack.h:97
static bool readFile(const wxString &aFileName, wxString &aOut, size_t aLimit=0)
Read a file into aOut.
One machining action, which is also one NC hit and one tool assignment.
Selects which operations EnumerateDrillOperations() returns.
DRILL_SPAN m_Span
static wxString ReadOdbLayerFeatures(const wxFileName &aOdbRoot, const wxString &aLayerDir)
static wxFileName ExportOdbTree(BOARD *aBoard, const wxFileName &aTempDir)
BOOST_AUTO_TEST_CASE(GerberDrillBoardDefaultsCreateProtectionFiles)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
VECTOR3I expected(15, 30, 45)
BOOST_CHECK_EQUAL(result, "25.4")
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:708