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