KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_board_downgrade.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 modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation, either version 3 of the License, or (at your
9 * option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * 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 <algorithm>
23#include <cmath>
24#include <fstream>
25#include <map>
26#include <memory>
27#include <filesystem>
28#include <set>
29
30#include <wx/image.h>
31#include <wx/imagpng.h>
32#include <wx/mstream.h>
33
34#include <base_units.h>
35#include <board.h>
37#include <footprint.h>
38#include <font/font.h>
39#include <gr_text.h>
40#include <pcb_track.h>
45#include <pad.h>
46#include <padstack.h>
47#include <pcb_barcode.h>
48#include <pcb_group.h>
49#include <pcb_dimension.h>
50#include <pcb_drill_chart.h>
51#include <pcb_drill_map.h>
52#include <pcb_grid_item.h>
53#include <pcb_reference_image.h>
54#include <pcb_shape.h>
55#include <pcb_table.h>
56#include <pcb_tablecell.h>
57#include <pcb_textbox.h>
59#include <zone.h>
60#include <zone_settings.h>
61#include <nlohmann/json.hpp>
62#include <netinfo.h>
63#include <project.h>
64#include <ki_exception.h>
65
67#include <downgrade_scan.h>
68#include <downgrade_target.h>
69#include <drc_rules_downgrade.h>
70#include <drc/drc_rule.h>
71#include <drc/drc_rule_parser.h>
78
79
80BOOST_AUTO_TEST_SUITE( BoardDowngrade )
81
82static const DOWNGRADE_TARGET& kicad9 = KI_TEST::RequireDowngradeTarget( wxT( "9.0" ) );
83static const DOWNGRADE_TARGET& kicad10 = KI_TEST::RequireDowngradeTarget( wxT( "10.0" ) );
84
85
86// A board with no post-10.0 features exports to 10.0 with nothing lost.
87BOOST_AUTO_TEST_CASE( EmptyBoardIsClean )
88{
89 BOARD board;
90
92
93 BOOST_CHECK( !report.IsLossy() );
94 BOOST_CHECK( !report.IsBlocked() );
95 BOOST_CHECK( report.Entries().empty() );
96}
97
98
99// The ellipse primitive only exists since 20260508, so 10.0 gets an approximation.
100BOOST_AUTO_TEST_CASE( EllipseIsLoweredForKicad10 )
101{
102 BOARD board;
103 board.Add( new PCB_SHAPE( &board, SHAPE_T::ELLIPSE ) );
104
106
107 BOOST_CHECK( report.IsLossy() );
109}
110
111
112// A target that already has the ellipse primitive keeps it. This proves the version gate.
113BOOST_AUTO_TEST_CASE( EllipseKeptForNewerTarget )
114{
115 BOARD board;
116 board.Add( new PCB_SHAPE( &board, SHAPE_T::ELLIPSE ) );
117
118 DOWNGRADE_TARGET future = { wxT( "future" ), wxT( "future" ), 20260623, 20260629, 20260629 };
119 COMPATIBILITY_REPORT report = ClassifyBoardForDowngrade( &board, future );
120
121 BOOST_CHECK( !report.IsLossy() );
122}
123
124
125BOOST_AUTO_TEST_CASE( EllipseTransformedToPolygon )
126{
127 BOARD board;
128 PCB_SHAPE* ellipse = new PCB_SHAPE( &board, SHAPE_T::ELLIPSE );
129 ellipse->SetStart( VECTOR2I( 0, 0 ) );
130 ellipse->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 5 ), 0 ) );
131 board.Add( ellipse );
132
134
135 BOOST_CHECK( ellipse->GetShape() == SHAPE_T::POLY );
136}
137
138
139// An unfilled ellipse must be lowered to its outline polygon, not to stroke debris.
140BOOST_AUTO_TEST_CASE( UnfilledEllipseKeepsOutline )
141{
142 BOARD board;
143 PCB_SHAPE* ellipse = new PCB_SHAPE( &board, SHAPE_T::ELLIPSE );
144 ellipse->SetEllipseCenter( VECTOR2I( 0, 0 ) );
145 ellipse->SetEllipseMajorRadius( pcbIUScale.mmToIU( 5 ) );
146 ellipse->SetEllipseMinorRadius( pcbIUScale.mmToIU( 3 ) );
147 ellipse->SetFillMode( FILL_T::NO_FILL );
148 ellipse->SetWidth( pcbIUScale.mmToIU( 0.2 ) );
149 board.Add( ellipse );
150
152
153 BOOST_REQUIRE( ellipse->GetShape() == SHAPE_T::POLY );
154 BOOST_REQUIRE_EQUAL( ellipse->GetPolyShape().OutlineCount(), 1 );
155 BOOST_CHECK( ellipse->GetFillMode() == FILL_T::NO_FILL );
156
157 double areaMM =
158 std::abs( ellipse->GetPolyShape().Outline( 0 ).Area() ) / ( pcbIUScale.IU_PER_MM * pcbIUScale.IU_PER_MM );
159
160 BOOST_CHECK_CLOSE( areaMM, M_PI * 5.0 * 3.0, 2.0 );
161}
162
163
164// An elliptical arc is an open curve, so it is lowered to a filled polygon of its ink.
165BOOST_AUTO_TEST_CASE( EllipseArcKeepsInk )
166{
167 BOARD board;
168 PCB_SHAPE* arc = new PCB_SHAPE( &board, SHAPE_T::ELLIPSE_ARC );
169 arc->SetEllipseCenter( VECTOR2I( 0, 0 ) );
170 arc->SetEllipseMajorRadius( pcbIUScale.mmToIU( 5 ) );
171 arc->SetEllipseMinorRadius( pcbIUScale.mmToIU( 3 ) );
174 arc->SetWidth( pcbIUScale.mmToIU( 0.3 ) );
175 board.Add( arc );
176
178
180
181 // A quarter arc of a 5 mm by 3 mm ellipse is about 6.4 mm of path at 0.3 mm width.
182 double areaMM = std::abs( arc->GetPolyShape().Area() ) / ( pcbIUScale.IU_PER_MM * pcbIUScale.IU_PER_MM );
183
184 BOOST_CHECK_GT( areaMM, 1.5 );
185 BOOST_CHECK_LT( areaMM, 2.6 );
186}
187
188
189// Text box knockout (20250210) postdates 9.0, so the flag is dropped and reported for 9.0.
190BOOST_AUTO_TEST_CASE( KnockoutTextBoxLoweredForKicad9 )
191{
192 BOARD board;
193 PCB_TEXTBOX* textbox = new PCB_TEXTBOX( &board );
194 textbox->SetIsKnockout( true );
195 board.Add( textbox );
196
199
200 DowngradeBoardInPlace( &board, kicad9 );
201
202 BOOST_CHECK( !textbox->IsKnockout() );
203}
204
205
206// Table cell knockout (20260603) postdates both targets, so the flag is dropped for 10.0.
207BOOST_AUTO_TEST_CASE( TableCellKnockoutLoweredForKicad10 )
208{
209 BOARD board;
210 PCB_TABLE* table = new PCB_TABLE( &board, pcbIUScale.mmToIU( 0.1 ) );
211 table->SetColCount( 1 );
212
213 PCB_TABLECELL* cell = new PCB_TABLECELL( table );
214 cell->SetIsKnockout( true );
215 table->AddCell( cell );
216 board.Add( table );
217
218 // A table inside a footprint carries the same flag and must be counted and lowered too.
219 FOOTPRINT* fp = new FOOTPRINT( &board );
220 PCB_TABLE* fpTable = new PCB_TABLE( fp, pcbIUScale.mmToIU( 0.1 ) );
221 fpTable->SetColCount( 1 );
222
223 PCB_TABLECELL* fpCell = new PCB_TABLECELL( fpTable );
224 fpCell->SetIsKnockout( true );
225 fpTable->AddCell( fpCell );
226 fp->Add( fpTable );
227 board.Add( fp );
228
230
232
233 BOOST_CHECK( !cell->IsKnockout() );
234 BOOST_CHECK( !fpCell->IsKnockout() );
235}
236
237
238// A knockout cell in a library footprint goes through the footprint-scoped rule.
239BOOST_AUTO_TEST_CASE( FootprintTableCellKnockoutLowered )
240{
241 FOOTPRINT* fp = new FOOTPRINT( nullptr );
242 PCB_TABLE* table = new PCB_TABLE( fp, pcbIUScale.mmToIU( 0.1 ) );
243 table->SetColCount( 1 );
244
245 PCB_TABLECELL* cell = new PCB_TABLECELL( table );
246 cell->SetIsKnockout( true );
247 table->AddCell( cell );
248 fp->Add( table );
249
251
252 BOOST_CHECK( !cell->IsKnockout() );
253 delete fp;
254}
255
256
257// Rounded rectangles (20250829) postdate 9.0, so 9.0 gets a polygon that keeps the rounding.
258BOOST_AUTO_TEST_CASE( RoundedRectangleLoweredForKicad9 )
259{
260 BOARD board;
261 PCB_SHAPE* rect = new PCB_SHAPE( &board, SHAPE_T::RECTANGLE );
262 rect->SetStart( VECTOR2I( 0, 0 ) );
263 rect->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 6 ) ) );
264 rect->SetCornerRadius( pcbIUScale.mmToIU( 1 ) );
266 rect->SetWidth( pcbIUScale.mmToIU( 0.2 ) );
267 board.Add( rect );
268
271
272 DowngradeBoardInPlace( &board, kicad9 );
273
275 BOOST_REQUIRE_EQUAL( rect->GetPolyShape().OutlineCount(), 1 );
276
277 // A 10 mm by 6 mm rectangle loses ( 4 - pi ) r^2 to the rounded corners.
278 double areaMM =
279 std::abs( rect->GetPolyShape().Outline( 0 ).Area() ) / ( pcbIUScale.IU_PER_MM * pcbIUScale.IU_PER_MM );
280
281 BOOST_CHECK_CLOSE( areaMM, 60.0 - ( 4.0 - M_PI ), 2.0 );
282}
283
284
285// A custom footprint stackup (20250818) remaps inner layers. 9.0 would expand them and change
286// the connectivity, so the export must block.
287BOOST_AUTO_TEST_CASE( CustomFootprintStackupBlocksKicad9 )
288{
289 BOARD board;
290 FOOTPRINT* fp = new FOOTPRINT( &board );
292 board.Add( fp );
293
296}
297
298
299// Per-zone layer properties (20250302) postdate 9.0, so they are dropped like the board-level
300// zone defaults.
301BOOST_AUTO_TEST_CASE( ZoneLayerPropertiesDroppedForKicad9 )
302{
303 BOARD board;
304 ZONE* zone = new ZONE( &board );
305 zone->LayerProperties()[F_Cu].hatching_offset = VECTOR2I( 100, 100 );
306 board.Add( zone );
307
308 // A zone inside a footprint carries the same properties and must be counted and dropped too.
309 FOOTPRINT* fp = new FOOTPRINT( &board );
310 ZONE* fpZone = new ZONE( fp );
311 fpZone->LayerProperties()[F_Cu].hatching_offset = VECTOR2I( 100, 100 );
312 fp->Add( fpZone );
313 board.Add( fp );
314
316
317 DowngradeBoardInPlace( &board, kicad9 );
318
319 BOOST_CHECK( zone->LayerProperties().empty() );
320 BOOST_CHECK( fpZone->LayerProperties().empty() );
321}
322
323
324BOOST_AUTO_TEST_CASE( BoardZoneDefaultsRoundTripAndDowngrade )
325{
326 SETTINGS_MANAGER settingsManager;
327 BOARD board;
328 board.GetDesignSettings().m_ZoneLayerProperties[F_Cu].hatching_offset = VECTOR2I( 100000, 200000 );
329 board.GetDesignSettings().m_ZoneLayerProperties[B_Cu].hatching_offset = VECTOR2I( 300000, 400000 );
330
331 KI_TEST::TEMPORARY_DIRECTORY tmp( "kicad_qa_board_zone_defaults" );
332 const std::string src = ( tmp.GetPath() / "source.kicad_pcb" ).string();
334 io.SaveBoard( src, board );
335 const std::string original = KI_TEST::ReadGoldenText( src );
336 std::unique_ptr<BOARD> current( io.LoadBoard( src, nullptr ) );
337 BOOST_REQUIRE( current );
338 const auto& loadedDefaults = current->GetDesignSettings().m_ZoneLayerProperties;
339 BOOST_REQUIRE_EQUAL( loadedDefaults.size(), 2 );
340 BOOST_CHECK( loadedDefaults.at( F_Cu ).hatching_offset == VECTOR2I( 100000, 200000 ) );
341 BOOST_CHECK( loadedDefaults.at( B_Cu ).hatching_offset == VECTOR2I( 300000, 400000 ) );
342 BOOST_CHECK( current->GetDesignSettings().GetDefaultZoneSettings().m_LayerProperties.empty() );
343
344 for( const auto& target : { kicad9, kicad10 } )
345 {
347 const std::string dest = ( tmp.GetPath() / ( target.m_id.ToStdString() + ".kicad_pcb" ) ).string();
348 BOOST_REQUIRE( ExportBoardToOlderVersion( src, dest, target, report ) );
349 std::unique_ptr<BOARD> reloaded( io.LoadBoard( dest, nullptr ) );
350 BOOST_REQUIRE( reloaded );
351 const auto& defaults = reloaded->GetDesignSettings().m_ZoneLayerProperties;
352
353 if( target.m_id == wxT( "9.0" ) )
354 {
356 BOOST_CHECK( defaults.empty() );
357 BOOST_CHECK( KI_TEST::ReadGoldenText( dest ).find( "zone_defaults" ) == std::string::npos );
358 }
359 else
360 {
361 BOOST_CHECK( !report.IsLossy() );
362 BOOST_REQUIRE_EQUAL( defaults.size(), 2 );
363 BOOST_CHECK( defaults.at( F_Cu ).hatching_offset == loadedDefaults.at( F_Cu ).hatching_offset );
364 BOOST_CHECK( defaults.at( B_Cu ).hatching_offset == loadedDefaults.at( B_Cu ).hatching_offset );
365 }
366
367 BOOST_CHECK( KI_TEST::ReadGoldenText( src ) == original );
368 }
369}
370
371
372// The old (at) header cannot represent a scaled footprint, so the export must block.
373BOOST_AUTO_TEST_CASE( ScaledFootprintBlocksExport )
374{
375 BOARD board;
376 FOOTPRINT* fp = new FOOTPRINT( &board );
377 fp->SetTransformScale( 2.0, 1.0 );
378 board.Add( fp );
379
381
382 BOOST_CHECK( report.IsBlocked() );
384}
385
386
387// IPC-4761 via protection postdates 9.0, so it is dropped and reported for 9.0.
388BOOST_AUTO_TEST_CASE( ViaProtectionDroppedForKicad9 )
389{
390 BOARD board;
391 PCB_VIA* via = new PCB_VIA( &board );
392 via->Padstack().Drill().is_capped = true;
393 board.Add( via );
394
396
397 DowngradeBoardInPlace( &board, kicad9 );
398
399 BOOST_CHECK( !via->Padstack().Drill().is_capped.has_value() );
400}
401
402
403// Frequency-dependent dielectric models postdate both targets, so they are dropped for 10.0.
404BOOST_AUTO_TEST_CASE( DielectricModelDroppedForKicad10 )
405{
406 BOARD board;
410
412
414
415 BOOST_CHECK( item->GetDielectricModel() == DIELECTRIC_MODEL::CONSTANT );
416}
417
418
419// Group design block links postdate 9.0, so they are dropped and reported for 9.0.
420BOOST_AUTO_TEST_CASE( GroupDesignBlockLinkDroppedForKicad9 )
421{
422 BOARD board;
423 PCB_GROUP* group = new PCB_GROUP( &board );
424 group->SetDesignBlockLibId( LIB_ID( wxT( "Lib" ), wxT( "Block" ) ) );
425 board.Add( group );
426
428
429 DowngradeBoardInPlace( &board, kicad9 );
430
431 BOOST_CHECK( !group->HasDesignBlockLink() );
432}
433
434
435// Extruded 3D bodies (20260410) have no equivalent in 10.0, so they are dropped.
436BOOST_AUTO_TEST_CASE( ExtrudedBodyDroppedForKicad10 )
437{
438 BOARD board;
439 FOOTPRINT* fp = new FOOTPRINT( &board );
440 fp->EnsureExtrudedBody();
441 board.Add( fp );
442
444
446
447 BOOST_CHECK( !fp->HasExtrudedBody() );
448}
449
450
451// A refill in the old version would turn a thieving pattern into solid copper, quietly.
452// That is silent corruption, so the export must block.
453BOOST_AUTO_TEST_CASE( ThievingZoneBlocksKicad10 )
454{
455 BOARD board;
456 ZONE* zone = new ZONE( &board );
458 board.Add( zone );
459
461
463
464 BOOST_CHECK( zone->IsCopperThieving() );
465}
466
467
468// Backdrilling changes copper connectivity between layers, so the export must block for 9.0.
469BOOST_AUTO_TEST_CASE( BackdrillBlocksKicad9 )
470{
471 BOARD board;
472 PCB_VIA* via = new PCB_VIA( &board );
473 via->SetSecondaryDrillSize( VECTOR2I( pcbIUScale.mmToIU( 0.3 ), pcbIUScale.mmToIU( 0.3 ) ) );
474 board.Add( via );
475
478}
479
480
481// The headline fail-closed promise: a blocked board export returns failure and writes nothing.
482// The staging name must not be guessable, because the writer resolves symlinks on purpose.
483BOOST_AUTO_TEST_CASE( BoardExportIgnoresAPredictableStagingSymlink )
484{
485 SETTINGS_MANAGER settingsManager;
486
487 const std::string src = KI_TEST::GetPcbnewTestDataDir() + "via_off_center.kicad_pcb";
488
489 KI_TEST::TEMPORARY_DIRECTORY tmp( "kicad_qa_staging_symlink", "" );
490 const wxString dest = ( tmp.GetPath() / "out.kicad_pcb" ).wstring();
491 const wxString victim = ( tmp.GetPath() / "victim.txt" ).wstring();
492
493 {
494 wxFFile file( victim, wxT( "wb" ) );
495 BOOST_REQUIRE( file.IsOpened() );
496 file.Write( wxT( "untouched" ) );
497 }
498
499 const wxString decoy =
500 dest + wxString::Format( wxT( ".downgrade_tmp.%lu" ), (unsigned long) wxGetProcessId() );
501 std::error_code ec;
502 std::filesystem::create_symlink( victim.ToStdString(), decoy.ToStdString(), ec );
503 BOOST_REQUIRE( !ec );
504
506 BOOST_CHECK( ExportBoardToOlderVersion( src, dest, kicad9, report ) );
507
508 wxString after;
509 {
510 wxFFile file( victim, wxT( "rb" ) );
511 BOOST_REQUIRE( file.IsOpened() && file.ReadAll( &after ) );
512 }
513
514 BOOST_CHECK( after == wxT( "untouched" ) );
515
516 // A normal save in the same directory shares this umask, so the modes must agree.
517 const wxString reference = ( tmp.GetPath() / "reference.kicad_pcb" ).wstring();
519 std::unique_ptr<BOARD> board( io.LoadBoard( dest, nullptr ) );
520 BOOST_REQUIRE( board );
521 io.SaveBoard( reference, *board );
522
523 BOOST_CHECK( std::filesystem::status( dest.ToStdString() ).permissions()
524 == std::filesystem::status( reference.ToStdString() ).permissions() );
525}
526
527
528BOOST_AUTO_TEST_CASE( BlockedBoardExportWritesNothing )
529{
530 SETTINGS_MANAGER settingsManager;
531
532 BOARD board;
533 ZONE* zone = new ZONE( &board );
534 zone->SetLayer( F_Cu );
535 zone->AppendCorner( VECTOR2I( 0, 0 ), -1 );
536 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 10 ), 0 ), -1 );
537 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 10 ) ), -1 );
539 board.Add( zone );
540
541 KI_TEST::TEMPORARY_DIRECTORY tmp( "kicad_qa_blocked_export", "" );
542 const wxString src = ( tmp.GetPath() / "src.kicad_pcb" ).wstring();
543 const wxString dest = ( tmp.GetPath() / "out.kicad_pcb" ).wstring();
544
546 io.SaveBoard( src, board );
547
549 BOOST_CHECK( !ExportBoardToOlderVersion( src, dest, kicad10, report ) );
550 BOOST_CHECK( report.IsBlocked() );
551 BOOST_CHECK( !wxFileExists( dest ) );
552}
553
554
555// Pad simulation electrical types (20260521) are dropped for 10.0.
556BOOST_AUTO_TEST_CASE( PadSimTypeDroppedForKicad10 )
557{
558 BOARD board;
559 FOOTPRINT* fp = new FOOTPRINT( &board );
560 PAD* pad = new PAD( fp );
561 pad->SetSimElectricalType( PAD_SIM_ELECTRICAL_TYPE::SOURCE );
562 fp->Add( pad );
563 board.Add( fp );
564
566
568
569 BOOST_CHECK( pad->GetSimElectricalType() == PAD_SIM_ELECTRICAL_TYPE::NONE );
570}
571
572
573// Counterbore / countersink is a post-9.0 feature 9.0 cannot parse, so it is dropped for 9.0.
574BOOST_AUTO_TEST_CASE( PostMachiningDroppedForKicad9 )
575{
576 BOARD board;
577 FOOTPRINT* fp = new FOOTPRINT( &board );
578 PAD* pad = new PAD( fp );
579 pad->Padstack().FrontPostMachining().mode = PAD_DRILL_POST_MACHINING_MODE::COUNTERBORE;
580 fp->Add( pad );
581 board.Add( fp );
582
584
585 DowngradeBoardInPlace( &board, kicad9 );
586
587 BOOST_CHECK( !pad->Padstack().FrontPostMachining().mode.has_value() );
588}
589
590
591// Press-fit is a post-9.0 pad property 9.0 cannot parse, so it is dropped for 9.0.
592BOOST_AUTO_TEST_CASE( PressFitDroppedForKicad9 )
593{
594 BOARD board;
595 FOOTPRINT* fp = new FOOTPRINT( &board );
596 PAD* pad = new PAD( fp );
597 pad->SetProperty( PAD_PROP::PRESSFIT );
598 fp->Add( pad );
599 board.Add( fp );
600
602
603 DowngradeBoardInPlace( &board, kicad9 );
604
605 BOOST_CHECK( pad->GetProperty() == PAD_PROP::NONE );
606}
607
608
609// Skip vias are post-9.0, so they are approximated by the nearest mode 9.0 understands.
610BOOST_AUTO_TEST_CASE( SkipViaLoweredForKicad9 )
611{
612 BOARD board;
613 PCB_VIA* via = new PCB_VIA( &board );
614 via->Padstack().SetUnconnectedLayerMode( UNCONNECTED_LAYER_MODE::START_END_ONLY );
615 board.Add( via );
616
618
619 DowngradeBoardInPlace( &board, kicad9 );
620
621 BOOST_CHECK( via->Padstack().UnconnectedLayerMode() == UNCONNECTED_LAYER_MODE::REMOVE_EXCEPT_START_AND_END );
622}
623
624
625// The v10 writer must not move anything. Every footprint and pad position in the export must match
626// the source exactly. This guards the extracted writer against silent model drift.
627BOOST_AUTO_TEST_CASE( ExportToKicad10PreservesGeometry )
628{
629 SETTINGS_MANAGER settingsManager;
630
631 const std::string src = KI_TEST::GetPcbnewTestDataDir() + "via_off_center.kicad_pcb";
632
633 KI_TEST::TEMPORARY_DIRECTORY tmp( "kicad_qa_downgrade_geom", "" );
634 const std::string dest = ( tmp.GetPath() / "out.kicad_pcb" ).string();
635
637 BOOST_REQUIRE( ExportBoardToOlderVersion( src, dest, kicad10, report ) );
638
640 std::unique_ptr<BOARD> source( io.LoadBoard( src, nullptr ) );
641 std::unique_ptr<BOARD> exported( io.LoadBoard( dest, nullptr ) );
642
643 BOOST_REQUIRE( source && exported );
644 BOOST_REQUIRE_EQUAL( source->Footprints().size(), exported->Footprints().size() );
645
646 for( size_t i = 0; i < source->Footprints().size(); i++ )
647 {
648 FOOTPRINT* a = source->Footprints()[i];
649 FOOTPRINT* b = exported->Footprints()[i];
650
652 BOOST_CHECK( a->GetPosition() == b->GetPosition() );
653 BOOST_CHECK( a->GetOrientation() == b->GetOrientation() );
654
655 BOOST_REQUIRE_EQUAL( a->Pads().size(), b->Pads().size() );
656
657 for( size_t p = 0; p < a->Pads().size(); p++ )
658 BOOST_CHECK( a->Pads()[p]->GetPosition() == b->Pads()[p]->GetPosition() );
659 }
660}
661
662
663// Same geometry guard for the v9 writer.
664BOOST_AUTO_TEST_CASE( ExportToKicad9PreservesGeometry )
665{
666 SETTINGS_MANAGER settingsManager;
667
668 const std::string src = KI_TEST::GetPcbnewTestDataDir() + "via_off_center.kicad_pcb";
669
670 KI_TEST::TEMPORARY_DIRECTORY tmp( "kicad_qa_downgrade_geom9", "" );
671 const std::string dest = ( tmp.GetPath() / "out.kicad_pcb" ).string();
672
674 BOOST_REQUIRE( ExportBoardToOlderVersion( src, dest, kicad9, report ) );
675
677 std::unique_ptr<BOARD> source( io.LoadBoard( src, nullptr ) );
678 std::unique_ptr<BOARD> exported( io.LoadBoard( dest, nullptr ) );
679
680 BOOST_REQUIRE( source && exported );
681 BOOST_REQUIRE_EQUAL( source->Footprints().size(), exported->Footprints().size() );
682
683 for( size_t i = 0; i < source->Footprints().size(); i++ )
684 {
685 FOOTPRINT* a = source->Footprints()[i];
686 FOOTPRINT* b = exported->Footprints()[i];
687
688 BOOST_CHECK( a->GetPosition() == b->GetPosition() );
689
690 BOOST_REQUIRE_EQUAL( a->Pads().size(), b->Pads().size() );
691
692 for( size_t pd = 0; pd < a->Pads().size(); pd++ )
693 BOOST_CHECK( a->Pads()[pd]->GetPosition() == b->Pads()[pd]->GetPosition() );
694 }
695}
696
697
698// Buried vias postdate 9.0's blind/buried split, so they are written with the old combined type.
699BOOST_AUTO_TEST_CASE( BuriedViaLoweredForKicad9 )
700{
701 BOARD board;
702 PCB_VIA* via = new PCB_VIA( &board );
703 via->SetViaType( VIATYPE::BURIED );
704 board.Add( via );
705
707
708 DowngradeBoardInPlace( &board, kicad9 );
709
710 BOOST_CHECK( via->GetViaType() == VIATYPE::BLIND );
711}
712
713
714// Per-layer zone defaults are a post-9.0 setup block, so they are dropped for 9.0.
715BOOST_AUTO_TEST_CASE( ZoneDefaultsDroppedForKicad9 )
716{
717 BOARD board;
719
721
722 DowngradeBoardInPlace( &board, kicad9 );
723
724 BOOST_CHECK( board.GetDesignSettings().m_ZoneLayerProperties.empty() );
725}
726
727
728// A barcode is fabricated ink, so it becomes its rendered polygons instead of vanishing.
729BOOST_AUTO_TEST_CASE( BarcodeLoweredToPolygonsForKicad9 )
730{
731 BOARD board;
732 PCB_BARCODE* barcode = new PCB_BARCODE( &board );
733 barcode->SetLayer( F_SilkS );
734 barcode->SetWidth( pcbIUScale.mmToIU( 10 ) );
735 barcode->SetHeight( pcbIUScale.mmToIU( 10 ) );
736 barcode->SetBarcodeText( wxT( "TEST123" ) );
737 board.Add( barcode );
738
741
742 // The net printed area, holes subtracted. A finder ring must not become a solid square.
744 barcode->TransformShapeToPolygon( expected, barcode->GetLayer(), 0, 0, ERROR_INSIDE );
745 expected.Simplify();
746 expected.Fracture();
747 double expectedMM = expected.Area() / ( pcbIUScale.IU_PER_MM * pcbIUScale.IU_PER_MM );
748
749 DowngradeBoardInPlace( &board, kicad9 );
750
751 int polyCount = 0;
752 double areaMM = 0;
753
754 for( BOARD_ITEM* item : board.Drawings() )
755 {
756 BOOST_REQUIRE( item->Type() != PCB_BARCODE_T );
757
758 if( item->Type() == PCB_SHAPE_T )
759 {
760 PCB_SHAPE* shape = static_cast<PCB_SHAPE*>( item );
761 BOOST_REQUIRE( shape->GetShape() == SHAPE_T::POLY );
762 BOOST_CHECK( shape->GetLayer() == F_SilkS );
763 polyCount++;
764 areaMM += std::abs( shape->GetPolyShape().Area() ) / ( pcbIUScale.IU_PER_MM * pcbIUScale.IU_PER_MM );
765 }
766 }
767
768 // The printed ink survives: many module polygons with a real total area.
769 BOOST_CHECK_GT( polyCount, 1 );
770 BOOST_CHECK_GT( areaMM, 5.0 );
771 BOOST_CHECK_CLOSE( areaMM, expectedMM, 0.1 );
772}
773
774
775BOOST_AUTO_TEST_CASE( BarcodeProjectVariablesResolvedBeforeExport )
776{
777 KI_TEST::TEMPORARY_DIRECTORY tmp( "kicad_qa_barcode_project" );
778 wxString projectPath = ( tmp.GetPath() / "source.kicad_pro" ).string();
779 wxString boardPath = ( tmp.GetPath() / "source.kicad_pcb" ).string();
780 wxString outputPath = ( tmp.GetPath() / "output.kicad_pcb" ).string();
781
782 {
783 std::ofstream project( projectPath.ToStdString() );
784 project << R"({"meta":{"version":3},"text_variables":{"SERIAL":"SERIAL123"}})";
785 }
786
787 SETTINGS_MANAGER settings;
788 BOOST_REQUIRE( settings.LoadProject( projectPath, false ) );
789 BOARD board;
790 board.SetProject( settings.GetProject( projectPath ), true );
791 PCB_BARCODE* barcode = new PCB_BARCODE( &board );
792 barcode->SetLayer( F_SilkS );
794 barcode->SetWidth( pcbIUScale.mmToIU( 10 ) );
795 barcode->SetHeight( pcbIUScale.mmToIU( 10 ) );
796 barcode->SetBarcodeText( wxT( "${SERIAL}" ) );
797 barcode->SetShowText( false );
798 board.Add( barcode );
799 BOOST_REQUIRE_EQUAL( barcode->GetShownText( FOR_CANVAS ), wxString( "SERIAL123" ) );
800
803 expected.Simplify();
804 expected.Fracture();
805 double expectedArea = expected.Area();
806
808 io.SaveBoard( boardPath, board );
810 BOOST_REQUIRE( ExportBoardToOlderVersion( boardPath, outputPath, kicad9, report ) );
811 std::unique_ptr<BOARD> result( io.LoadBoard( outputPath, nullptr ) );
813 int polygonCount = 0;
814 double resultArea = 0;
815
816 for( BOARD_ITEM* item : result->Drawings() )
817 {
818 BOOST_REQUIRE( item->Type() != PCB_BARCODE_T );
819
820 if( item->Type() == PCB_SHAPE_T )
821 {
822 PCB_SHAPE* shape = static_cast<PCB_SHAPE*>( item );
823 BOOST_REQUIRE( shape->GetShape() == SHAPE_T::POLY );
824 polygonCount++;
825 resultArea += std::abs( shape->GetPolyShape().Area() );
826 }
827 }
828
829 BOOST_CHECK_EQUAL( polygonCount, expected.OutlineCount() );
830 BOOST_CHECK_CLOSE( resultArea, expectedArea, 0.01 );
831}
832
833
834// A barcode inside a footprint takes the same conversion through the footprint path.
835BOOST_AUTO_TEST_CASE( FootprintBarcodeLoweredForKicad9 )
836{
837 BOARD board;
838 FOOTPRINT* fp = new FOOTPRINT( &board );
839 PCB_BARCODE* barcode = new PCB_BARCODE( fp );
840 barcode->SetLayer( F_SilkS );
841 barcode->SetWidth( pcbIUScale.mmToIU( 10 ) );
842 barcode->SetHeight( pcbIUScale.mmToIU( 10 ) );
843 barcode->SetBarcodeText( wxT( "TEST123" ) );
844 fp->Add( barcode );
845 board.Add( fp );
846
848
849 DowngradeBoardInPlace( &board, kicad9 );
850
851 int polyCount = 0;
852
853 for( BOARD_ITEM* item : fp->GraphicalItems() )
854 {
855 BOOST_REQUIRE( item->Type() != PCB_BARCODE_T );
856
857 if( item->Type() == PCB_SHAPE_T && static_cast<PCB_SHAPE*>( item )->GetShape() == SHAPE_T::POLY )
858 polyCount++;
859 }
860
861 BOOST_CHECK_GT( polyCount, 1 );
862}
863
864
865// Exercise the real .kicad_mod write path used to downgrade a footprint library, and confirm the
866// result carries the target stamp and reloads cleanly.
867BOOST_AUTO_TEST_CASE( FootprintFileDowngradeRoundTrips )
868{
869 const std::string src = KI_TEST::GetPcbnewTestDataDir() + "issue24045/QFN-24_L4.0-W4.0-P0.50-BL-EP2.6.kicad_mod";
870
871 PCB_IO_KICAD_SEXPR loader;
872 wxString nameOut;
873 std::unique_ptr<FOOTPRINT> fp( loader.ImportFootprint( src, nameOut ) );
874 BOOST_REQUIRE( fp != nullptr );
875
877
878 KI_TEST::TEMPORARY_DIRECTORY tmp( "kicad_qa_fp_downgrade", "" );
879 const wxString dest = ( tmp.GetPath() / "out.kicad_mod" ).wstring();
880
881 SaveFootprintForTarget( fp.get(), dest, kicad9 );
882
883 // The stamp proves the dispatch picked the right writer, not just a parseable one.
884 std::ifstream out( dest.ToStdString() );
885 std::string content( ( std::istreambuf_iterator<char>( out ) ), std::istreambuf_iterator<char>() );
886 BOOST_CHECK( content.find( "(version " + std::to_string( kicad9.m_boardVersion ) + ")" ) != std::string::npos );
887
888 PCB_IO_KICAD_SEXPR reader;
889 wxString reloadName;
890 std::unique_ptr<FOOTPRINT> reloaded( reader.ImportFootprint( dest, reloadName ) );
891 BOOST_CHECK( reloaded != nullptr );
892}
893
894
895// The v10 footprint file path gets the same round-trip check as v9.
896BOOST_AUTO_TEST_CASE( FootprintFileV10RoundTrips )
897{
898 SETTINGS_MANAGER settingsManager;
899
900 const std::string src = KI_TEST::GetPcbnewTestDataDir() + "issue24045/QFN-24_L4.0-W4.0-P0.50-BL-EP2.6.kicad_mod";
901
902 PCB_IO_KICAD_SEXPR loader;
903 wxString nameOut;
904 std::unique_ptr<FOOTPRINT> fp( loader.ImportFootprint( src, nameOut ) );
905 BOOST_REQUIRE( fp != nullptr );
906
908
909 KI_TEST::TEMPORARY_DIRECTORY tmp( "kicad_qa_fp_downgrade10", "" );
910 const wxString dest = ( tmp.GetPath() / "out.kicad_mod" ).wstring();
911
912 SaveFootprintForTarget( fp.get(), dest, kicad10 );
913
914 std::ifstream out( dest.ToStdString() );
915 std::string content( ( std::istreambuf_iterator<char>( out ) ), std::istreambuf_iterator<char>() );
916 BOOST_CHECK( content.find( "(version " + std::to_string( kicad10.m_boardVersion ) + ")" ) != std::string::npos );
917
918 PCB_IO_KICAD_SEXPR reader;
919 wxString reloadName;
920 std::unique_ptr<FOOTPRINT> reloaded( reader.ImportFootprint( dest, reloadName ) );
921 BOOST_CHECK( reloaded != nullptr );
922}
923
924
925// A library footprint carries the same features but is downgraded through its own path.
926BOOST_AUTO_TEST_CASE( FootprintPressFitDroppedForKicad9 )
927{
928 BOARD board;
929 FOOTPRINT* fp = new FOOTPRINT( &board );
930 PAD* pad = new PAD( fp );
931 pad->SetProperty( PAD_PROP::PRESSFIT );
932 fp->Add( pad );
933
935
936 BOOST_CHECK( pad->GetProperty() == PAD_PROP::NONE );
937
938 delete fp;
939}
940
941
942// Saving with a target version writes the older stamp, and reloading reads it back.
943BOOST_AUTO_TEST_CASE( SaveWithTargetVersionStampsOlder )
944{
945 SETTINGS_MANAGER settingsManager;
946
947 BOARD board;
948
949 KI_TEST::TEMPORARY_DIRECTORY tmp( "kicad_qa_downgrade", "" );
950 std::string dest = ( tmp.GetPath() / "out.kicad_pcb" ).string();
951
952 SaveBoardForTarget( &board, dest, kicad10 );
953
954 PCB_IO_KICAD_SEXPR reader;
955 std::unique_ptr<BOARD> reloaded( reader.LoadBoard( dest, nullptr ) );
956
957 BOOST_REQUIRE( reloaded != nullptr );
958 BOOST_CHECK_EQUAL( reloaded->GetFileFormatVersionAtLoad(), kicad10.m_boardVersion );
959}
960
961
962// The fields deque can hold null slots. The writer must skip them like the current one does.
963BOOST_AUTO_TEST_CASE( NullFieldSlotDoesNotCrashV9Writer )
964{
965 SETTINGS_MANAGER settingsManager;
966
967 std::unique_ptr<FOOTPRINT> fp = std::make_unique<FOOTPRINT>( nullptr );
968 fp->GetFields().push_back( nullptr );
969
970 KI_TEST::TEMPORARY_DIRECTORY tmp( "kicad_qa_v9_nullfield", "" );
971 const wxString dest = ( tmp.GetPath() / "out.kicad_mod" ).wstring();
972
973 SaveFootprintForTarget( fp.get(), dest, kicad9 );
974
975 BOOST_CHECK( std::filesystem::exists( dest.ToStdString() ) );
976}
977
978
979// The 10.0 release writer skips group members that are no longer on the board, so the v10
980// snapshot must too. The 9.0 release writer had no such check and its snapshot stays faithful.
981BOOST_AUTO_TEST_CASE( StaleGroupMemberNotWrittenForV10 )
982{
983 SETTINGS_MANAGER settingsManager;
984
985 BOARD board;
986 PCB_SHAPE* onBoard = new PCB_SHAPE( &board, SHAPE_T::SEGMENT );
987 board.Add( onBoard );
988
989 PCB_SHAPE* stale = new PCB_SHAPE( &board, SHAPE_T::SEGMENT );
990
991 PCB_GROUP* group = new PCB_GROUP( &board );
992 group->AddItem( onBoard );
993 group->AddItem( stale );
994 board.Add( group );
995
996 KI_TEST::TEMPORARY_DIRECTORY tmp( "kicad_qa_v10_stalegroup", "" );
997 const std::string dest = ( tmp.GetPath() / "out.kicad_pcb" ).string();
998
999 SaveBoardForTarget( &board, dest, kicad10 );
1000
1001 std::ifstream out( dest );
1002 std::string content( ( std::istreambuf_iterator<char>( out ) ), std::istreambuf_iterator<char>() );
1003
1004 BOOST_CHECK( content.find( onBoard->m_Uuid.AsString().ToStdString() ) != std::string::npos );
1005 BOOST_CHECK( content.find( stale->m_Uuid.AsString().ToStdString() ) == std::string::npos );
1006
1007 group->RemoveAll();
1008 delete stale;
1009}
1010
1011
1012// A via mode the era writer predates must be written as the nearest era mode, not silently
1013// fall through to keep-all.
1014BOOST_AUTO_TEST_CASE( SkipViaModeWrittenForV9 )
1015{
1016 SETTINGS_MANAGER settingsManager;
1017
1018 BOARD board;
1019 PCB_VIA* via = new PCB_VIA( &board );
1020 via->Padstack().SetUnconnectedLayerMode( UNCONNECTED_LAYER_MODE::START_END_ONLY );
1021 board.Add( via );
1022
1023 KI_TEST::TEMPORARY_DIRECTORY tmp( "kicad_qa_v9_skipvia", "" );
1024 const std::string dest = ( tmp.GetPath() / "out.kicad_pcb" ).string();
1025
1026 SaveBoardForTarget( &board, dest, kicad9 );
1027
1028 std::ifstream out( dest );
1029 std::string content( ( std::istreambuf_iterator<char>( out ) ), std::istreambuf_iterator<char>() );
1030
1031 BOOST_CHECK( content.find( "(remove_unused_layers yes)" ) != std::string::npos );
1032 BOOST_CHECK( content.find( "(keep_end_layers yes)" ) != std::string::npos );
1033
1034 PCB_IO_KICAD_SEXPR reader;
1035 std::unique_ptr<BOARD> reloaded( reader.LoadBoard( dest, nullptr ) );
1036 BOOST_REQUIRE( reloaded );
1037 BOOST_REQUIRE_EQUAL( reloaded->Tracks().size(), 1 );
1038 BOOST_REQUIRE( reloaded->Tracks().front()->Type() == PCB_VIA_T );
1039 const PCB_VIA* reloadedVia = static_cast<const PCB_VIA*>( reloaded->Tracks().front() );
1040 BOOST_CHECK( reloadedVia->Padstack().UnconnectedLayerMode()
1042}
1043
1044
1045// The target computes the image PPI the old truncating way, so the stored scale must be
1046// migrated back or the image renders at the wrong size there.
1047BOOST_AUTO_TEST_CASE( ImageScaleMigratedForV9 )
1048{
1049 SETTINGS_MANAGER settingsManager;
1050
1051 BOARD board;
1052 PCB_REFERENCE_IMAGE* otherImage = new PCB_REFERENCE_IMAGE( &board );
1053 wxMemoryBuffer buf = KI_TEST::MakePngWithFractionalPixelsPerCm();
1054 BOOST_REQUIRE( otherImage->GetReferenceImage().ReadImageFile( buf ) );
1055 otherImage->GetReferenceImage().SetImageScale( 0.5 );
1056 board.Add( otherImage );
1057
1059 REFERENCE_IMAGE& ref = image->GetReferenceImage();
1060
1061 BOOST_REQUIRE( ref.ReadImageFile( buf ) );
1062 ref.SetImageScale( 2.0 );
1063 board.Add( image );
1064
1065 int ppi = ref.GetImage().GetPPI();
1066 int legacyPPI = ref.GetImage().GetLegacyPPI();
1067 BOOST_REQUIRE( legacyPPI > 0 && ppi != legacyPPI );
1068
1069 double originalScale = ref.GetImageScale();
1070
1071 KI_TEST::TEMPORARY_DIRECTORY tmp( "kicad_qa_v9_imgscale", "" );
1072 const std::string dest = ( tmp.GetPath() / "out.kicad_pcb" ).string();
1073
1074 DowngradeBoardInPlace( &board, kicad9 );
1075 SaveBoardForTarget( &board, dest, kicad9 );
1076
1077 std::ifstream out( dest );
1078 std::string content( ( std::istreambuf_iterator<char>( out ) ), std::istreambuf_iterator<char>() );
1079
1080 const double written = KI_TEST::SerializedImageScale( content, image->m_Uuid.AsStdString() );
1081 BOOST_CHECK_CLOSE( written, originalScale * legacyPPI / ppi, 0.1 );
1082 BOOST_CHECK_CLOSE( KI_TEST::SerializedImageScale( content, otherImage->m_Uuid.AsStdString() ),
1083 0.5 * legacyPPI / ppi, 0.1 );
1084}
1085
1086
1087// The fail-closed gate: a post-10.0 token surviving in the output is caught, and lookalikes are not.
1088BOOST_AUTO_TEST_CASE( VerifyGateCatchesUnsupportedTokens )
1089{
1090 // An unstripped feature (e.g. an affine transform) must be caught.
1091 BOOST_CHECK( FindUnsupportedBoardToken( wxT( "(footprint (transform (translate 1 2)))" ), kicad10 )
1092 == wxT( "transform" ) );
1093 BOOST_CHECK( FindUnsupportedBoardToken( wxT( "(net_chain \"n\")" ), kicad10 ) == wxT( "net_chain" ) );
1094
1095 // A clean board, and lookalike prefixes, must pass.
1096 BOOST_CHECK( FindUnsupportedBoardToken( wxT( "(kicad_pcb (net 0 \"n\") (gr_line ...))" ), kicad10 ).IsEmpty() );
1097
1098 // Value tokens the node scan cannot see must still be caught for 9.0, but not inside strings.
1099
1100 BOOST_CHECK( FindUnsupportedBoardToken( wxT( "(via buried (at 1 1))" ), kicad9 ) == wxT( "buried" ) );
1101 BOOST_CHECK( FindUnsupportedBoardToken( wxT( "(gr_text \"buried treasure\")" ), kicad9 ).IsEmpty() );
1102}
1103
1104
1105// One unknown keyword makes an old KiCad drop the whole rules file, so the export must
1106// filter out rules the target cannot parse and keep the rest byte-identical.
1107BOOST_AUTO_TEST_CASE( DrcRulesFilterForKicad9 )
1108{
1109 const wxString rules = wxT( "(version 2)\n"
1110 "# keep me\n"
1111 "(rule \"clearance_ok\"\n"
1112 " # chamfer runs at 45deg here\n"
1113 " (constraint clearance (min 0.2mm))\n"
1114 " (condition \"A.NetClass == 'HV'\"))\n"
1115 "(rule \"mask_check\"\n"
1116 " (constraint bridged_mask))\n"
1117 "(rule \"chain_check\"\n"
1118 " (condition \"inNetChain('x')\")\n"
1119 " (constraint clearance (min 0.1mm)))\n"
1120 "(rule \"delay_check\"\n"
1121 " (constraint skew (max 5ps)))\n" );
1122
1124
1125 BOOST_REQUIRE_EQUAL( result.m_dropped.size(), 3 );
1126 BOOST_CHECK( result.m_dropped[0] == wxT( "mask_check" ) );
1127 BOOST_CHECK( result.m_dropped[1] == wxT( "chain_check" ) );
1128 BOOST_CHECK( result.m_dropped[2] == wxT( "delay_check" ) );
1129
1130 BOOST_CHECK( result.m_text.Contains( wxT( "(version 1)" ) ) );
1131 BOOST_CHECK( result.m_text.Contains( wxT( "# keep me" ) ) );
1132 BOOST_CHECK( result.m_text.Contains( wxT( "(rule \"clearance_ok\"" ) ) );
1133 BOOST_CHECK( result.m_text.Contains( wxT( "A.NetClass == 'HV'" ) ) );
1134 BOOST_CHECK( !result.m_text.Contains( wxT( "bridged_mask" ) ) );
1135 BOOST_CHECK( !result.m_text.Contains( wxT( "inNetChain" ) ) );
1136 BOOST_CHECK( !result.m_text.Contains( wxT( "5ps" ) ) );
1137}
1138
1139
1140BOOST_AUTO_TEST_CASE( DrcRuleNamesAndExpressionLiteralsDoNotTriggerDrops )
1141{
1142 const wxString rules = wxT( "(version 1)\n"
1143 "(rule \"return_path\" (constraint clearance (min 0.2mm)))\n"
1144 "(rule inNetChain (constraint clearance (min 0.2mm)))\n"
1145 "(rule \"literal\" (condition \"A.NetName == 'hasNetChain() 100 ps .Parent.'\")\n"
1146 " (constraint clearance (min 0.2mm)))\n" );
1147
1148 for( const DOWNGRADE_TARGET* target : { &kicad9, &kicad10 } )
1149 {
1151 BOOST_CHECK( result.m_error.IsEmpty() );
1152 BOOST_CHECK( result.m_dropped.empty() );
1153 BOOST_CHECK_EQUAL( result.m_text.Mid( result.m_text.Find( "(rule" ) ), rules.Mid( rules.Find( "(rule" ) ) );
1154 }
1155}
1156
1157
1158BOOST_AUTO_TEST_CASE( SpacedTimeAndAngleUnitsAreDroppedForKicad9 )
1159{
1160 const wxString rules = wxT( "(version 1)\n"
1161 "(rule \"time\" (constraint length (min \"100 ps\")))\n"
1162 "(rule \"femtoseconds\" (constraint length (max \"2 * 100 fs\")))\n"
1163 "(rule \"angle\" (constraint track_angle (max \"90 deg\")))\n"
1164 "(rule \"ordinary\" (constraint clearance (min \"0.2 mm\")))\n" );
1165
1167 BOOST_CHECK( result9.m_error.IsEmpty() );
1168 BOOST_CHECK_EQUAL( result9.m_dropped.size(), 3 );
1169 BOOST_CHECK( result9.m_text.Contains( "ordinary" ) );
1170
1172 BOOST_CHECK( result10.m_error.IsEmpty() );
1173 BOOST_CHECK( result10.m_dropped.empty() );
1174 BOOST_CHECK_EQUAL( result10.m_text.Mid( result10.m_text.Find( "(rule" ) ), rules.Mid( rules.Find( "(rule" ) ) );
1175}
1176
1177
1178// 10.0 knows the mask constraints and time units. Only post-10.0 syntax drops for it.
1179BOOST_AUTO_TEST_CASE( DrcRulesFilterForKicad10 )
1180{
1181 const wxString rules = wxT( "(version 2)\n"
1182 "(rule \"mask_check\" (constraint bridged_mask))\n"
1183 "(rule \"delay_check\" (constraint skew (max 5ps)))\n"
1184 "(rule \"chain_check\" (constraint net_chain_length (max 10mm)))\n" );
1185
1187
1188 BOOST_REQUIRE_EQUAL( result.m_dropped.size(), 1 );
1189 BOOST_CHECK( result.m_dropped[0] == wxT( "chain_check" ) );
1190 BOOST_CHECK( result.m_text.Contains( wxT( "bridged_mask" ) ) );
1191 BOOST_CHECK( result.m_text.Contains( wxT( "5ps" ) ) );
1192}
1193
1194
1195// Comment syntax must never participate in balancing a rule. Parentheses or quotes
1196// in a full-line comment used to truncate or swallow otherwise valid rules.
1197BOOST_AUTO_TEST_CASE( DrcRulesWithCommentsRemainIntact )
1198{
1199 const wxString kept = wxT( "(rule \"clearance # literal\"\n"
1200 " # unmatched: ) (( \" \\\n"
1201 " (condition \"A.NetName == 'SIGNAL#1'\")\n"
1202 " # inNetChain('x')\n"
1203 " (constraint clearance (min 0.2mm))\n"
1204 " # ) \" (\n"
1205 ")\n"
1206 "(rule \"second\" (constraint track_width (min 0.25mm)))" );
1207 const wxString input = wxT( "(version 2)\n" ) + kept
1208 + wxT( "\n(rule \"unsupported\"\n # ) \" (\n"
1209 " (constraint net_chain_length (max 10mm)))\n" );
1210
1211 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
1212 {
1214 BOOST_REQUIRE( result.m_error.IsEmpty() );
1215 BOOST_REQUIRE_EQUAL( result.m_dropped.size(), 1 );
1216 BOOST_CHECK( result.m_dropped.front() == wxT( "unsupported" ) );
1217 BOOST_CHECK( result.m_text.Contains( kept ) );
1218
1219 DRC_RULES_PARSER parser( result.m_text, wxT( "downgrade_inline_comments" ) );
1221 std::vector<std::shared_ptr<DRC_RULE>> rules;
1222 parser.Parse( rules, &reporter );
1223 BOOST_CHECK_MESSAGE( !reporter.HasMessageOfSeverity( RPT_SEVERITY_ERROR ), reporter.GetMessages() );
1224 BOOST_REQUIRE_EQUAL( rules.size(), 2 );
1225 BOOST_CHECK( rules[0]->FindConstraint( CLEARANCE_CONSTRAINT ).has_value() );
1226 BOOST_CHECK( rules[1]->FindConstraint( TRACK_WIDTH_CONSTRAINT ).has_value() );
1227 }
1228}
1229
1230
1231BOOST_AUTO_TEST_CASE( InvalidDrcCommentAndUnbalancedSyntaxFailClosed )
1232{
1233 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
1234 {
1235 for( const wxString& input : { wxT( "(version 1)\n(rule \"inline\" # )\n (constraint clearance (min 0.2mm)))" ),
1236 wxT( "(version 1)\n(rule \"missing_close\" (constraint clearance (min 0.2mm))" ),
1237 wxT( "(version 1)\n(rule \"missing_quote" ) } )
1238 {
1239 BOOST_CHECK( !FilterDrcRulesForTarget( input, target ).m_error.IsEmpty() );
1240 }
1241 }
1242}
1243
1244
1245BOOST_AUTO_TEST_CASE( DrcRulesMatchIndependentOlderReferences )
1246{
1247 const std::string fixture = KI_TEST::GetPcbnewTestDataDir() + "../downgrade/golden/";
1248 const wxString input = wxString::FromUTF8( KI_TEST::ReadGoldenText( fixture + "current/rules.kicad_dru" ) );
1249
1250 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
1251 {
1253 BOOST_REQUIRE( result.m_error.IsEmpty() );
1254 const std::string release = target.m_id == wxT( "9.0" ) ? "v9" : "v10";
1255 BOOST_CHECK( result.m_text.ToStdString() == KI_TEST::ReadGoldenText( fixture + release + "/rules.kicad_dru" ) );
1256 BOOST_CHECK_EQUAL( result.m_dropped.size(), release == "v9" ? 2 : 1 );
1257 DRC_RULES_PARSER parser( result.m_text, wxT( "downgrade_drc_golden" ) );
1259 std::vector<std::shared_ptr<DRC_RULE>> rules;
1260 parser.Parse( rules, &reporter );
1261 BOOST_CHECK_MESSAGE( !reporter.HasMessageOfSeverity( RPT_SEVERITY_ERROR ), reporter.GetMessages() );
1262 BOOST_CHECK_EQUAL( rules.size(), release == "v9" ? 1 : 2 );
1263 }
1264}
1265
1266
1267// Flattening bakes the chosen variant's overrides into the base attributes, so the exported
1268// board matches that variant after the registry is dropped.
1269BOOST_AUTO_TEST_CASE( VariantFlattenedIntoBoard )
1270{
1271 BOARD board;
1272 FOOTPRINT* fp = new FOOTPRINT( &board );
1273 fp->SetDNP( false );
1274
1275 FOOTPRINT_VARIANT* variant = fp->AddVariant( wxT( "V1" ) );
1276 variant->SetDNP( true );
1277 variant->SetFieldValue( wxT( "Value" ), wxT( "10k" ) );
1278 board.Add( fp );
1279
1280 FlattenBoardVariant( &board, wxT( "V1" ) );
1281
1282 BOOST_CHECK( fp->IsDNP() );
1283 BOOST_REQUIRE( fp->GetField( wxT( "Value" ) ) != nullptr );
1284 BOOST_CHECK( fp->GetField( wxT( "Value" ) )->GetText() == wxT( "10k" ) );
1285}
1286
1287
1288// The project file is migrated down: newer settings are removed, nested schema versions are
1289// rewound, and meaningful losses are reported.
1290BOOST_AUTO_TEST_CASE( ProjectFileDowngradeForKicad9 )
1291{
1292 nlohmann::json doc = {
1293 { "meta", { { "filename", "p.kicad_pro" }, { "version", 3 } } },
1294 { "board", { { "layer_presets", { { { "name", "P" }, { "renderLayers", { "tracks", "vias" } } } } } } },
1295 { "net_settings",
1296 { { "meta", { { "version", 5 } } },
1297 { "classes", { { { "name", "Default" }, { "priority", 0 }, { "tuning_profile", "tp" } } } },
1298 { "net_chain_classes", { { { "name", "chainA" } } } } } },
1299 { "tuning_profiles",
1300 { { "meta", { { "version", 1 } } },
1301 { "tuning_profiles_impedance_geometric",
1302 { { { "name", "tp" }, { "frequency", 1e9 }, { "model_solder_mask", false } } } } } },
1303 { "component_class_settings", { { "meta", { { "version", 0 } } } } },
1304 { "schematic",
1305 { { "top_level_sheets", { "aaaa" } },
1306 { "bus_aliases", nlohmann::json::array() },
1307 { "variants", nlohmann::json::array() } } },
1308 };
1309
1310 COMPATIBILITY_REPORT report;
1311 DowngradeProjectFileJson( doc, kicad9, report );
1312
1313 // 9.0 shipped project schema 3 and reads render layers by name, skipping names it does
1314 // not know. So the version and the preset strings must pass through untouched.
1315 BOOST_CHECK_EQUAL( doc["meta"]["version"].get<int>(), 3 );
1316
1317 const nlohmann::json& preset = doc["board"]["layer_presets"][0];
1318 BOOST_REQUIRE( preset["renderLayers"].is_array() );
1319 BOOST_REQUIRE_EQUAL( preset["renderLayers"].size(), 2 );
1320 BOOST_CHECK_EQUAL( preset["renderLayers"][0].get<std::string>(), "tracks" );
1321 BOOST_CHECK_EQUAL( preset["renderLayers"][1].get<std::string>(), "vias" );
1322
1323 BOOST_CHECK_EQUAL( doc["net_settings"]["meta"]["version"].get<int>(), 4 );
1324 BOOST_CHECK( !doc["net_settings"]["classes"][0].contains( "tuning_profile" ) );
1325 BOOST_CHECK( doc["net_settings"]["classes"][0].contains( "name" ) );
1326 BOOST_CHECK( !doc["net_settings"].contains( "net_chain_classes" ) );
1327 BOOST_CHECK( !doc.contains( "tuning_profiles" ) );
1328 BOOST_CHECK( !doc.contains( "component_class_settings" ) );
1329 BOOST_CHECK( !doc["schematic"].contains( "top_level_sheets" ) );
1330 BOOST_CHECK( !doc["schematic"].contains( "bus_aliases" ) );
1331 BOOST_CHECK( !doc["schematic"].contains( "variants" ) );
1332
1333 // The meaningful losses show in the report. Hygiene removals stay silent.
1334 BOOST_CHECK( report.IsLossy() );
1336}
1337
1338
1339BOOST_AUTO_TEST_CASE( EmptyNativeProjectSectionsDoNotRequireConsent )
1340{
1341 const nlohmann::json empty = nlohmann::json::array();
1342 const std::vector<nlohmann::json> tuningDefaults = {
1343 nullptr,
1344 nlohmann::json::object(),
1345 { { "meta", { { "version", 2 } } } },
1346 { { "meta", { { "version", 2 } } }, { "tuning_profiles_impedance_geometric", empty } },
1347 };
1348 const std::vector<nlohmann::json> componentDefaults = {
1349 nullptr,
1350 nlohmann::json::object(),
1351 { { "meta", { { "version", 0 } } } },
1352 { { "assignments", empty }, { "sheet_component_classes", nlohmann::json::object() } },
1353 { { "meta", { { "version", 0 } } },
1354 { "assignments", empty },
1355 { "sheet_component_classes", { { "enabled", false } } } },
1356 };
1357
1358 for( const auto& target : { kicad9, kicad10 } )
1359 {
1360 for( const auto& tuning : tuningDefaults )
1361 {
1362 for( const auto& components : componentDefaults )
1363 {
1364 nlohmann::json doc = {
1365 { "meta", { { "version", 4 } } },
1366 { "board", { { "design_settings", { { "meta", { { "version", 3 } } } } } } },
1367 { "erc", { { "meta", { { "version", 1 } } }, { "erc_exclusions", empty } } },
1368 { "net_settings",
1369 { { "meta", { { "version", 5 } } },
1370 { "classes", { { { "name", "Default" }, { "tuning_profile", "" } } } } } },
1371 { "tuning_profiles", tuning },
1372 { "component_class_settings", components },
1373 };
1374
1375 COMPATIBILITY_REPORT report;
1376 DowngradeProjectFileJson( doc, target, report );
1377
1378 BOOST_CHECK( !report.IsBlocked() );
1379 BOOST_CHECK( !report.IsLossy() );
1381 BOOST_CHECK_EQUAL( doc["meta"]["version"].get<int>(), 3 );
1382 BOOST_CHECK_EQUAL( doc["board"]["design_settings"]["meta"]["version"].get<int>(), 2 );
1383 BOOST_CHECK_EQUAL( doc["erc"]["meta"]["version"].get<int>(), 0 );
1384 BOOST_CHECK_EQUAL( doc.contains( "tuning_profiles" ), target.m_id == wxT( "10.0" ) );
1385 BOOST_CHECK_EQUAL( doc.contains( "component_class_settings" ), target.m_id == wxT( "10.0" ) );
1386
1387 const nlohmann::json once = doc;
1388 COMPATIBILITY_REPORT repeated;
1389 DowngradeProjectFileJson( doc, target, repeated );
1390 BOOST_CHECK( doc == once );
1391 BOOST_CHECK( !repeated.IsLossy() );
1392 }
1393 }
1394 }
1395}
1396
1397
1398BOOST_AUTO_TEST_CASE( SavedProjectTuningAndClassChoicesStillRequireConsent )
1399{
1400 const nlohmann::json empty = nlohmann::json::array();
1401 const nlohmann::json profile = {
1402 { "profile_name", "Default" },
1403 { "frequency", 1e9 },
1404 { "model_solder_mask", false },
1405 { "net_chain_bridge_prop_delay", 0 },
1406 };
1407 const std::vector<nlohmann::json> meaningful = {
1408 { { "tuning_profiles", { { "tuning_profiles_impedance_geometric", { profile } } } } },
1409 { { "net_settings", { { "classes", { { { "tuning_profile", "Saved" } } } } } } },
1410 { { "tuning_profiles", { { "tuning_profiles_impedance_geometric", empty } } },
1411 { "net_settings", { { "classes", { { { "tuning_profile", "Saved" } } } } } } },
1412 { { "tuning_profiles", { { "tuning_profiles_impedance_geometric", { profile } } } },
1413 { "net_settings",
1414 { { "classes", { { { "tuning_profile", "Default" } }, { { "tuning_profile", "Default" } } } } } } },
1415 { { "component_class_settings",
1416 { { "assignments",
1417 { { { "component_class", "Fast" }, { "conditions_operator", "ALL" }, { "conditions", empty } } } },
1418 { "sheet_component_classes", { { "enabled", false } } } } } },
1419 { { "component_class_settings",
1420 { { "assignments", empty }, { "sheet_component_classes", { { "enabled", true } } } } } },
1421 };
1422
1423 for( const auto& source : meaningful )
1424 {
1425 for( const auto& target : { kicad9, kicad10 } )
1426 {
1427 nlohmann::json doc = source;
1428 COMPATIBILITY_REPORT report;
1429 DowngradeProjectFileJson( doc, target, report );
1430 BOOST_CHECK( !report.IsBlocked() );
1431 BOOST_CHECK_EQUAL( report.Count( DOWNGRADE_BUCKET::DROP ), target.m_id == wxT( "9.0" ) ? 1 : 0 );
1432 BOOST_CHECK_EQUAL( report.IsLossy(), target.m_id == wxT( "9.0" ) );
1433
1434 const nlohmann::json once = doc;
1435 COMPATIBILITY_REPORT repeated;
1436 DowngradeProjectFileJson( doc, target, repeated );
1437 BOOST_CHECK( doc == once );
1438 BOOST_CHECK( !repeated.IsLossy() );
1439 }
1440 }
1441}
1442
1443
1444BOOST_AUTO_TEST_CASE( FutureComponentClassSchemaBlocksWithoutMutation )
1445{
1446 const nlohmann::json source = {
1447 { "meta", { { "version", 4 } } },
1448 { "component_class_settings", { { "meta", { { "version", 1 } } } } },
1449 };
1450
1451 for( const auto& target : { kicad9, kicad10 } )
1452 {
1453 nlohmann::json doc = source;
1454 COMPATIBILITY_REPORT report;
1455 DowngradeProjectFileJson( doc, target, report );
1456 BOOST_CHECK( report.IsBlocked() );
1457 BOOST_CHECK( doc == source );
1458 }
1459}
1460
1461
1462// A structurally odd but valid-JSON project file must not throw. Classification swallows
1463// exceptions, so a throw here would surface only mid-export and break fail-closed.
1464BOOST_AUTO_TEST_CASE( WrongShapeProjectFileDoesNotThrow )
1465{
1466 nlohmann::json metaIsString = {
1467 { "meta", "not an object" },
1468 { "net_settings", { { "meta", "also a string" }, { "classes", "not an array" } } },
1469 { "schematic", "a string" },
1470 };
1471
1472 nlohmann::json profilesWrong = {
1473 { "tuning_profiles",
1474 { { "meta", 7 }, { "tuning_profiles_impedance_geometric", { "just", "strings" } } } },
1475 };
1476
1477 COMPATIBILITY_REPORT report;
1478 BOOST_CHECK_NO_THROW( DowngradeProjectFileJson( metaIsString, kicad9, report ) );
1479 BOOST_CHECK_NO_THROW( DowngradeProjectFileJson( profilesWrong, kicad10, report ) );
1480}
1481
1482
1483// An empty net chain class array is removed as hygiene, not reported as a loss.
1484BOOST_AUTO_TEST_CASE( EmptyNetChainClassesSilent )
1485{
1486 nlohmann::json doc = {
1487 { "net_settings", { { "net_chain_classes", nlohmann::json::array() } } },
1488 };
1489
1490 COMPATIBILITY_REPORT report;
1491 DowngradeProjectFileJson( doc, kicad10, report );
1492
1493 BOOST_CHECK( !doc["net_settings"].contains( "net_chain_classes" ) );
1495}
1496
1497
1498BOOST_AUTO_TEST_CASE( NonDefaultTuningModelSettingsAreReportedForKicad10 )
1499{
1500 nlohmann::json doc = {
1501 { "tuning_profiles",
1502 { { "meta", { { "version", 1 } } },
1503 { "tuning_profiles_impedance_geometric",
1504 { { { "profile_name", "5 GHz" }, { "frequency", 5e9 }, { "model_solder_mask", true } },
1505 { { "profile_name", "Default" }, { "frequency", 1e9 }, { "model_solder_mask", false } } } } } }
1506 };
1507
1508 COMPATIBILITY_REPORT report;
1509 DowngradeProjectFileJson( doc, kicad10, report );
1510 BOOST_CHECK( report.IsLossy() );
1512
1513 for( const auto& profile : doc["tuning_profiles"]["tuning_profiles_impedance_geometric"] )
1514 {
1515 BOOST_CHECK( !profile.contains( "frequency" ) );
1516 BOOST_CHECK( !profile.contains( "model_solder_mask" ) );
1517 }
1518
1519 COMPATIBILITY_REPORT secondReport;
1520 DowngradeProjectFileJson( doc, kicad10, secondReport );
1521 BOOST_CHECK( !secondReport.IsLossy() );
1522}
1523
1524
1525BOOST_AUTO_TEST_CASE( ProjectFileDowngradeForKicad10 )
1526{
1527 nlohmann::json doc = {
1528 { "meta", { { "version", 3 } } },
1529 { "net_settings",
1530 { { "meta", { { "version", 5 } } },
1531 { "classes", { { { "name", "Default" }, { "priority", 0 }, { "tuning_profile", "tp" } } } },
1532 { "net_chain_classes", { { { "name", "chainA" } } } } } },
1533 { "tuning_profiles",
1534 { { "meta", { { "version", 1 } } },
1535 { "tuning_profiles_impedance_geometric",
1536 { { { "name", "tp" }, { "frequency", 1e9 }, { "model_solder_mask", false } } } } } },
1537 };
1538
1539 COMPATIBILITY_REPORT report;
1540 DowngradeProjectFileJson( doc, kicad10, report );
1541
1542 // 10.0 knows tuning profiles and reads the per-class reference unguarded, so both stay.
1543 BOOST_CHECK_EQUAL( doc["meta"]["version"].get<int>(), 3 );
1544 BOOST_CHECK_EQUAL( doc["net_settings"]["meta"]["version"].get<int>(), 5 );
1545 BOOST_CHECK( doc["net_settings"]["classes"][0].contains( "tuning_profile" ) );
1546 BOOST_CHECK( !doc["net_settings"].contains( "net_chain_classes" ) );
1547
1548 BOOST_CHECK_EQUAL( doc["tuning_profiles"]["meta"]["version"].get<int>(), 0 );
1549 BOOST_CHECK( !doc["tuning_profiles"]["tuning_profiles_impedance_geometric"][0].contains( "frequency" ) );
1550 BOOST_CHECK( !doc["tuning_profiles"]["tuning_profiles_impedance_geometric"][0].contains( "model_solder_mask" ) );
1551
1553}
1554
1555
1556// A near-closed thick elliptical arc strokes to a ring. The writers save one outline per
1557// shape, so the lowered polygon must carry no holes.
1558BOOST_AUTO_TEST_CASE( EllipseArcRingHasNoHoles )
1559{
1560 BOARD board;
1561 PCB_SHAPE* arc = new PCB_SHAPE( &board, SHAPE_T::ELLIPSE_ARC );
1562 arc->SetEllipseCenter( VECTOR2I( 0, 0 ) );
1563 arc->SetEllipseMajorRadius( pcbIUScale.mmToIU( 5 ) );
1564 arc->SetEllipseMinorRadius( pcbIUScale.mmToIU( 3 ) );
1567 arc->SetWidth( pcbIUScale.mmToIU( 0.3 ) );
1568 board.Add( arc );
1569
1570 DowngradeBoardInPlace( &board, kicad10 );
1571
1573
1574 SHAPE_POLY_SET& poly = arc->GetPolyShape();
1575
1576 for( int ii = 0; ii < poly.OutlineCount(); ++ii )
1577 BOOST_CHECK_EQUAL( poly.HoleCount( ii ), 0 );
1578
1579 // The ring's ink, nowhere near the filled disk's 47 square mm.
1580 double areaMM = std::abs( poly.Area() ) / ( pcbIUScale.IU_PER_MM * pcbIUScale.IU_PER_MM );
1581
1582 BOOST_CHECK_GT( areaMM, 4.0 );
1583 BOOST_CHECK_LT( areaMM, 15.0 );
1584}
1585
1586
1587// The scale compensation must reach images inside footprints on the board.
1588BOOST_AUTO_TEST_CASE( FootprintImageScaleCompensatedOnBoard )
1589{
1590 BOARD board;
1591 FOOTPRINT* fp = new FOOTPRINT( &board );
1593 REFERENCE_IMAGE& ref = image->GetReferenceImage();
1594
1595 wxMemoryBuffer buf = KI_TEST::MakePngWithFractionalPixelsPerCm();
1596 BOOST_REQUIRE( ref.ReadImageFile( buf ) );
1597 ref.SetImageScale( 2.0 );
1598 fp->Add( image );
1599 board.Add( fp );
1600
1601 int ppi = ref.GetImage().GetPPI();
1602 int legacyPPI = ref.GetImage().GetLegacyPPI();
1603 BOOST_REQUIRE( legacyPPI > 0 && ppi != legacyPPI );
1604
1605 DowngradeBoardInPlace( &board, kicad9 );
1606
1607 BOOST_CHECK_CLOSE( ref.GetImageScale(), 2.0 * legacyPPI / ppi, 0.1 );
1608}
1609
1610
1611// A library footprint goes through the footprint-scoped path and gets the same compensation.
1612BOOST_AUTO_TEST_CASE( FootprintImageScaleCompensatedInLibrary )
1613{
1614 std::unique_ptr<FOOTPRINT> fp = std::make_unique<FOOTPRINT>( nullptr );
1616 REFERENCE_IMAGE& ref = image->GetReferenceImage();
1617
1618 wxMemoryBuffer buf = KI_TEST::MakePngWithFractionalPixelsPerCm();
1619 BOOST_REQUIRE( ref.ReadImageFile( buf ) );
1620 ref.SetImageScale( 2.0 );
1621 fp->Add( image );
1622
1623 int ppi = ref.GetImage().GetPPI();
1624 int legacyPPI = ref.GetImage().GetLegacyPPI();
1625 BOOST_REQUIRE( legacyPPI > 0 && ppi != legacyPPI );
1626
1627 DowngradeFootprintInPlace( fp.get(), kicad9 );
1628
1629 BOOST_CHECK_CLOSE( ref.GetImageScale(), 2.0 * legacyPPI / ppi, 0.1 );
1630}
1631
1632
1633// The duplicate-pad-numbers jumper flag is jumper data too, so 9.0 must drop and report it.
1634BOOST_AUTO_TEST_CASE( DuplicatePadJumperFlagDroppedForKicad9 )
1635{
1636 BOARD board;
1637 FOOTPRINT* fp = new FOOTPRINT( &board );
1639 board.Add( fp );
1640
1642
1643 DowngradeBoardInPlace( &board, kicad9 );
1644
1645 BOOST_CHECK( !fp->GetDuplicatePadNumbersAreJumpers() );
1646}
1647
1648
1649// Downgrading a library file must not clip zone layers to a scratch board's layer set. The
1650// source file is its own oracle: the layer set must survive the round trip unchanged.
1651BOOST_AUTO_TEST_CASE( FootprintFileKeepsZoneLayers )
1652{
1653 SETTINGS_MANAGER settingsManager;
1654
1655 KI_TEST::TEMPORARY_DIRECTORY tmp( "kicad_qa_fp_zone_layers", "" );
1656 const wxString src = ( tmp.GetPath() / "src.kicad_mod" ).wstring();
1657 const wxString dest = ( tmp.GetPath() / "out.kicad_mod" ).wstring();
1658
1659 {
1660 std::unique_ptr<FOOTPRINT> fp = std::make_unique<FOOTPRINT>( nullptr );
1661 ZONE* zone = new ZONE( fp.get() );
1662
1663 zone->SetLayerSet( LSET::AllCuMask() );
1664 zone->Outline()->NewOutline();
1665 zone->Outline()->Append( 0, 0 );
1666 zone->Outline()->Append( pcbIUScale.mmToIU( 1 ), 0 );
1667 zone->Outline()->Append( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) );
1668 zone->Outline()->Append( 0, pcbIUScale.mmToIU( 1 ) );
1669 fp->Add( zone );
1670
1671 SaveFootprintForTarget( fp.get(), src, kicad10 );
1672 }
1673
1675
1676 PCB_IO_KICAD_SEXPR reader;
1677 wxString nameOut;
1678 std::unique_ptr<FOOTPRINT> srcAgain( reader.ImportFootprint( src, nameOut ) );
1679 std::unique_ptr<FOOTPRINT> converted( reader.ImportFootprint( dest, nameOut ) );
1680
1681 BOOST_REQUIRE( srcAgain && converted );
1682 BOOST_REQUIRE_EQUAL( srcAgain->Zones().size(), 1 );
1683 BOOST_REQUIRE_EQUAL( converted->Zones().size(), 1 );
1684
1685 BOOST_CHECK_EQUAL( converted->Zones()[0]->GetLayerSet().count(), srcAgain->Zones()[0]->GetLayerSet().count() );
1686}
1687
1688
1689// The staged library downgrade converts and verifies every file before any original is
1690// replaced, and a refusal leaves the folder untouched with no temp files behind.
1691BOOST_AUTO_TEST_CASE( LibraryFilesDowngradeFailsClosed )
1692{
1693 KI_TEST::TEMPORARY_DIRECTORY tmp( "kicad_qa_lib_staged", "" );
1694 const wxString dir = tmp.GetPath().wstring();
1695
1696 auto writeFile = [&]( const wxString& aName, const wxString& aContent )
1697 {
1698 wxFFile file( dir + wxFileName::GetPathSeparator() + aName, wxT( "wb" ) );
1699 BOOST_REQUIRE( file.IsOpened() );
1700 file.Write( aContent );
1701 };
1702
1703 writeFile( wxT( "a.fake_mod" ), wxT( "old a" ) );
1704 writeFile( wxT( "b.fake_mod" ), wxT( "old b" ) );
1705
1706 auto noForbidden = []( const wxString& )
1707 {
1708 return wxString();
1709 };
1710
1711 auto convertOk = [&]( const wxString& aFile, const wxString& aTmp ) -> DOWNGRADE_FILE_RESULT
1712 {
1713 wxFFile out( aTmp, wxT( "wb" ) );
1714 out.Write( wxT( "(version 123) converted" ) );
1716 };
1717
1718 BOOST_CHECK( DowngradeLibraryFilesInPlace( dir, wxT( "*.fake_mod" ), 123, noForbidden, convertOk ).IsEmpty() );
1719
1720 wxFFile check( dir + wxFileName::GetPathSeparator() + wxT( "a.fake_mod" ), wxT( "rb" ) );
1721 wxString content;
1722 BOOST_REQUIRE( check.IsOpened() && check.ReadAll( &content ) );
1723 BOOST_CHECK( content.Contains( wxT( "converted" ) ) );
1724
1725 // Second pass: the second file refuses, so the first must stay as it is now.
1726 auto convertRefuseB = [&]( const wxString& aFile, const wxString& aTmp ) -> DOWNGRADE_FILE_RESULT
1727 {
1728 if( aFile.Contains( wxT( "b.fake_mod" ) ) )
1730
1731 wxFFile out( aTmp, wxT( "wb" ) );
1732 out.Write( wxT( "(version 123) second pass" ) );
1734 };
1735
1736 wxString bad = DowngradeLibraryFilesInPlace( dir, wxT( "*.fake_mod" ), 123, noForbidden, convertRefuseB );
1737 BOOST_CHECK( bad.Contains( wxT( "b.fake_mod" ) ) );
1738
1739 wxArrayString leftovers;
1740 wxDir::GetAllFiles( dir, &leftovers );
1741 BOOST_CHECK_EQUAL( leftovers.size(), 2 );
1742
1743 wxFFile recheck( dir + wxFileName::GetPathSeparator() + wxT( "a.fake_mod" ), wxT( "rb" ) );
1744 wxString unchanged;
1745 BOOST_REQUIRE( recheck.IsOpened() && recheck.ReadAll( &unchanged ) );
1746 BOOST_CHECK( !unchanged.Contains( wxT( "second pass" ) ) );
1747}
1748
1749
1750// The project export carries design files only. UI state and non-KiCad files stay behind.
1751BOOST_AUTO_TEST_CASE( DesignFileFilter )
1752{
1753 BOOST_CHECK( IsDowngradeDesignFile( wxT( "proj/proj.kicad_pro" ) ) );
1754 BOOST_CHECK( IsDowngradeDesignFile( wxT( "proj/sub/sheet.kicad_sch" ) ) );
1755 BOOST_CHECK( IsDowngradeDesignFile( wxT( "proj/proj.kicad_pcb" ) ) );
1756 BOOST_CHECK( IsDowngradeDesignFile( wxT( "proj/proj.kicad_dru" ) ) );
1757 BOOST_CHECK( IsDowngradeDesignFile( wxT( "proj/frame.kicad_wks" ) ) );
1758 BOOST_CHECK( IsDowngradeDesignFile( wxT( "proj/lib.kicad_sym" ) ) );
1759 BOOST_CHECK( IsDowngradeDesignFile( wxT( "proj/lib.pretty/fp.kicad_mod" ) ) );
1760 BOOST_CHECK( IsDowngradeDesignFile( wxT( "proj/sym-lib-table" ) ) );
1761 BOOST_CHECK( IsDowngradeDesignFile( wxT( "proj/fp-lib-table" ) ) );
1762
1763 BOOST_CHECK( !IsDowngradeDesignFile( wxT( "proj/proj.kicad_prl" ) ) );
1764 BOOST_CHECK( !IsDowngradeDesignFile( wxT( "proj/datasheet.pdf" ) ) );
1765 BOOST_CHECK( !IsDowngradeDesignFile( wxT( "proj/out/proj-F_Cu.gbr" ) ) );
1766 BOOST_CHECK( !IsDowngradeDesignFile( wxT( "proj/proj.kicad_jobset" ) ) );
1767 BOOST_CHECK( !IsDowngradeDesignFile( wxT( "proj/notes.txt" ) ) );
1768}
1769
1770
1771// When this fails a format bump happened. Review the new keywords, update the denylist, then bump
1772// BoardDowngradeCoveredVersion.
1777
1778
1779// Every graphical LOWER rule has an omission path, both on boards and in footprints.
1780BOOST_AUTO_TEST_CASE( DropApproximationsOmitsGraphicsWithoutReplacements )
1781{
1782 for( bool footprintOnly : { false, true } )
1783 {
1784 BOARD board;
1785 FOOTPRINT* fp = new FOOTPRINT( &board );
1786 board.Add( fp );
1787 BOARD_ITEM_CONTAINER* parent = footprintOnly ? static_cast<BOARD_ITEM_CONTAINER*>( fp ) : &board;
1788 PCB_GROUP* group = new PCB_GROUP( parent );
1789 parent->Add( group );
1790
1792 {
1793 PCB_SHAPE* shape = new PCB_SHAPE( parent, type );
1794 if( type == SHAPE_T::RECTANGLE )
1795 {
1796 shape->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 6 ) ) );
1797 shape->SetCornerRadius( pcbIUScale.mmToIU( 1 ) );
1798 }
1799 parent->Add( shape );
1800 group->AddItem( shape );
1801 }
1802
1803 PCB_BARCODE* barcode = new PCB_BARCODE( parent );
1804 parent->Add( barcode );
1805 group->AddItem( barcode );
1806 PCB_TEXTBOX* textbox = new PCB_TEXTBOX( parent );
1807 textbox->SetIsKnockout( true );
1808 textbox->SetText( wxT( "Do not retain as plain text" ) );
1809 parent->Add( textbox );
1810 group->AddItem( textbox );
1811
1812 PCB_SHAPE* supported = new PCB_SHAPE( parent, SHAPE_T::CIRCLE );
1813 parent->Add( supported );
1814 group->AddItem( supported );
1815 const KIID supportedUuid = supported->m_Uuid;
1816 const DOWNGRADE_TARGET& target = kicad9;
1817 auto normal = ClassifyBoardForDowngrade( &board, target );
1818 auto omit = ClassifyBoardForDowngrade( &board, target, true );
1820 BOOST_CHECK_EQUAL( omit.Count( DOWNGRADE_BUCKET::LOWER ), 0 );
1821 BOOST_CHECK_EQUAL( omit.Count( DOWNGRADE_BUCKET::DROP ), 5 );
1822 BOOST_REQUIRE_EQUAL( normal.Entries().size(), omit.Entries().size() );
1823 for( size_t i = 0; i < normal.Entries().size(); ++i )
1824 {
1825 BOOST_CHECK( normal.Entries()[i].m_feature == omit.Entries()[i].m_feature );
1826 BOOST_CHECK_EQUAL( normal.Entries()[i].m_count, omit.Entries()[i].m_count );
1827 BOOST_CHECK( normal.Entries()[i].m_detail != omit.Entries()[i].m_detail );
1828 BOOST_CHECK( !omit.Entries()[i].m_detail.IsEmpty() );
1829 }
1830
1831 // Classification is read-only.
1832 BOOST_CHECK_EQUAL( group->GetItems().size(), 6 );
1833 if( footprintOnly )
1834 DowngradeFootprintInPlace( fp, target, true );
1835 else
1836 DowngradeBoardInPlace( &board, target, true );
1837 BOOST_REQUIRE_EQUAL( group->GetItems().size(), 1 );
1838 BOOST_CHECK( *group->GetItems().begin() == supported );
1839 BOOST_CHECK( supported->m_Uuid == supportedUuid );
1840 BOOST_CHECK( !ClassifyBoardForDowngrade( &board, target, true ).IsLossy() );
1841 }
1842}
1843
1844
1845BOOST_AUTO_TEST_CASE( DropApproximationsOmitsFillAndCellContentsOnly )
1846{
1847 BOARD board;
1848 FOOTPRINT* fp = new FOOTPRINT( &board );
1849 board.Add( fp );
1850 for( BOARD_ITEM_CONTAINER* parent :
1851 { static_cast<BOARD_ITEM_CONTAINER*>( &board ), static_cast<BOARD_ITEM_CONTAINER*>( fp ) } )
1852 {
1853 PCB_SHAPE* hatch = new PCB_SHAPE( parent, SHAPE_T::RECTANGLE );
1854 hatch->SetLayer( F_SilkS );
1855 hatch->SetFillMode( FILL_T::HATCH );
1856 parent->Add( hatch );
1857 PCB_TABLE* table = new PCB_TABLE( parent, pcbIUScale.mmToIU( 0.1 ) );
1858 table->SetColCount( 2 );
1859 PCB_TABLECELL* knockout = new PCB_TABLECELL( table );
1860 knockout->SetIsKnockout( true );
1861 knockout->SetText( wxT( "knockout" ) );
1862 table->AddCell( knockout );
1863 PCB_TABLECELL* plain = new PCB_TABLECELL( table );
1864 plain->SetText( wxT( "supported" ) );
1865 table->AddCell( plain );
1866 parent->Add( table );
1867
1868 if( parent == fp )
1869 DowngradeFootprintInPlace( fp, kicad9, true );
1870 else
1871 DowngradeBoardInPlace( &board, kicad9, true );
1872 BOOST_CHECK( hatch->GetFillMode() == FILL_T::NO_FILL );
1873 BOOST_CHECK( hatch->GetShape() == SHAPE_T::RECTANGLE );
1874 BOOST_REQUIRE_EQUAL( table->GetCells().size(), 2 );
1875 BOOST_CHECK( knockout->GetText().IsEmpty() );
1876 BOOST_CHECK( !knockout->IsKnockout() );
1877 BOOST_CHECK( plain->GetText() == wxT( "supported" ) );
1878 }
1879}
1880
1881
1882// Removing a hatch fill on a copper layer would delete copper, and the omission preference is
1883// not an electrical override. Copper keeps a solid fill under both policies. Other layers carry
1884// ink, so there the fill may go.
1885// The report must name the copper case separately. One combined row would label the copper
1886// approximation a drop as soon as the omission preference is set, and hide how many of the
1887// counted shapes are copper.
1888BOOST_AUTO_TEST_CASE( HatchedCopperAndInkFillsAreReportedSeparately )
1889{
1890 BOARD board;
1891
1892 for( int ii = 0; ii < 3; ++ii )
1893 {
1894 PCB_SHAPE* ink = new PCB_SHAPE( &board, SHAPE_T::RECTANGLE );
1895 ink->SetLayer( F_SilkS );
1896 ink->SetFillMode( FILL_T::HATCH );
1897 board.Add( ink );
1898 }
1899
1900 PCB_SHAPE* copper = new PCB_SHAPE( &board, SHAPE_T::RECTANGLE );
1901 copper->SetLayer( F_Cu );
1902 copper->SetFillMode( FILL_T::HATCH );
1903 board.Add( copper );
1904
1905 for( bool drop : { false, true } )
1906 {
1907 BOOST_TEST_CONTEXT( "Drop approximations: " << drop )
1908 {
1909 COMPATIBILITY_REPORT report = ClassifyBoardForDowngrade( &board, kicad9, drop );
1910
1911 BOOST_CHECK_EQUAL( report.Entries().size(), 2 );
1912 BOOST_CHECK_EQUAL( report.Count( DOWNGRADE_BUCKET::LOWER ), drop ? 1 : 4 );
1913 BOOST_CHECK_EQUAL( report.Count( DOWNGRADE_BUCKET::DROP ), drop ? 3 : 0 );
1914 }
1915 }
1916}
1917
1918
1919BOOST_AUTO_TEST_CASE( DropApproximationsKeepsHatchedCopperAsSolidFill )
1920{
1921 for( bool drop : { false, true } )
1922 {
1923 BOOST_TEST_CONTEXT( "Drop approximations: " << drop )
1924 {
1925 BOARD board;
1926 FOOTPRINT* fp = new FOOTPRINT( &board );
1927 board.Add( fp );
1928
1929 std::vector<PCB_SHAPE*> copper;
1930 std::vector<PCB_SHAPE*> silk;
1931
1932 for( BOARD_ITEM_CONTAINER* parent : { static_cast<BOARD_ITEM_CONTAINER*>( &board ),
1933 static_cast<BOARD_ITEM_CONTAINER*>( fp ) } )
1934 {
1935 PCB_SHAPE* onCopper = new PCB_SHAPE( parent, SHAPE_T::RECTANGLE );
1936 onCopper->SetLayer( F_Cu );
1937 onCopper->SetFillMode( FILL_T::HATCH );
1938 parent->Add( onCopper );
1939 copper.push_back( onCopper );
1940
1941 PCB_SHAPE* onSilk = new PCB_SHAPE( parent, SHAPE_T::RECTANGLE );
1942 onSilk->SetLayer( F_SilkS );
1943 onSilk->SetFillMode( FILL_T::HATCH );
1944 parent->Add( onSilk );
1945 silk.push_back( onSilk );
1946 }
1947
1948 DowngradeBoardInPlace( &board, kicad9, drop );
1949
1950 for( PCB_SHAPE* shape : copper )
1951 BOOST_CHECK( shape->GetFillMode() == FILL_T::FILLED_SHAPE );
1952
1953 for( PCB_SHAPE* shape : silk )
1954 BOOST_CHECK( shape->GetFillMode() == ( drop ? FILL_T::NO_FILL : FILL_T::FILLED_SHAPE ) );
1955 }
1956 }
1957}
1958
1959
1960BOOST_AUTO_TEST_CASE( DropApproximationsOmitsRoundedPadPrimitives )
1961{
1962 for( bool footprintOnly : { false, true } )
1963 {
1964 BOARD board;
1965 FOOTPRINT* fp = new FOOTPRINT( &board );
1966 board.Add( fp );
1967 PAD* pad = new PAD( fp );
1968 fp->Add( pad );
1969 pad->SetShape( F_Cu, PAD_SHAPE::CUSTOM );
1970 PCB_SHAPE* rounded = new PCB_SHAPE( fp, SHAPE_T::RECTANGLE );
1971 rounded->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 6 ) ) );
1972 rounded->SetCornerRadius( pcbIUScale.mmToIU( 1 ) );
1973 pad->AddPrimitive( F_Cu, rounded );
1974 PCB_SHAPE* supported = new PCB_SHAPE( fp, SHAPE_T::CIRCLE );
1975 pad->AddPrimitive( F_Cu, supported );
1976 const DOWNGRADE_TARGET& target = kicad9;
1977 BOOST_CHECK_EQUAL( ClassifyBoardForDowngrade( &board, target, true ).Count( DOWNGRADE_BUCKET::DROP ), 1 );
1978 if( footprintOnly )
1979 DowngradeFootprintInPlace( fp, target, true );
1980 else
1981 DowngradeBoardInPlace( &board, target, true );
1982 BOOST_REQUIRE_EQUAL( pad->GetPrimitives( F_Cu ).size(), 1 );
1983 BOOST_CHECK( pad->GetPrimitives( F_Cu ).front()->GetShape() == SHAPE_T::CIRCLE );
1984 BOOST_CHECK_EQUAL( fp->Pads().size(), 1 );
1985 }
1986}
1987
1988
1989// Removing a via deletes copper and splits a net, so the omission preference does not reach it.
1990BOOST_AUTO_TEST_CASE( ViaApproximationsSurviveDropPolicy )
1991{
1992 for( bool drop : { false, true } )
1993 {
1994 BOOST_TEST_CONTEXT( "Drop approximations: " << drop )
1995 {
1996 BOARD board;
1997 PCB_VIA* skip = new PCB_VIA( &board );
1999 board.Add( skip );
2000 PCB_VIA* buried = new PCB_VIA( &board );
2001 buried->SetViaType( VIATYPE::BURIED );
2002 board.Add( buried );
2003 PCB_VIA* supported = new PCB_VIA( &board );
2004 board.Add( supported );
2005
2006 const auto report = ClassifyBoardForDowngrade( &board, kicad9, drop );
2007
2008 BOOST_CHECK_EQUAL( report.Count( DOWNGRADE_BUCKET::LOWER ), 2 );
2009 BOOST_CHECK_EQUAL( report.Count( DOWNGRADE_BUCKET::DROP ), 0 );
2010
2011 DowngradeBoardInPlace( &board, kicad9, drop );
2012
2013 BOOST_REQUIRE_EQUAL( board.Tracks().size(), 3 );
2014 BOOST_CHECK( skip->Padstack().UnconnectedLayerMode()
2016 BOOST_CHECK( buried->GetViaType() == VIATYPE::BLIND );
2017 BOOST_CHECK( supported->GetViaType() == VIATYPE::THROUGH );
2018 }
2019 }
2020}
2021
2022
2023BOOST_AUTO_TEST_CASE( DropApproximationsKeepsTargetSupportedFeatures )
2024{
2025 BOARD board;
2026 PCB_SHAPE* rect = new PCB_SHAPE( &board, SHAPE_T::RECTANGLE );
2027 rect->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 6 ) ) );
2028 rect->SetCornerRadius( pcbIUScale.mmToIU( 1 ) );
2029 rect->SetFillMode( FILL_T::HATCH );
2030 board.Add( rect );
2031 PCB_TEXTBOX* textbox = new PCB_TEXTBOX( &board );
2032 textbox->SetIsKnockout( true );
2033 board.Add( textbox );
2034 board.Add( new PCB_BARCODE( &board ) );
2035 PCB_VIA* buried = new PCB_VIA( &board );
2036 buried->SetViaType( VIATYPE::BURIED );
2037 board.Add( buried );
2038 PCB_VIA* skip = new PCB_VIA( &board );
2040 board.Add( skip );
2041 const auto& target = kicad10;
2042 BOOST_CHECK( !ClassifyBoardForDowngrade( &board, target, true ).IsLossy() );
2043 DowngradeBoardInPlace( &board, target, true );
2044 BOOST_CHECK_EQUAL( board.Drawings().size(), 3 );
2045 BOOST_CHECK_EQUAL( board.Tracks().size(), 2 );
2046 BOOST_CHECK_EQUAL( rect->GetCornerRadius(), pcbIUScale.mmToIU( 1 ) );
2047 BOOST_CHECK( rect->GetFillMode() == FILL_T::HATCH );
2048 BOOST_CHECK( textbox->IsKnockout() );
2049 BOOST_CHECK( buried->GetViaType() == VIATYPE::BURIED );
2051}
2052
2053
2054BOOST_AUTO_TEST_CASE( DropApproximationsCannotBypassBlockRules )
2055{
2056 BOARD board;
2057 ZONE* zone = new ZONE( &board );
2059 board.Add( zone );
2060 BOOST_CHECK( ClassifyBoardForDowngrade( &board, kicad10, true ).IsBlocked() );
2061}
2062
2063
2064BOOST_AUTO_TEST_CASE( LibraryReplacementsWaitForTheDesignTransaction )
2065{
2066 KI_TEST::TEMPORARY_DIRECTORY temporary( "kicad_qa_shared_downgrade_transaction" );
2067 const wxString directory = temporary.GetPath().wstring();
2068 const wxString library = directory + wxT( "/library.fake_mod" );
2069 const wxString board = directory + wxT( "/board.kicad_pcb" );
2070
2071 auto write = []( const wxString& aPath, const wxString& aContent )
2072 {
2073 wxFFile file( aPath, wxT( "wb" ) );
2074 BOOST_REQUIRE( file.IsOpened() && file.Write( aContent ) && file.Close() );
2075 };
2076
2077 auto read = []( const wxString& aPath )
2078 {
2079 wxFFile file( aPath, wxT( "rb" ) );
2080 wxString content;
2081 BOOST_REQUIRE( file.IsOpened() && file.ReadAll( &content ) );
2082 return content;
2083 };
2084
2085 auto noForbidden = []( const wxString& )
2086 {
2087 return wxString();
2088 };
2089 auto convert = [&]( const wxString&, const wxString& aTemporary )
2090 {
2091 write( aTemporary, wxT( "(version 123) converted" ) );
2093 };
2094
2095 write( library, wxT( "original library" ) );
2096 write( board, wxT( "original board" ) );
2097
2098 {
2099 DOWNGRADE_FILE_TRANSACTION transaction;
2101 DowngradeLibraryFilesInPlace( directory, wxT( "*.fake_mod" ), 123, noForbidden, convert, &transaction )
2102 .IsEmpty() );
2103 BOOST_CHECK_EQUAL( read( library ), wxString( wxT( "original library" ) ) );
2104 BOOST_CHECK( transaction.Stage( directory + wxT( "/missing/board.kicad_pcb" ) ).IsEmpty() );
2105 }
2106
2107 BOOST_CHECK_EQUAL( read( library ), wxString( wxT( "original library" ) ) );
2108 BOOST_CHECK_EQUAL( read( board ), wxString( wxT( "original board" ) ) );
2109
2110 {
2111 DOWNGRADE_FILE_TRANSACTION transaction;
2113 DowngradeLibraryFilesInPlace( directory, wxT( "*.fake_mod" ), 123, noForbidden, convert, &transaction )
2114 .IsEmpty() );
2115 wxString stagedBoard = transaction.Stage( board );
2116 BOOST_REQUIRE( !stagedBoard.IsEmpty() );
2117 write( stagedBoard, wxT( "converted board" ) );
2118 BOOST_REQUIRE( transaction.Commit().IsEmpty() );
2119 }
2120
2121 BOOST_CHECK_EQUAL( read( library ), wxString( wxT( "(version 123) converted" ) ) );
2122 BOOST_CHECK_EQUAL( read( board ), wxString( wxT( "converted board" ) ) );
2123 wxArrayString files;
2124 wxDir::GetAllFiles( directory, &files );
2125 BOOST_CHECK_EQUAL( files.size(), 2 );
2126}
2127
2128
2129BOOST_AUTO_TEST_CASE( DirectoryDestinationStopsAllTransactionReplacements )
2130{
2131 KI_TEST::TEMPORARY_DIRECTORY temporary( "kicad_qa_downgrade_transaction_directory" );
2132 const wxString directory = temporary.GetPath().wstring();
2133 const wxString first = directory + wxT( "/first.kicad_pcb" );
2134 const wxString second = directory + wxT( "/second.kicad_pcb" );
2135
2136 {
2137 wxFFile file( first, wxT( "wb" ) );
2138 BOOST_REQUIRE( file.IsOpened() && file.Write( wxT( "original" ) ) && file.Close() );
2139 }
2140
2141 {
2142 DOWNGRADE_FILE_TRANSACTION transaction;
2143 wxString stagedFirst = transaction.Stage( first );
2144 wxString stagedSecond = transaction.Stage( second );
2145 BOOST_REQUIRE( !stagedFirst.IsEmpty() && !stagedSecond.IsEmpty() );
2146
2147 for( const wxString& file : { stagedFirst, stagedSecond } )
2148 {
2149 wxFFile output( file, wxT( "wb" ) );
2150 BOOST_REQUIRE( output.IsOpened() && output.Write( wxT( "converted" ) ) && output.Close() );
2151 }
2152
2153 BOOST_REQUIRE( wxMkdir( second ) );
2154 BOOST_CHECK_EQUAL( transaction.Commit(), second );
2155 BOOST_CHECK( transaction.GetRecoveryError().IsEmpty() );
2156 }
2157
2158 wxFFile input( first, wxT( "rb" ) );
2159 wxString content;
2160 BOOST_REQUIRE( input.IsOpened() && input.ReadAll( &content ) );
2161 BOOST_CHECK_EQUAL( content, wxString( wxT( "original" ) ) );
2162 BOOST_CHECK( wxDirExists( second ) );
2163 wxArrayString files;
2164 wxDir::GetAllFiles( directory, &files );
2165 BOOST_CHECK_EQUAL( files.size(), 1 );
2166}
2167
2168
2169BOOST_AUTO_TEST_CASE( ConcurrentTransactionsUseDifferentOwnedStagingFiles )
2170{
2171 KI_TEST::TEMPORARY_DIRECTORY temporary( "kicad_qa_downgrade_transaction_unique" );
2172 const wxString destination = temporary.GetPath().wstring() + wxT( "/board.kicad_pcb" );
2173
2174 {
2177 wxString firstTemporary = first.Stage( destination );
2178 wxString secondTemporary = second.Stage( destination );
2179 BOOST_REQUIRE( !firstTemporary.IsEmpty() && !secondTemporary.IsEmpty() );
2180 BOOST_CHECK( firstTemporary != secondTemporary );
2181 BOOST_CHECK( wxFileExists( firstTemporary ) );
2182 BOOST_CHECK( wxFileExists( secondTemporary ) );
2183 first.Discard( firstTemporary );
2184 BOOST_CHECK( !wxFileExists( firstTemporary ) );
2185 BOOST_CHECK( wxFileExists( secondTemporary ) );
2186 BOOST_CHECK( !wxFileExists( destination ) );
2187 }
2188
2189 BOOST_CHECK( std::filesystem::is_empty( temporary.GetPath() ) );
2190}
2191
2192
2193BOOST_AUTO_TEST_CASE( TransactionSymlinkReplacementLeavesLinkedSourcesUnchanged )
2194{
2195 KI_TEST::TEMPORARY_DIRECTORY temporary( "kicad_qa_downgrade_transaction_symlink" );
2196 const auto original = temporary.GetPath() / u8"original-μ.kicad_mod";
2197 const auto alias = temporary.GetPath() / "alias.kicad_mod";
2198 const auto danglingAlias = temporary.GetPath() / "dangling.kicad_mod";
2199
2200 {
2201 wxFFile file( original.wstring(), wxT( "wb" ) );
2202 BOOST_REQUIRE( file.IsOpened() && file.Write( wxT( "original source" ) ) && file.Close() );
2203 }
2204
2205 std::error_code error;
2206 std::filesystem::create_symlink( original.filename(), alias, error );
2207
2208 if( error )
2209 {
2210 BOOST_TEST_MESSAGE( "Symlink creation is unavailable on this system" );
2211 return;
2212 }
2213
2214 std::filesystem::create_symlink( "missing.kicad_mod", danglingAlias, error );
2215 BOOST_REQUIRE( !error );
2216
2217 {
2218 DOWNGRADE_FILE_TRANSACTION transaction;
2219
2220 for( const auto& destination : { alias, danglingAlias } )
2221 {
2222 wxString staged = transaction.Stage( destination.wstring() );
2223 BOOST_REQUIRE( !staged.IsEmpty() );
2224 wxFFile file( staged, wxT( "wb" ) );
2225 BOOST_REQUIRE( file.IsOpened() && file.Write( wxT( "converted output" ) ) && file.Close() );
2226 }
2227
2228 BOOST_REQUIRE( transaction.Commit().IsEmpty() );
2229 }
2230
2231 wxFFile file( original.wstring(), wxT( "rb" ) );
2232 wxString content;
2233 BOOST_REQUIRE( file.IsOpened() && file.ReadAll( &content ) );
2234 BOOST_CHECK_EQUAL( content, wxString( wxT( "original source" ) ) );
2235 BOOST_CHECK( !std::filesystem::is_symlink( alias ) );
2236 BOOST_CHECK( !std::filesystem::is_symlink( danglingAlias ) );
2237 BOOST_CHECK_EQUAL( std::distance( std::filesystem::directory_iterator( temporary.GetPath() ),
2238 std::filesystem::directory_iterator() ),
2239 3 );
2240}
2241
2242
2243static void checkFeature( const COMPATIBILITY_REPORT& aReport, const wxString& aName, DOWNGRADE_BUCKET aBucket,
2244 int aCount )
2245{
2246 for( const COMPAT_ENTRY& entry : aReport.Entries() )
2247 {
2248 if( entry.m_feature == aName )
2249 {
2250 BOOST_CHECK( entry.m_bucket == aBucket );
2251 BOOST_CHECK_EQUAL( entry.m_count, aCount );
2252 BOOST_CHECK( !entry.m_detail.IsEmpty() );
2253 return;
2254 }
2255 }
2256
2257 BOOST_ERROR( "Missing compatibility feature: " + aName.ToStdString() );
2258}
2259
2260
2261BOOST_AUTO_TEST_CASE( GeometricConstraintsDropWithoutMovingBoardOrFootprintGeometry )
2262{
2263 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
2264 {
2265 for( bool drop : { false, true } )
2266 {
2267 BOARD board;
2268 FOOTPRINT* fp = new FOOTPRINT( &board );
2269 board.Add( fp );
2270
2271 std::vector<PCB_SHAPE*> shapes;
2272
2273 for( BOARD_ITEM_CONTAINER* parent :
2274 { static_cast<BOARD_ITEM_CONTAINER*>( &board ), static_cast<BOARD_ITEM_CONTAINER*>( fp ) } )
2275 {
2276 PCB_SHAPE* shape = new PCB_SHAPE( parent, SHAPE_T::SEGMENT );
2277 shape->SetStart( VECTOR2I( 1000000, 2000000 ) );
2278 shape->SetEnd( VECTOR2I( 7000000, 2000000 ) );
2279 parent->Add( shape );
2280 shapes.push_back( shape );
2282 constraint->AddMember( shape->m_Uuid );
2283 parent->Add( constraint );
2284 }
2285
2286 const auto report = ClassifyBoardForDowngrade( &board, target, drop );
2287 BOOST_CHECK( !report.IsBlocked() );
2288 checkFeature( report, wxT( "Geometric constraints" ), DOWNGRADE_BUCKET::DROP, 2 );
2289 BOOST_CHECK_EQUAL( board.Constraints().size(), 1 );
2290 BOOST_CHECK_EQUAL( fp->Constraints().size(), 1 );
2291 DowngradeBoardInPlace( &board, target, drop );
2292 BOOST_CHECK( board.Constraints().empty() );
2293 BOOST_CHECK( fp->Constraints().empty() );
2294
2295 for( PCB_SHAPE* shape : shapes )
2296 {
2297 BOOST_CHECK( shape->GetStart() == VECTOR2I( 1000000, 2000000 ) );
2298 BOOST_CHECK( shape->GetEnd() == VECTOR2I( 7000000, 2000000 ) );
2299 }
2300 }
2301 }
2302}
2303
2304
2305BOOST_AUTO_TEST_CASE( GridAndDrillDrawingLossesAreReportedBeforeRemoval )
2306{
2307 for( bool drop : { false, true } )
2308 {
2309 BOARD board;
2310 board.Add( new PCB_GRID_ITEM( &board ) );
2311 board.Add( new PCB_DRILL_CHART( &board ) );
2312 board.Add( new PCB_DRILL_MAP( &board ) );
2313 board.GetDesignSettings().GetDrillSymbolProfile().SetName( wxT( "Fabrication symbols" ) );
2314 PCB_VIA* via = new PCB_VIA( &board );
2315 via->SetPosition( VECTOR2I( 1000000, 2000000 ) );
2316 board.Add( via );
2317
2318 const auto report = ClassifyBoardForDowngrade( &board, kicad10, drop );
2319 checkFeature( report, wxT( "Local grids" ), DOWNGRADE_BUCKET::DROP, 1 );
2320 checkFeature( report, wxT( "Drill charts and maps" ), DOWNGRADE_BUCKET::DROP, 2 );
2321 checkFeature( report, wxT( "Drill symbol configuration" ), DOWNGRADE_BUCKET::DROP, 1 );
2322 BOOST_CHECK_EQUAL( board.Drawings().size(), 3 );
2323 DowngradeBoardInPlace( &board, kicad10, drop );
2324 BOOST_CHECK( board.Drawings().empty() );
2325 BOOST_REQUIRE_EQUAL( board.Tracks().size(), 1 );
2326 BOOST_CHECK( via->GetPosition() == VECTOR2I( 1000000, 2000000 ) );
2327 BOOST_CHECK( board.GetDesignSettings().GetDrillSymbolProfile() == DRILL_SYMBOL_PROFILE() );
2328 }
2329}
2330
2331
2332BOOST_AUTO_TEST_CASE( NewGeneratorsLoseEditabilityAndKeepCopperAndOuterGroups )
2333{
2334 for( bool drop : { false, true } )
2335 {
2336 BOARD board;
2337 PCB_GROUP* outer = new PCB_GROUP( &board );
2338 board.Add( outer );
2339 PCB_VIA_STITCH* stitch = new PCB_VIA_STITCH( &board );
2340 PCB_VIA_STACK* stack = new PCB_VIA_STACK( &board );
2341 board.Add( stitch );
2342 board.Add( stack );
2343 outer->AddItem( stitch );
2344 outer->AddItem( stack );
2345 std::vector<PCB_VIA*> vias;
2346
2347 for( PCB_GENERATOR* generator :
2348 { static_cast<PCB_GENERATOR*>( stitch ), static_cast<PCB_GENERATOR*>( stack ) } )
2349 {
2350 PCB_VIA* via = new PCB_VIA( &board );
2351 via->SetPosition( VECTOR2I( 1000000 * ( vias.size() + 1 ), 2000000 ) );
2352 via->SetWidth( F_Cu, 600000 );
2353 via->SetDrill( 300000 );
2354 board.Add( via );
2355 generator->AddItem( via );
2356 vias.push_back( via );
2357 }
2358
2359 const auto report = ClassifyBoardForDowngrade( &board, kicad10, drop );
2360 checkFeature( report, wxT( "Via stitching and guarding" ), DOWNGRADE_BUCKET::DROP, 1 );
2361 checkFeature( report, wxT( "Microvia stack generators" ), DOWNGRADE_BUCKET::DROP, 1 );
2362 DowngradeBoardInPlace( &board, kicad10, drop );
2363 BOOST_CHECK( board.Generators().empty() );
2364 BOOST_REQUIRE_EQUAL( board.Tracks().size(), 2 );
2365 BOOST_REQUIRE_EQUAL( outer->GetItems().size(), 2 );
2366
2367 for( size_t ii = 0; ii < vias.size(); ++ii )
2368 {
2369 BOOST_CHECK( vias[ii]->GetPosition() == VECTOR2I( 1000000 * ( ii + 1 ), 2000000 ) );
2370 BOOST_CHECK_EQUAL( vias[ii]->GetWidth( F_Cu ), 600000 );
2371 BOOST_CHECK_EQUAL( vias[ii]->GetDrill(), 300000 );
2372 BOOST_CHECK( vias[ii]->GetParentGroup() == outer );
2373 }
2374 }
2375}
2376
2377
2378BOOST_AUTO_TEST_CASE( NativeViaGeneratorFixtureExportsCopperAndOwnershipForBothTargetsAndPolicies )
2379{
2380 SETTINGS_MANAGER settingsManager;
2381 const std::string src =
2382 KI_TEST::GetPcbnewTestDataDir() + "../downgrade/regressions/via_generators/via_generators.kicad_pcb";
2383 const std::string originalBytes = KI_TEST::ReadGoldenText( src );
2385 std::unique_ptr<BOARD> source( io.LoadBoard( src, nullptr ) );
2387 BOOST_REQUIRE_EQUAL( source->GetCopperLayerCount(), 6 );
2388 BOOST_REQUIRE_EQUAL( source->Zones().size(), 3u );
2389 BOOST_REQUIRE_EQUAL( source->Generators().size(), 3u );
2390 BOOST_REQUIRE_EQUAL( source->Groups().size(), 1u );
2391 BOOST_REQUIRE( source->GroupsSanityCheck().IsEmpty() );
2392
2393 const auto copperById = []( const BOARD& aBoard )
2394 {
2395 std::map<KIID, const PCB_TRACK*> items;
2396
2397 for( const PCB_TRACK* item : aBoard.Tracks() )
2398 BOOST_REQUIRE( items.emplace( item->m_Uuid, item ).second );
2399
2400 return items;
2401 };
2402 const auto memberIds = []( const PCB_GROUP& aGroup )
2403 {
2404 std::set<KIID> ids;
2405
2406 for( const EDA_ITEM* member : aGroup.GetItems() )
2407 BOOST_REQUIRE( ids.insert( member->m_Uuid ).second );
2408
2409 return ids;
2410 };
2411 const auto serializeNative = []( BOARD* aBoard )
2412 {
2413 STRING_FORMATTER formatter;
2414 PCB_IO_KICAD_SEXPR serializer;
2415 serializer.FormatBoardToFormatter( &formatter, aBoard );
2416 return formatter.GetString();
2417 };
2418
2419 const auto sourceCopper = copperById( *source );
2420 BOOST_REQUIRE_EQUAL( sourceCopper.size(), 83u );
2421 std::set<KIID> plainVias;
2422 std::set<KIID> generatedVias;
2423 size_t sourceVias = 0;
2424 size_t sourceSegments = 0;
2425 size_t sourceMicrovias = 0;
2426
2427 for( const auto& [id, item] : sourceCopper )
2428 {
2429 if( const PCB_VIA* via = dynamic_cast<const PCB_VIA*>( item ) )
2430 {
2431 ++sourceVias;
2432
2433 if( via->GetViaType() == VIATYPE::MICROVIA )
2434 {
2435 ++sourceMicrovias;
2436 BOOST_REQUIRE( via->Padstack().Drill().is_filled.value_or( false ) );
2437 }
2438
2439 if( !via->GetParentGroup() )
2440 plainVias.insert( id );
2441 }
2442 else
2443 {
2444 BOOST_REQUIRE( item->Type() == PCB_TRACE_T );
2445 ++sourceSegments;
2446 }
2447 }
2448
2449 BOOST_REQUIRE_EQUAL( sourceVias, 80u );
2450 BOOST_REQUIRE_EQUAL( sourceSegments, 3u );
2451 BOOST_REQUIRE_EQUAL( sourceMicrovias, 2u );
2452 BOOST_REQUIRE_EQUAL( plainVias.size(), 3u );
2453
2454 PCB_VIA_STITCH* stitch = nullptr;
2455 PCB_VIA_STITCH* guard = nullptr;
2456 PCB_VIA_STACK* stack = nullptr;
2457
2458 for( PCB_GENERATOR* generator : source->Generators() )
2459 {
2460 if( PCB_VIA_STITCH* viaStitch = dynamic_cast<PCB_VIA_STITCH*>( generator ) )
2461 {
2462 BOOST_REQUIRE( source->FindNet( viaStitch->GetNetCode() ) );
2463 BOOST_REQUIRE( source->FindNet( viaStitch->GetNetCode() )->GetNetname() == wxT( "GND" ) );
2464
2465 if( viaStitch->GetMode() == PCB_VIA_STITCH_MODE::STITCH )
2466 {
2467 BOOST_REQUIRE( !stitch );
2468 stitch = viaStitch;
2469 }
2470 else
2471 {
2472 BOOST_REQUIRE( viaStitch->GetMode() == PCB_VIA_STITCH_MODE::GUARD );
2473 BOOST_REQUIRE( !guard );
2474 guard = viaStitch;
2475 BOOST_REQUIRE( source->FindNet( guard->GetGuardedNetCode() ) );
2476 BOOST_REQUIRE( source->FindNet( guard->GetGuardedNetCode() )->GetNetname() == wxT( "SIGNAL" ) );
2477 }
2478 }
2479 else
2480 {
2481 BOOST_REQUIRE( !stack );
2482 stack = dynamic_cast<PCB_VIA_STACK*>( generator );
2483 BOOST_REQUIRE( stack );
2485 BOOST_REQUIRE( stack->GetStartLayer() == F_Cu );
2486 BOOST_REQUIRE( stack->GetEndLayer() == In2_Cu );
2487 }
2488
2489 for( BOARD_ITEM* member : generator->GetBoardItems() )
2490 {
2491 BOOST_REQUIRE( member->Type() == PCB_VIA_T );
2492 BOOST_REQUIRE( member->GetParentGroup() == static_cast<EDA_GROUP*>( generator ) );
2493 BOOST_REQUIRE( sourceCopper.count( member->m_Uuid ) == 1u );
2494 BOOST_REQUIRE( generatedVias.insert( member->m_Uuid ).second );
2495 }
2496 }
2497
2498 BOOST_REQUIRE( stitch && guard && stack );
2499 BOOST_REQUIRE_EQUAL( stitch->GetItems().size(), 49u );
2500 BOOST_REQUIRE_EQUAL( guard->GetItems().size(), 26u );
2501 BOOST_REQUIRE_EQUAL( stack->GetItems().size(), 2u );
2502 BOOST_REQUIRE_EQUAL( generatedVias.size(), 77u );
2503 const auto stitchMembers = memberIds( *stitch );
2504 PCB_GROUP* outer = *source->Groups().begin();
2505 const KIID outerId = outer->m_Uuid;
2506 const wxString outerName = outer->GetName();
2507 BOOST_REQUIRE( stitch->GetParentGroup() == static_cast<EDA_GROUP*>( outer ) );
2508 BOOST_REQUIRE( !guard->GetParentGroup() && !stack->GetParentGroup() );
2509 BOOST_REQUIRE( memberIds( *outer ) == std::set<KIID>{ stitch->m_Uuid } );
2510
2511 const std::string sourceModel = serializeNative( source.get() );
2512 KI_TEST::TEMPORARY_DIRECTORY tmp( "kicad_qa_native_via_generators", "" );
2513
2514 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
2515 {
2516 const bool preTen = target.m_boardVersion == kicad9.m_boardVersion;
2517
2518 for( bool drop : { false, true } )
2519 {
2520 BOOST_TEST_CONTEXT( target.m_id.ToStdString() << " drop=" << drop )
2521 {
2522 const COMPATIBILITY_REPORT preview = ClassifyBoardForDowngrade( source.get(), target, drop );
2523 BOOST_CHECK( !preview.IsBlocked() );
2525 BOOST_CHECK_EQUAL( preview.Count( DOWNGRADE_BUCKET::DROP ), preTen ? 5 : 3 );
2526 BOOST_CHECK_EQUAL( serializeNative( source.get() ), sourceModel );
2527 const std::string dest =
2528 ( tmp.GetPath()
2529 / ( target.m_id.ToStdString() + ( drop ? "-drop.kicad_pcb" : "-lower.kicad_pcb" ) ) )
2530 .string();
2531 COMPATIBILITY_REPORT report;
2532 wxString unsupportedToken;
2533 BOOST_REQUIRE( ExportBoardToOlderVersion( src, dest, target, report, &unsupportedToken, wxEmptyString,
2534 drop ) );
2535 BOOST_CHECK( unsupportedToken.IsEmpty() );
2536 BOOST_CHECK( !report.IsBlocked() );
2538 BOOST_CHECK_EQUAL( report.Count( DOWNGRADE_BUCKET::DROP ), preTen ? 5 : 3 );
2539 BOOST_REQUIRE_EQUAL( report.Entries().size(), preTen ? 3u : 2u );
2540 checkFeature( report, wxT( "Via stitching and guarding" ), DOWNGRADE_BUCKET::DROP, 2 );
2541 checkFeature( report, wxT( "Microvia stack generators" ), DOWNGRADE_BUCKET::DROP, 1 );
2542
2543 if( preTen )
2544 checkFeature( report, wxT( "Via protection" ), DOWNGRADE_BUCKET::DROP, 2 );
2545
2546 BOOST_REQUIRE_EQUAL( report.Entries().size(), preview.Entries().size() );
2547
2548 for( size_t ii = 0; ii < preview.Entries().size(); ++ii )
2549 {
2550 BOOST_CHECK( report.Entries()[ii].m_bucket == preview.Entries()[ii].m_bucket );
2551 BOOST_CHECK( report.Entries()[ii].m_feature == preview.Entries()[ii].m_feature );
2552 BOOST_CHECK_EQUAL( report.Entries()[ii].m_count, preview.Entries()[ii].m_count );
2553 }
2554
2555 const std::string outputBytes = KI_TEST::ReadGoldenText( dest );
2556 BOOST_CHECK( FindUnsupportedBoardToken( wxString::FromUTF8( outputBytes ), target ).IsEmpty() );
2557 BOOST_CHECK( outputBytes.find( "(generated" ) == std::string::npos );
2558
2559 if( preTen )
2560 {
2561 for( const char* token : { "(filling", "(capping", "(covering", "(plugging" } )
2562 BOOST_CHECK( outputBytes.find( token ) == std::string::npos );
2563 }
2564
2565 std::unique_ptr<BOARD> exported( io.LoadBoard( dest, nullptr ) );
2566 BOOST_REQUIRE( exported );
2567 BOOST_CHECK_EQUAL( exported->GetFileFormatVersionAtLoad(), target.m_boardVersion );
2568 BOOST_CHECK_EQUAL( exported->GetCopperLayerCount(), 6 );
2569 BOOST_CHECK_EQUAL( exported->Zones().size(), 3u );
2570 BOOST_CHECK( exported->Generators().empty() );
2571 BOOST_REQUIRE_EQUAL( exported->Groups().size(), 1u );
2572 BOOST_CHECK( exported->GroupsSanityCheck().IsEmpty() );
2573
2574 const auto exportedCopper = copperById( *exported );
2575 BOOST_REQUIRE_EQUAL( exportedCopper.size(), sourceCopper.size() );
2576 PCB_GROUP* exportedOuter = *exported->Groups().begin();
2577 BOOST_CHECK( exportedOuter->m_Uuid == outerId );
2578 BOOST_CHECK( exportedOuter->GetName() == outerName );
2579 BOOST_CHECK( memberIds( *exportedOuter ) == stitchMembers );
2580
2581 for( const auto& [id, original] : sourceCopper )
2582 {
2583 const auto found = exportedCopper.find( id );
2584 BOOST_REQUIRE( found != exportedCopper.end() );
2585 const PCB_TRACK* item = found->second;
2586 BOOST_CHECK( item->Type() == original->Type() );
2587 BOOST_CHECK( item->GetStart() == original->GetStart() );
2588 BOOST_CHECK( item->GetEnd() == original->GetEnd() );
2589 BOOST_CHECK( item->GetLayerSet() == original->GetLayerSet() );
2590 BOOST_CHECK_EQUAL( item->GetNetname().ToStdString(), original->GetNetname().ToStdString() );
2591
2592 if( const PCB_VIA* originalVia = dynamic_cast<const PCB_VIA*>( original ) )
2593 {
2594 const PCB_VIA* via = dynamic_cast<const PCB_VIA*>( item );
2595 BOOST_REQUIRE( via );
2596 BOOST_CHECK( via->GetPosition() == originalVia->GetPosition() );
2597 BOOST_CHECK( via->GetViaType() == originalVia->GetViaType() );
2598 BOOST_CHECK( via->TopLayer() == originalVia->TopLayer() );
2599 BOOST_CHECK( via->BottomLayer() == originalVia->BottomLayer() );
2600 BOOST_CHECK_EQUAL( via->GetDrillValue(), originalVia->GetDrillValue() );
2601
2602 for( PCB_LAYER_ID layer : LSET::AllCuMask( 6 ) )
2603 BOOST_CHECK_EQUAL( via->GetWidth( layer ), originalVia->GetWidth( layer ) );
2604
2605 const PADSTACK& padstack = via->Padstack();
2606
2607 if( preTen )
2608 {
2609 // The current reader resolves omitted legacy fields to board defaults.
2610 BOOST_CHECK( !padstack.Drill().is_filled.value_or( false ) );
2611 BOOST_CHECK( !padstack.Drill().is_capped.value_or( false ) );
2612 BOOST_CHECK( !padstack.FrontOuterLayers().has_covering.value_or( false ) );
2613 BOOST_CHECK( !padstack.BackOuterLayers().has_covering.value_or( false ) );
2614 BOOST_CHECK( !padstack.FrontOuterLayers().has_plugging.value_or( false ) );
2615 BOOST_CHECK( !padstack.BackOuterLayers().has_plugging.value_or( false ) );
2616 }
2617 else
2618 {
2619 const PADSTACK& originalStack = originalVia->Padstack();
2620 BOOST_CHECK( padstack.Drill().is_filled == originalStack.Drill().is_filled );
2621 BOOST_CHECK( padstack.Drill().is_capped == originalStack.Drill().is_capped );
2622 BOOST_CHECK( padstack.FrontOuterLayers().has_covering
2623 == originalStack.FrontOuterLayers().has_covering );
2624 BOOST_CHECK( padstack.BackOuterLayers().has_covering
2625 == originalStack.BackOuterLayers().has_covering );
2626 BOOST_CHECK( padstack.FrontOuterLayers().has_plugging
2627 == originalStack.FrontOuterLayers().has_plugging );
2628 BOOST_CHECK( padstack.BackOuterLayers().has_plugging
2629 == originalStack.BackOuterLayers().has_plugging );
2630 }
2631
2632 if( plainVias.count( id ) )
2633 BOOST_CHECK( !via->GetParentGroup() );
2634 }
2635 else
2636 {
2637 BOOST_CHECK( item->GetLayer() == original->GetLayer() );
2638 BOOST_CHECK_EQUAL( item->GetWidth(), original->GetWidth() );
2639 }
2640
2641 if( stitchMembers.count( id ) )
2642 BOOST_CHECK( item->GetParentGroup() == static_cast<EDA_GROUP*>( exportedOuter ) );
2643 else
2644 BOOST_CHECK( !item->GetParentGroup() );
2645 }
2646
2647 for( BOARD_ITEM* drawing : source->Drawings() )
2648 {
2649 if( const EDA_TEXT* text = dynamic_cast<const EDA_TEXT*>( drawing ) )
2650 {
2651 const auto found = std::find_if( exported->Drawings().begin(), exported->Drawings().end(),
2652 [&]( const BOARD_ITEM* aItem )
2653 {
2654 return aItem->m_Uuid == drawing->m_Uuid;
2655 } );
2656 BOOST_REQUIRE( found != exported->Drawings().end() );
2657 const EDA_TEXT* exportedText = dynamic_cast<const EDA_TEXT*>( *found );
2658 BOOST_REQUIRE( exportedText );
2659 BOOST_CHECK( exportedText->GetText() == text->GetText() );
2660 }
2661 }
2662
2663 BOOST_CHECK_EQUAL( serializeNative( source.get() ), sourceModel );
2664 BOOST_CHECK_EQUAL( KI_TEST::ReadGoldenText( src ), originalBytes );
2665 }
2666 }
2667 }
2668}
2669
2670
2671BOOST_AUTO_TEST_CASE( NativeTuningFixturesPreserveCopperAndEraEditabilityForBothPolicies )
2672{
2673 SETTINGS_MANAGER settingsManager;
2675 const std::string lengthPath = KI_TEST::GetPcbnewTestDataDir() + "tuning_generators_load_save.kicad_pcb";
2676 const std::string timePath = KI_TEST::GetPcbnewTestDataDir() + "issue24772/fw16_MCIO8i.kicad_pcb";
2677 const std::string lengthBytes = KI_TEST::ReadGoldenText( lengthPath );
2678 const std::string timeBytes = KI_TEST::ReadGoldenText( timePath );
2679 std::unique_ptr<BOARD> lengthFixture( io.LoadBoard( lengthPath, nullptr ) );
2680 std::unique_ptr<BOARD> timeFixture( io.LoadBoard( timePath, nullptr ) );
2681 BOOST_REQUIRE( lengthFixture && timeFixture );
2682 BOOST_REQUIRE_EQUAL( lengthFixture->Generators().size(), 1u );
2683 BOOST_REQUIRE_EQUAL( timeFixture->Generators().size(), 16u );
2684 PCB_TUNING_PATTERN* originalTime = nullptr;
2685
2686 for( PCB_GENERATOR* generator : timeFixture->Generators() )
2687 {
2688 if( generator->m_Uuid == KIID( "2acf841f-25ce-450c-b8df-dbbc34cc96ac" ) )
2689 originalTime = dynamic_cast<PCB_TUNING_PATTERN*>( generator );
2690 }
2691
2692 BOOST_REQUIRE( originalTime );
2693 BOOST_REQUIRE( originalTime->GetSettings().m_isTimeDomain );
2694 BOOST_REQUIRE_EQUAL( originalTime->GetItems().size(), 1u );
2695 BOARD board;
2696 board.SetCopperLayerCount( timeFixture->GetCopperLayerCount() );
2697 board.SetEnabledLayers( timeFixture->GetEnabledLayers() );
2698 auto length = dynamic_cast<PCB_TUNING_PATTERN*>( ( *lengthFixture->Generators().begin() )->DeepClone() );
2699 auto time = dynamic_cast<PCB_TUNING_PATTERN*>( originalTime->DeepClone() );
2700 BOOST_REQUIRE( length && time );
2701 BOOST_REQUIRE( !length->GetSettings().m_isTimeDomain );
2702 BOOST_REQUIRE_GT( length->GetItems().size(), 1u );
2703
2704 const auto addCopper = [&]( PCB_TRACK* aTrack )
2705 {
2706 NETINFO_ITEM* net = board.FindNet( aTrack->GetNetname() );
2707
2708 if( !net )
2709 {
2710 net = new NETINFO_ITEM( &board, aTrack->GetNetname() );
2711 board.Add( net );
2712 }
2713
2714 aTrack->SetParent( &board );
2715 aTrack->SetNetCode( net->GetNetCode() );
2716 board.Add( aTrack );
2717 };
2718
2719 for( PCB_TUNING_PATTERN* pattern : { length, time } )
2720 {
2721 for( BOARD_ITEM* member : pattern->GetBoardItems() )
2722 {
2723 PCB_TRACK* track = dynamic_cast<PCB_TRACK*>( member );
2724 BOOST_REQUIRE( track );
2725 addCopper( track );
2726 }
2727
2728 board.Add( pattern );
2729 }
2730
2731 PCB_TRACK* originalControl = nullptr;
2732
2733 for( PCB_TRACK* track : timeFixture->Tracks() )
2734 {
2735 if( track->Type() == PCB_TRACE_T && !dynamic_cast<PCB_GENERATOR*>( track->GetParentGroup() ) )
2736 {
2737 originalControl = track;
2738 break;
2739 }
2740 }
2741
2742 BOOST_REQUIRE( originalControl );
2743 auto control = static_cast<PCB_TRACK*>( originalControl->Clone() );
2744 control->SetParentGroup( nullptr );
2745 addCopper( control );
2746 PCB_GROUP* outer = new PCB_GROUP( &board );
2747 outer->SetName( wxT( "Native tuning ownership" ) );
2748 board.Add( outer );
2749 outer->AddItem( length );
2750 outer->AddItem( time );
2751
2752 // Populate the numeric cache through native APIs without changing fixture copper.
2753 double patternLength = 0;
2754
2755 for( BOARD_ITEM* member : length->GetBoardItems() )
2756 patternLength += static_cast<PCB_TRACK*>( member )->GetLength();
2757
2758 STRING_ANY_MAP lengthProperties = length->GetProperties();
2759
2760 // The native setter expects wxString values for these serialized string enums.
2761 for( const char* key : { "tuning_mode", "initial_side", "last_status" } )
2762 {
2763 std::string value;
2764 BOOST_REQUIRE( lengthProperties.get_to( key, value ) );
2765 lengthProperties[key] = wxAny( wxString::FromUTF8( value ) );
2766 }
2767
2768 lengthProperties["last_tuning_length"] = wxAny( patternLength / pcbIUScale.IU_PER_MM );
2769 length->SetProperties( lengthProperties );
2770 const wxString tuningInfo = length->GetRowData().back().second.GetString();
2771 BOOST_REQUIRE( !tuningInfo.IsEmpty() );
2772 const auto memberIds = []( const PCB_GROUP& aGroup )
2773 {
2774 std::set<KIID> ids;
2775
2776 for( const EDA_ITEM* member : aGroup.GetItems() )
2777 BOOST_REQUIRE( ids.insert( member->m_Uuid ).second );
2778
2779 return ids;
2780 };
2781 const auto serializeNative = []( BOARD* aBoard )
2782 {
2783 STRING_FORMATTER formatter;
2784 PCB_IO_KICAD_SEXPR serializer;
2785 serializer.FormatBoardToFormatter( &formatter, aBoard );
2786 return formatter.GetString();
2787 };
2788 std::map<KIID, const PCB_TRACK*> copper;
2789
2790 for( const PCB_TRACK* track : board.Tracks() )
2791 BOOST_REQUIRE( copper.emplace( track->m_Uuid, track ).second );
2792
2793 const auto lengthMembers = memberIds( *length );
2794 const auto timeMembers = memberIds( *time );
2795 BOOST_REQUIRE_EQUAL( copper.size(), lengthMembers.size() + timeMembers.size() + 1u );
2796 BOOST_REQUIRE( board.GroupsSanityCheck().IsEmpty() );
2797 KI_TEST::TEMPORARY_DIRECTORY tmp( "kicad_qa_native_tuning_boundaries" );
2798 const std::string src = ( tmp.GetPath() / "native-source.kicad_pcb" ).string();
2799 io.SaveBoard( src, board );
2800 const std::string sourceBytes = KI_TEST::ReadGoldenText( src );
2801 const std::string sourceModel = serializeNative( &board );
2802
2803 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
2804 {
2805 const bool preTen = target.m_boardVersion == kicad9.m_boardVersion;
2806
2807 for( bool drop : { false, true } )
2808 {
2809 BOOST_TEST_CONTEXT( target.m_id.ToStdString() << " drop=" << drop )
2810 {
2811 const auto preview = ClassifyBoardForDowngrade( &board, target, drop );
2812 BOOST_CHECK( !preview.IsBlocked() );
2813 BOOST_CHECK_EQUAL( preview.Count( DOWNGRADE_BUCKET::LOWER ), 0 );
2814 BOOST_CHECK_EQUAL( preview.Count( DOWNGRADE_BUCKET::DROP ), preTen ? 1 : 0 );
2815 BOOST_CHECK_EQUAL( serializeNative( &board ), sourceModel );
2816 const std::string dest =
2817 ( tmp.GetPath()
2818 / ( target.m_id.ToStdString() + ( drop ? "-drop.kicad_pcb" : "-lower.kicad_pcb" ) ) )
2819 .string();
2820 COMPATIBILITY_REPORT report;
2821 wxString unsupported;
2823 ExportBoardToOlderVersion( src, dest, target, report, &unsupported, wxEmptyString, drop ) );
2824 BOOST_CHECK( unsupported.IsEmpty() );
2825 BOOST_CHECK( !report.IsBlocked() );
2827 BOOST_CHECK_EQUAL( report.Count( DOWNGRADE_BUCKET::DROP ), preTen ? 1 : 0 );
2828 BOOST_REQUIRE_EQUAL( report.Entries().size(), preTen ? 1u : 0u );
2829
2830 if( preTen )
2831 checkFeature( report, wxT( "Time-domain tuning patterns" ), DOWNGRADE_BUCKET::DROP, 1 );
2832
2833 const std::string outputBytes = KI_TEST::ReadGoldenText( dest );
2834 BOOST_CHECK( FindUnsupportedBoardToken( wxString::FromUTF8( outputBytes ), target ).IsEmpty() );
2835
2836 if( preTen )
2837 {
2838 for( const char* token : { "(is_time_domain", "(target_delay", "(last_tuning_length" } )
2839 BOOST_CHECK( outputBytes.find( token ) == std::string::npos );
2840
2841 STRING_FORMATTER quoting;
2842 BOOST_CHECK( outputBytes.find( "(last_tuning " + quoting.Quotew( tuningInfo ) + ")" )
2843 != std::string::npos );
2844 }
2845 else
2846 {
2847 BOOST_CHECK( outputBytes.find( "(is_time_domain yes)" ) != std::string::npos );
2848 BOOST_CHECK( outputBytes.find( "(last_tuning_length " ) != std::string::npos );
2849 BOOST_CHECK( outputBytes.find( "(last_tuning " ) == std::string::npos );
2850 }
2851
2852 std::unique_ptr<BOARD> exported( io.LoadBoard( dest, nullptr ) );
2853 BOOST_REQUIRE( exported );
2854 BOOST_CHECK_EQUAL( exported->GetFileFormatVersionAtLoad(), target.m_boardVersion );
2855 BOOST_REQUIRE_EQUAL( exported->Generators().size(), preTen ? 1u : 2u );
2856 BOOST_REQUIRE_EQUAL( exported->Groups().size(), 1u );
2857 BOOST_CHECK( exported->GroupsSanityCheck().IsEmpty() );
2858 PCB_GROUP* exportedOuter = *exported->Groups().begin();
2859 BOOST_CHECK( exportedOuter->m_Uuid == outer->m_Uuid );
2860 std::set<KIID> expectedOuter{ length->m_Uuid };
2861
2862 if( preTen )
2863 expectedOuter.insert( timeMembers.begin(), timeMembers.end() );
2864 else
2865 expectedOuter.insert( time->m_Uuid );
2866
2867 BOOST_CHECK( memberIds( *exportedOuter ) == expectedOuter );
2868 BOOST_REQUIRE_EQUAL( exported->Tracks().size(), copper.size() );
2869
2870 for( const PCB_TRACK* track : exported->Tracks() )
2871 {
2872 const auto found = copper.find( track->m_Uuid );
2873 BOOST_REQUIRE( found != copper.end() );
2874 const PCB_TRACK* original = found->second;
2875 BOOST_CHECK( track->Type() == original->Type() );
2876 BOOST_CHECK( track->GetStart() == original->GetStart() );
2877 BOOST_CHECK( track->GetEnd() == original->GetEnd() );
2878 BOOST_CHECK( track->GetLayerSet() == original->GetLayerSet() );
2879 BOOST_CHECK_EQUAL( track->GetWidth(), original->GetWidth() );
2880 BOOST_CHECK_EQUAL( track->GetNetname().ToStdString(), original->GetNetname().ToStdString() );
2881
2882 if( const PCB_ARC* arc = dynamic_cast<const PCB_ARC*>( original ) )
2883 {
2884 const PCB_ARC* exportedArc = dynamic_cast<const PCB_ARC*>( track );
2885 BOOST_REQUIRE( exportedArc );
2886 BOOST_CHECK( exportedArc->GetMid() == arc->GetMid() );
2887 }
2888
2889 if( timeMembers.count( track->m_Uuid ) && preTen )
2890 BOOST_CHECK( track->GetParentGroup() == static_cast<EDA_GROUP*>( exportedOuter ) );
2891 else if( track->m_Uuid == control->m_Uuid )
2892 BOOST_CHECK( !track->GetParentGroup() );
2893 }
2894
2895 for( PCB_GENERATOR* generator : exported->Generators() )
2896 {
2897 auto pattern = dynamic_cast<PCB_TUNING_PATTERN*>( generator );
2898 BOOST_REQUIRE( pattern );
2899 BOOST_CHECK( pattern->GetParentGroup() == static_cast<EDA_GROUP*>( exportedOuter ) );
2900 BOOST_CHECK( pattern->m_Uuid == length->m_Uuid || ( !preTen && pattern->m_Uuid == time->m_Uuid ) );
2901 const bool isTime = pattern->m_Uuid == time->m_Uuid;
2902 BOOST_CHECK( pattern->GetSettings().m_isTimeDomain == isTime );
2903 BOOST_CHECK( memberIds( *pattern ) == ( isTime ? timeMembers : lengthMembers ) );
2904 const PCB_TUNING_PATTERN* original = isTime ? time : length;
2905 const auto properties = pattern->GetProperties();
2906 const auto originalProperties = original->GetProperties();
2907
2908 for( const char* key :
2909 { "target_length", "target_length_min", "target_length_max", "target_skew", "target_skew_min",
2910 "target_skew_max", "min_spacing", "max_amplitude", "min_amplitude" } )
2911 {
2912 double value = 0;
2913 double originalValue = 0;
2914 BOOST_REQUIRE( properties.get_to( key, value ) );
2915 BOOST_REQUIRE( originalProperties.get_to( key, originalValue ) );
2916 BOOST_CHECK_EQUAL( value, originalValue );
2917 }
2918
2919 if( !preTen )
2920 {
2921 for( const char* key :
2922 { "target_delay", "target_delay_min", "target_delay_max", "last_tuning_length" } )
2923 {
2924 double value = 0;
2925 double originalValue = 0;
2926 BOOST_REQUIRE( properties.get_to( key, value ) );
2927 BOOST_REQUIRE( originalProperties.get_to( key, originalValue ) );
2928 BOOST_CHECK_EQUAL( value, originalValue );
2929 }
2930 }
2931 }
2932
2933 BOOST_CHECK_EQUAL( serializeNative( &board ), sourceModel );
2934 BOOST_CHECK_EQUAL( KI_TEST::ReadGoldenText( src ), sourceBytes );
2935 BOOST_CHECK_EQUAL( KI_TEST::ReadGoldenText( lengthPath ), lengthBytes );
2936 BOOST_CHECK_EQUAL( KI_TEST::ReadGoldenText( timePath ), timeBytes );
2937 }
2938 }
2939 }
2940}
2941
2942
2943BOOST_AUTO_TEST_CASE( UnreviewedTuningPropertiesBlockBothTargetsAndPolicies )
2944{
2945 class UNREVIEWED_TUNING_PATTERN : public PCB_TUNING_PATTERN
2946 {
2947 public:
2949
2950 const STRING_ANY_MAP GetProperties() const override
2951 {
2953 properties.set( "future_tuning_setting", true );
2954 return properties;
2955 }
2956 };
2957
2958 BOARD board;
2959 auto pattern = new UNREVIEWED_TUNING_PATTERN( &board );
2960 auto track = new PCB_TRACK( &board );
2961 track->SetStart( VECTOR2I( 1000000, 2000000 ) );
2962 track->SetEnd( VECTOR2I( 3000000, 2000000 ) );
2963 board.Add( track );
2964 board.Add( pattern );
2965 pattern->AddItem( track );
2966
2967 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
2968 {
2969 for( bool drop : { false, true } )
2970 {
2971 const auto report = ClassifyBoardForDowngrade( &board, target, drop );
2972 checkFeature( report, wxT( "Unreviewed tuning properties" ), DOWNGRADE_BUCKET::BLOCK, 1 );
2973 BOOST_REQUIRE_EQUAL( board.Generators().size(), 1u );
2974 BOOST_CHECK( track->GetParentGroup() == static_cast<EDA_GROUP*>( pattern ) );
2975 }
2976 }
2977
2978 KI_TEST::TEMPORARY_DIRECTORY tmp( "kicad_qa_unreviewed_tuning" );
2979
2980 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
2981 {
2982 const wxString path = ( tmp.GetPath() / ( target.m_id.ToStdString() + ".kicad_pcb" ) ).wstring();
2983 BOOST_CHECK_THROW( SaveBoardForTarget( &board, path, target ), IO_ERROR );
2984 }
2985}
2986
2987
2988BOOST_AUTO_TEST_CASE( HistoricalTuningWritersRejectUnsafeDirectSerialization )
2989{
2990 BOARD board;
2991 auto pattern = new PCB_TUNING_PATTERN( &board );
2992 pattern->SetTargetDelay( 100000 );
2993 auto track = new PCB_TRACK( &board );
2994 track->SetStart( VECTOR2I( 1000000, 2000000 ) );
2995 track->SetEnd( VECTOR2I( 3000000, 2000000 ) );
2996 board.Add( track );
2997 board.Add( pattern );
2998 pattern->AddItem( track );
2999 KI_TEST::TEMPORARY_DIRECTORY tmp( "kicad_qa_direct_time_tuning" );
3000 BOOST_CHECK_THROW( SaveBoardForTarget( &board, ( tmp.GetPath() / "unsafe.kicad_pcb" ).wstring(), kicad9 ),
3001 IO_ERROR );
3002
3003 for( const char* token :
3004 { "is_time_domain", "target_delay", "target_delay_min", "target_delay_max", "last_tuning_length" } )
3005 {
3006 const wxString value = wxString::FromUTF8( "(kicad_pcb (generated (" + std::string( token ) + " 0)))" );
3007 BOOST_CHECK_EQUAL( FindUnsupportedBoardToken( value, kicad9 ), wxString::FromUTF8( token ) );
3008 BOOST_CHECK( FindUnsupportedBoardToken( value, kicad10 ).IsEmpty() );
3009 }
3010}
3011
3012
3013BOOST_AUTO_TEST_CASE( HistoricalWritersRejectDirectViaGeneratorsAtomically )
3014{
3015 KI_TEST::TEMPORARY_DIRECTORY tmp( "kicad_qa_direct_via_generators" );
3016
3017 for( bool stack : { false, true } )
3018 {
3019 BOARD board;
3020
3021 if( stack )
3022 board.Add( new PCB_VIA_STACK( &board ) );
3023 else
3024 board.Add( new PCB_VIA_STITCH( &board ) );
3025
3026 const std::string kind = stack ? "via-stack" : "via-stitch";
3027
3028 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
3029 {
3030 BOOST_TEST_CONTEXT( kind << " target=" << target.m_id.ToStdString() )
3031 {
3032 const auto absent = tmp.GetPath() / ( kind + "-" + target.m_id.ToStdString() + "-absent.kicad_pcb" );
3033 BOOST_CHECK_THROW( SaveBoardForTarget( &board, absent.wstring(), target ), IO_ERROR );
3034 BOOST_CHECK( !wxFileExists( absent.wstring() ) );
3035
3036 const auto existing =
3037 tmp.GetPath() / ( kind + "-" + target.m_id.ToStdString() + "-existing.kicad_pcb" );
3038
3039 {
3040 std::ofstream sentinel( existing, std::ios::binary );
3041 sentinel << "existing destination";
3042 BOOST_REQUIRE( sentinel.good() );
3043 }
3044
3045 BOOST_CHECK_THROW( SaveBoardForTarget( &board, existing.wstring(), target ), IO_ERROR );
3046 BOOST_CHECK_EQUAL( KI_TEST::ReadGoldenText( existing.string() ), "existing destination" );
3047 }
3048 }
3049 }
3050}
3051
3052
3053BOOST_AUTO_TEST_CASE( NativeGroupPlacementFixtureKeepsGeometryAndOwnershipForBothPolicies )
3054{
3055 SETTINGS_MANAGER settingsManager;
3056 const std::string src =
3057 KI_TEST::GetPcbnewTestDataDir() + "../downgrade/regressions/group_placement/group_placement.kicad_pcb";
3058 wxFileName projectFile( wxString::FromUTF8( src ) );
3059 projectFile.SetExt( wxT( "kicad_pro" ) );
3060 BOOST_REQUIRE( wxFileExists( projectFile.GetFullPath() ) );
3061 BOOST_REQUIRE( settingsManager.LoadProject( projectFile.GetFullPath(), false ) );
3062 const std::string sourceBytes = KI_TEST::ReadGoldenText( src );
3064 std::unique_ptr<BOARD> source( io.LoadBoard( src, nullptr ) );
3066 BOOST_REQUIRE_EQUAL( source->Zones().size(), 7u );
3067 BOOST_REQUIRE_EQUAL( source->Groups().size(), 4u );
3068 BOOST_REQUIRE_EQUAL( source->Footprints().size(), 4u );
3069 BOOST_REQUIRE( source->GroupsSanityCheck().IsEmpty() );
3070 std::map<KIID, const PCB_TEXT*> sourceNotes;
3071
3072 for( const BOARD_ITEM* drawing : source->Drawings() )
3073 {
3074 if( const PCB_TEXT* note = dynamic_cast<const PCB_TEXT*>( drawing ) )
3075 BOOST_REQUIRE( sourceNotes.emplace( note->m_Uuid, note ).second );
3076 }
3077
3078 BOOST_REQUIRE_EQUAL( sourceNotes.size(), 1u );
3079 std::map<KIID, const ZONE*> sourceZones;
3080 int placementAreas = 0;
3081 int filledOutlines = 0;
3082
3083 for( const ZONE* zone : source->Zones() )
3084 {
3085 BOOST_REQUIRE( sourceZones.emplace( zone->m_Uuid, zone ).second );
3086
3087 if( zone->GetPlacementAreaSourceType() == PLACEMENT_SOURCE_T::GROUP_PLACEMENT )
3088 {
3089 ++placementAreas;
3090 BOOST_REQUIRE( zone->GetPlacementAreaEnabled() );
3091 BOOST_REQUIRE( !zone->GetPlacementAreaSource().IsEmpty() );
3092 }
3093
3094 for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
3095 filledOutlines += zone->GetFilledPolysList( layer )->OutlineCount();
3096 }
3097
3098 BOOST_REQUIRE_EQUAL( placementAreas, 4 );
3099 BOOST_REQUIRE_GT( filledOutlines, 0 );
3100 const auto memberIds = []( const PCB_GROUP& aGroup )
3101 {
3102 std::set<KIID> ids;
3103
3104 for( const EDA_ITEM* member : aGroup.GetItems() )
3105 BOOST_REQUIRE( ids.insert( member->m_Uuid ).second );
3106
3107 return ids;
3108 };
3109 const auto checkPolygons = []( const SHAPE_POLY_SET& aOriginal, const SHAPE_POLY_SET& aExported )
3110 {
3111 BOOST_REQUIRE_EQUAL( aExported.OutlineCount(), aOriginal.OutlineCount() );
3112
3113 for( int outline = 0; outline < aOriginal.OutlineCount(); ++outline )
3114 {
3115 BOOST_CHECK( aExported.COutline( outline ).CompareGeometry( aOriginal.COutline( outline ) ) );
3116 BOOST_REQUIRE_EQUAL( aExported.HoleCount( outline ), aOriginal.HoleCount( outline ) );
3117
3118 for( int hole = 0; hole < aOriginal.HoleCount( outline ); ++hole )
3119 BOOST_CHECK( aExported.CHole( outline, hole ).CompareGeometry( aOriginal.CHole( outline, hole ) ) );
3120 }
3121 };
3122 const auto serializeNative = []( BOARD* aBoard )
3123 {
3124 STRING_FORMATTER formatter;
3125 PCB_IO_KICAD_SEXPR serializer;
3126 serializer.FormatBoardToFormatter( &formatter, aBoard );
3127 return formatter.GetString();
3128 };
3129 const std::string sourceModel = serializeNative( source.get() );
3130 KI_TEST::TEMPORARY_DIRECTORY tmp( "kicad_qa_native_group_placement" );
3131 BOOST_CHECK_THROW( SaveBoardForTarget( source.get(), ( tmp.GetPath() / "unsafe.kicad_pcb" ).wstring(), kicad9 ),
3132 IO_ERROR );
3133 BOOST_CHECK_EQUAL( serializeNative( source.get() ), sourceModel );
3134
3135 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
3136 {
3137 const bool preTen = target.m_boardVersion == kicad9.m_boardVersion;
3138
3139 for( bool drop : { false, true } )
3140 {
3141 BOOST_TEST_CONTEXT( target.m_id.ToStdString() << " drop=" << drop )
3142 {
3143 const auto preview = ClassifyBoardForDowngrade( source.get(), target, drop );
3144 BOOST_CHECK( !preview.IsBlocked() );
3145 BOOST_CHECK_EQUAL( preview.Count( DOWNGRADE_BUCKET::DROP ), preTen ? 4 : 0 );
3146 BOOST_CHECK_EQUAL( preview.Count( DOWNGRADE_BUCKET::LOWER ), 0 );
3147 BOOST_CHECK_EQUAL( serializeNative( source.get() ), sourceModel );
3148 const std::string dest =
3149 ( tmp.GetPath()
3150 / ( target.m_id.ToStdString() + ( drop ? "-drop.kicad_pcb" : "-lower.kicad_pcb" ) ) )
3151 .string();
3152 COMPATIBILITY_REPORT report;
3153 wxString unsupported;
3155 ExportBoardToOlderVersion( src, dest, target, report, &unsupported, wxEmptyString, drop ) );
3156 BOOST_CHECK( unsupported.IsEmpty() );
3157 BOOST_CHECK( !report.IsBlocked() );
3158 BOOST_CHECK_EQUAL( report.Count( DOWNGRADE_BUCKET::DROP ), preTen ? 4 : 0 );
3160 BOOST_REQUIRE_EQUAL( report.Entries().size(), preTen ? 1u : 0u );
3161
3162 if( preTen )
3163 checkFeature( report, wxT( "Group placement areas" ), DOWNGRADE_BUCKET::DROP, 4 );
3164
3165 const std::string outputBytes = KI_TEST::ReadGoldenText( dest );
3166 BOOST_CHECK( FindUnsupportedBoardToken( wxString::FromUTF8( outputBytes ), target ).IsEmpty() );
3167 std::unique_ptr<BOARD> exported( io.LoadBoard( dest, nullptr ) );
3168 BOOST_REQUIRE( exported );
3169 BOOST_CHECK_EQUAL( exported->GetFileFormatVersionAtLoad(), target.m_boardVersion );
3170 BOOST_REQUIRE_EQUAL( exported->Zones().size(), source->Zones().size() );
3171 BOOST_REQUIRE_EQUAL( exported->Groups().size(), source->Groups().size() );
3172 BOOST_REQUIRE_EQUAL( exported->Footprints().size(), source->Footprints().size() );
3173 BOOST_CHECK( exported->GroupsSanityCheck().IsEmpty() );
3174
3175 for( const auto& [id, original] : sourceNotes )
3176 {
3177 const auto found = std::find_if( exported->Drawings().begin(), exported->Drawings().end(),
3178 [&]( const BOARD_ITEM* aItem )
3179 {
3180 return aItem->m_Uuid == id;
3181 } );
3182 BOOST_REQUIRE( found != exported->Drawings().end() );
3183 const PCB_TEXT* note = dynamic_cast<const PCB_TEXT*>( *found );
3184 BOOST_REQUIRE( note );
3185 BOOST_CHECK( note->GetText() == original->GetText() );
3186 BOOST_CHECK( note->GetPosition() == original->GetPosition() );
3187 BOOST_CHECK( note->GetLayer() == original->GetLayer() );
3188 }
3189
3190 for( const ZONE* zone : exported->Zones() )
3191 {
3192 const auto found = sourceZones.find( zone->m_Uuid );
3193 BOOST_REQUIRE( found != sourceZones.end() );
3194 const ZONE* original = found->second;
3195 BOOST_CHECK( zone->GetZoneName() == original->GetZoneName() );
3196 BOOST_CHECK( zone->GetLayerSet() == original->GetLayerSet() );
3197 BOOST_CHECK( zone->GetIsRuleArea() == original->GetIsRuleArea() );
3198 BOOST_CHECK( zone->GetDoNotAllowTracks() == original->GetDoNotAllowTracks() );
3199 BOOST_CHECK( zone->GetDoNotAllowVias() == original->GetDoNotAllowVias() );
3200 BOOST_CHECK( zone->GetDoNotAllowPads() == original->GetDoNotAllowPads() );
3201 BOOST_CHECK( zone->GetDoNotAllowZoneFills() == original->GetDoNotAllowZoneFills() );
3202 BOOST_CHECK( zone->GetDoNotAllowFootprints() == original->GetDoNotAllowFootprints() );
3203 BOOST_CHECK( zone->GetNetname() == original->GetNetname() );
3204 BOOST_CHECK( zone->IsFilled() == original->IsFilled() );
3205 checkPolygons( original->GetBoardOutline(), zone->GetBoardOutline() );
3206
3207 for( PCB_LAYER_ID layer : original->GetLayerSet().Seq() )
3208 {
3209 const auto originalFill = original->GetFilledPolysList( layer );
3210 const auto exportedFill = zone->GetFilledPolysList( layer );
3211 checkPolygons( *originalFill, *exportedFill );
3212
3213 for( int outline = 0; outline < originalFill->OutlineCount(); ++outline )
3214 BOOST_CHECK( zone->IsIsland( layer, outline ) == original->IsIsland( layer, outline ) );
3215 }
3216
3218 {
3219 BOOST_CHECK( !zone->GetPlacementAreaEnabled() );
3220 BOOST_CHECK( zone->GetPlacementAreaSource().IsEmpty() );
3221 BOOST_CHECK( zone->GetPlacementAreaSourceType() == PLACEMENT_SOURCE_T::SHEETNAME );
3222 }
3223 else
3224 {
3225 BOOST_CHECK( zone->GetPlacementAreaEnabled() == original->GetPlacementAreaEnabled() );
3226 BOOST_CHECK( zone->GetPlacementAreaSource() == original->GetPlacementAreaSource() );
3227 BOOST_CHECK( zone->GetPlacementAreaSourceType() == original->GetPlacementAreaSourceType() );
3228 }
3229 }
3230
3231 for( const PCB_GROUP* group : exported->Groups() )
3232 {
3233 const auto original = std::find_if( source->Groups().begin(), source->Groups().end(),
3234 [&]( const PCB_GROUP* aGroup )
3235 {
3236 return aGroup->m_Uuid == group->m_Uuid;
3237 } );
3238 BOOST_REQUIRE( original != source->Groups().end() );
3239 BOOST_CHECK( group->GetName() == ( *original )->GetName() );
3240 BOOST_CHECK( memberIds( *group ) == memberIds( **original ) );
3241 }
3242
3243 std::map<KIID, const PAD*> sourcePads;
3244
3245 for( const PAD* pad : source->GetPads() )
3246 BOOST_REQUIRE( sourcePads.emplace( pad->m_Uuid, pad ).second );
3247
3248 BOOST_REQUIRE_EQUAL( sourcePads.size(), 8u );
3249 BOOST_REQUIRE_EQUAL( exported->GetPads().size(), sourcePads.size() );
3250
3251 for( const PAD* pad : exported->GetPads() )
3252 {
3253 const auto found = sourcePads.find( pad->m_Uuid );
3254 BOOST_REQUIRE( found != sourcePads.end() );
3255 const PAD* original = found->second;
3256 BOOST_CHECK( pad->GetPosition() == original->GetPosition() );
3257 BOOST_CHECK( pad->GetOrientation() == original->GetOrientation() );
3258 BOOST_CHECK( pad->GetLayerSet() == original->GetLayerSet() );
3259 BOOST_CHECK( pad->GetNetname() == original->GetNetname() );
3260 BOOST_CHECK( pad->GetShape( F_Cu ) == original->GetShape( F_Cu ) );
3261 BOOST_CHECK( pad->GetSize( F_Cu ) == original->GetSize( F_Cu ) );
3262 BOOST_CHECK_EQUAL( pad->GetRoundRectRadiusRatio( F_Cu ),
3263 original->GetRoundRectRadiusRatio( F_Cu ) );
3264 }
3265
3266 BOOST_CHECK_EQUAL( serializeNative( source.get() ), sourceModel );
3267 BOOST_CHECK_EQUAL( KI_TEST::ReadGoldenText( src ), sourceBytes );
3268 }
3269 }
3270 }
3271}
3272
3273
3274BOOST_AUTO_TEST_CASE( GroupPlacementLinkageUsesTheSameFootprintPolicy )
3275{
3276 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
3277 {
3278 for( bool drop : { false, true } )
3279 {
3280 BOARD board;
3281 auto footprint = new FOOTPRINT( &board );
3282 auto zone = new ZONE( footprint );
3283 zone->SetIsRuleArea( true );
3284 zone->SetPlacementAreaEnabled( true );
3285 zone->SetPlacementAreaSourceType( PLACEMENT_SOURCE_T::GROUP_PLACEMENT );
3286 zone->SetPlacementAreaSource( wxT( "Repeated group" ) );
3287 zone->SetDoNotAllowTracks( true );
3288 footprint->Add( zone );
3289 board.Add( footprint );
3290 const auto report = ClassifyBoardForDowngrade( &board, target, drop );
3291 BOOST_CHECK_EQUAL( report.Count( DOWNGRADE_BUCKET::DROP ), target.m_id == kicad9.m_id ? 1 : 0 );
3292 BOOST_CHECK( zone->GetPlacementAreaEnabled() );
3293 DowngradeFootprintInPlace( footprint, target, drop );
3294 BOOST_CHECK( zone->GetIsRuleArea() );
3295 BOOST_CHECK( zone->GetDoNotAllowTracks() );
3296 BOOST_CHECK( zone->GetPlacementAreaEnabled() == ( target.m_id == kicad10.m_id ) );
3297 BOOST_CHECK( zone->GetPlacementAreaSource()
3298 == ( target.m_id == kicad10.m_id ? wxT( "Repeated group" ) : wxEmptyString ) );
3299 }
3300 }
3301}
3302
3303
3304BOOST_AUTO_TEST_CASE( GroupPlacementGuardIgnoresOrdinaryGroupsAndQuotedText )
3305{
3306 BOOST_CHECK( FindUnsupportedBoardToken( wxT( "(zone (placement (group \"SourceA\")))" ), kicad9 )
3307 == wxT( "placement/group" ) );
3308 BOOST_CHECK( FindUnsupportedBoardToken( wxT( "(zone (placement (group \"SourceA\")))" ), kicad10 ).IsEmpty() );
3309 BOOST_CHECK( FindUnsupportedBoardToken( wxT( "(group \"Ordinary\" (members \"uuid\"))" ), kicad9 ).IsEmpty() );
3310 BOOST_CHECK( FindUnsupportedBoardToken( wxT( "(gr_text \"(zone (placement (group foo)))\")" ), kicad9 ).IsEmpty() );
3311}
3312
3313
3314BOOST_AUTO_TEST_CASE( InertGroupPlacementMetadataDoesNotRequireConsent )
3315{
3316 for( bool drop : { false, true } )
3317 {
3318 BOARD board;
3319 auto emptyRuleArea = new ZONE( &board );
3320 emptyRuleArea->SetIsRuleArea( true );
3321 emptyRuleArea->SetPlacementAreaSourceType( PLACEMENT_SOURCE_T::GROUP_PLACEMENT );
3322 board.Add( emptyRuleArea );
3323 auto ordinaryZone = new ZONE( &board );
3324 ordinaryZone->SetPlacementAreaSourceType( PLACEMENT_SOURCE_T::GROUP_PLACEMENT );
3325 ordinaryZone->SetPlacementAreaEnabled( true );
3326 ordinaryZone->SetPlacementAreaSource( wxT( "Unused by ordinary copper zones" ) );
3327 board.Add( ordinaryZone );
3328 BOOST_CHECK( !ClassifyBoardForDowngrade( &board, kicad9, drop ).IsLossy() );
3329 DowngradeBoardInPlace( &board, kicad9, drop );
3330 BOOST_CHECK( emptyRuleArea->GetIsRuleArea() );
3331 BOOST_CHECK( !ordinaryZone->GetIsRuleArea() );
3332 BOOST_CHECK( emptyRuleArea->GetPlacementAreaSourceType() == PLACEMENT_SOURCE_T::SHEETNAME );
3333 BOOST_CHECK( ordinaryZone->GetPlacementAreaSourceType() == PLACEMENT_SOURCE_T::SHEETNAME );
3334 }
3335}
3336
3337
3338BOOST_AUTO_TEST_CASE( UnreviewedGeneratorKindBlocksBothPolicies )
3339{
3340 class FUTURE_GENERATOR : public PCB_VIA_STITCH
3341 {
3342 public:
3343 explicit FUTURE_GENERATOR( BOARD* aBoard ) :
3344 PCB_VIA_STITCH( aBoard )
3345 {
3346 }
3347 wxString GetGeneratorType() const override { return wxT( "future_generator" ); }
3348 };
3349
3350 BOARD board;
3351 board.Add( new FUTURE_GENERATOR( &board ) );
3352
3353 for( bool drop : { false, true } )
3354 BOOST_CHECK( ClassifyBoardForDowngrade( &board, kicad10, drop ).IsBlocked() );
3355}
3356
3357
3358BOOST_AUTO_TEST_CASE( LineEndingsLowerPrintedInkOrDropOnlyEmbellishments )
3359{
3360 for( bool drop : { false, true } )
3361 {
3362 BOARD board;
3363 FOOTPRINT* fp = new FOOTPRINT( &board );
3364 board.Add( fp );
3365 std::vector<PCB_SHAPE*> shapes;
3366 double expectedArea = 0;
3367
3368 for( BOARD_ITEM_CONTAINER* parent :
3369 { static_cast<BOARD_ITEM_CONTAINER*>( &board ), static_cast<BOARD_ITEM_CONTAINER*>( fp ) } )
3370 {
3371 PCB_SHAPE* shape = new PCB_SHAPE( parent, SHAPE_T::SEGMENT );
3372 shape->SetStart( VECTOR2I( 0, 0 ) );
3373 shape->SetEnd( VECTOR2I( 10000000, 0 ) );
3374 shape->SetWidth( 200000 );
3375 shape->SetLayer( F_SilkS );
3376 shape->SetEndEnding( LINE_ENDING( LINE_ENDING_STYLE::ARROW, 2000000, 1000000 ) );
3377 parent->Add( shape );
3378 shapes.push_back( shape );
3379 SHAPE_POLY_SET ink;
3380 shape->TransformWithLineEndingsToPolygon( ink, 0, 5000, ERROR_INSIDE );
3381 ink.Simplify();
3382 expectedArea += ink.Area();
3383 }
3384
3385 const auto report = ClassifyBoardForDowngrade( &board, kicad10, drop );
3386 checkFeature( report, wxT( "Graphic line endings" ), drop ? DOWNGRADE_BUCKET::DROP : DOWNGRADE_BUCKET::LOWER,
3387 2 );
3388 DowngradeBoardInPlace( &board, kicad10, drop );
3389
3390 if( drop )
3391 {
3392 for( PCB_SHAPE* shape : shapes )
3393 {
3394 BOOST_CHECK( shape->GetShape() == SHAPE_T::SEGMENT );
3395 BOOST_CHECK( shape->GetEnd() == VECTOR2I( 10000000, 0 ) );
3396 BOOST_CHECK( shape->GetEndEndingStyle() == LINE_ENDING_STYLE::NONE );
3397 }
3398 }
3399 else
3400 {
3401 double area = 0;
3402
3403 for( BOARD_ITEM* item : board.Drawings() )
3404 area += static_cast<PCB_SHAPE*>( item )->GetPolyShape().Area();
3405
3406 for( BOARD_ITEM* item : fp->GraphicalItems() )
3407 area += static_cast<PCB_SHAPE*>( item )->GetPolyShape().Area();
3408
3409 BOOST_CHECK_CLOSE( area, expectedArea, 0.05 );
3410 }
3411 }
3412}
3413
3414
3415BOOST_AUTO_TEST_CASE( DashedEndedLinesBlockApproximationButAllowDroppingEndings )
3416{
3417 BOARD board;
3418 PCB_SHAPE* line = new PCB_SHAPE( &board, SHAPE_T::SEGMENT );
3419 line->SetStart( VECTOR2I( 0, 0 ) );
3420 line->SetEnd( VECTOR2I( 10000000, 0 ) );
3421 line->SetWidth( 200000 );
3424 board.Add( line );
3425 BOOST_CHECK( ClassifyBoardForDowngrade( &board, kicad10 ).IsBlocked() );
3426 BOOST_CHECK( !ClassifyBoardForDowngrade( &board, kicad10, true ).IsBlocked() );
3427 DowngradeBoardInPlace( &board, kicad10, true );
3428 BOOST_CHECK( line->GetLineStyle() == LINE_STYLE::DASH );
3429 BOOST_CHECK( line->GetEndEndingStyle() == LINE_ENDING_STYLE::NONE );
3430}
3431
3432
3433BOOST_AUTO_TEST_CASE( CustomPropertiesBakeOnlyAffectedVariablesBeforeRemoval )
3434{
3435 for( bool footprintOnly : { false, true } )
3436 {
3437 BOARD board;
3438 FOOTPRINT* fp = new FOOTPRINT( &board );
3439 fp->SetReference( wxT( "42" ) );
3440 fp->SetCustomProperty( wxT( "Serial" ), wxT( "42" ) );
3441 fp->Value().SetText( wxT( "${PROPERTY.Reference}/${REFERENCE}" ) );
3442 fp->Reference().SetCustomProperty( wxT( "Owner" ), wxT( "Engineering" ) );
3443 PCB_TEXT* escaped = new PCB_TEXT( fp );
3444 escaped->SetText( wxT( "\\${PROPERTY.Serial}" ) );
3445 fp->Add( escaped );
3446 PCB_TEXT* expression = new PCB_TEXT( fp );
3447 expression->SetText( wxT( "@{${PROPERTY.Reference}+1}/${REFERENCE}" ) );
3448 fp->Add( expression );
3449 board.Add( fp );
3450 const auto report = ClassifyBoardForDowngrade( &board, kicad10 );
3451 checkFeature( report, wxT( "Custom properties" ), DOWNGRADE_BUCKET::DROP, 2 );
3452 BOOST_CHECK( !report.IsBlocked() );
3453
3454 if( footprintOnly )
3456 else
3457 DowngradeBoardInPlace( &board, kicad10 );
3458
3459 BOOST_CHECK( !fp->HasCustomProperties() );
3460 BOOST_CHECK( !fp->Reference().HasCustomProperties() );
3461 BOOST_CHECK_EQUAL( fp->Value().GetText(), wxString( "42/${REFERENCE}" ) );
3462 BOOST_CHECK_EQUAL( expression->GetText(), wxString( "43/${REFERENCE}" ) );
3463 BOOST_CHECK_EQUAL( escaped->GetText(), wxString( "\\${PROPERTY.Serial}" ) );
3464 }
3465}
3466
3467
3468BOOST_AUTO_TEST_CASE( UnresolvedPropertyVariablesBlockBothPolicies )
3469{
3470 BOARD board;
3471 FOOTPRINT* fp = new FOOTPRINT( &board );
3472 fp->Value().SetText( wxT( "${PROPERTY.Missing}" ) );
3473 board.Add( fp );
3474
3475 for( bool drop : { false, true } )
3476 BOOST_CHECK( ClassifyBoardForDowngrade( &board, kicad10, drop ).IsBlocked() );
3477}
3478
3479
3480BOOST_AUTO_TEST_CASE( UsedProjectVariablesBakeInFootprintContextAndUnusedDefinitionsDoNotBlock )
3481{
3482 KI_TEST::TEMPORARY_DIRECTORY tmp( "kicad_qa_property_dependencies" );
3483 const wxString projectPath = ( tmp.GetPath() / "source.kicad_pro" ).string();
3484
3485 {
3486 std::ofstream project( projectPath.ToStdString() );
3487 project << R"({"meta":{"version":4},"text_variables":{"SERIAL":"${PROPERTY.Reference}","ALIAS":"${SERIAL}","UNUSED":"${PROPERTY.Missing}"}})";
3488 }
3489
3490 SETTINGS_MANAGER settings;
3491 BOOST_REQUIRE( settings.LoadProject( projectPath, false ) );
3492 BOARD board;
3493 board.SetProject( settings.GetProject( projectPath ), true );
3494
3495 std::vector<FOOTPRINT*> footprints;
3496
3497 for( const wxString& serial : { wxT( "42" ), wxT( "73" ) } )
3498 {
3499 FOOTPRINT* fp = new FOOTPRINT( &board );
3500 fp->SetReference( serial );
3501 fp->SetCustomProperty( wxT( "Serial" ), serial );
3502 fp->Value().SetText( wxT( "${ALIAS}/${REFERENCE}" ) );
3503 board.Add( fp );
3504 footprints.push_back( fp );
3505 }
3506
3507 BOOST_CHECK( !ClassifyBoardForDowngrade( &board, kicad10 ).IsBlocked() );
3508 DowngradeBoardInPlace( &board, kicad10 );
3509 BOOST_CHECK_EQUAL( footprints[0]->Value().GetText(), wxString( "42/${REFERENCE}" ) );
3510 BOOST_CHECK_EQUAL( footprints[1]->Value().GetText(), wxString( "73/${REFERENCE}" ) );
3511
3512 footprints[0]->Value().SetText( wxT( "${UNUSED}" ) );
3513 BOOST_CHECK( ClassifyBoardForDowngrade( &board, kicad10 ).IsBlocked() );
3514 BOOST_CHECK( ClassifyBoardForDowngrade( &board, kicad10, true ).IsBlocked() );
3515}
3516
3517
3518BOOST_AUTO_TEST_CASE( CachedBoardVariableDependenciesBakeInFootprintContext )
3519{
3520 BOARD board;
3521 board.SetProperties( { { wxT( "SERIAL" ), wxT( "${PROPERTY.Reference}" ) },
3522 { wxT( "UNUSED" ), wxT( "${PROPERTY.Missing}" ) } } );
3523 FOOTPRINT* fp = new FOOTPRINT( &board );
3524 fp->SetReference( wxT( "42" ) );
3525 fp->Value().SetText( wxT( "${SERIAL}/${REFERENCE}" ) );
3526 board.Add( fp );
3527 BOOST_CHECK( !ClassifyBoardForDowngrade( &board, kicad10 ).IsBlocked() );
3528 DowngradeBoardInPlace( &board, kicad10 );
3529 BOOST_CHECK_EQUAL( fp->Value().GetText(), wxString( "42/${REFERENCE}" ) );
3530}
3531
3532
3533BOOST_AUTO_TEST_CASE( MixedPropertyTextKeepsEscapedLiteralsAndSupportedVariables )
3534{
3535 BOARD board;
3536 FOOTPRINT* fp = new FOOTPRINT( &board );
3537 fp->SetReference( wxT( "42" ) );
3538 fp->SetCustomProperty( wxT( "Serial" ), wxT( "42" ) );
3539 fp->SetKeywords( wxT( "\\${REFERENCE}/\\@{1+2}" ) );
3540 fp->Value().SetText( wxT( "${PROPERTY.Reference}/\\${U1:PROPERTY.Missing}/${REFERENCE}" ) );
3541 PCB_TEXT* text = new PCB_TEXT( fp );
3542 text->SetText( wxT( "${PROPERTY.Keywords}/${REFERENCE}" ) );
3543 fp->Add( text );
3544 board.Add( fp );
3545 const wxString shown = text->GetShownText( FOR_CANVAS );
3546 BOOST_CHECK( !ClassifyBoardForDowngrade( &board, kicad10 ).IsBlocked() );
3547 DowngradeBoardInPlace( &board, kicad10 );
3548 BOOST_CHECK_EQUAL( fp->Value().GetText(), wxString( "42/\\${U1:PROPERTY.Missing}/${REFERENCE}" ) );
3549 BOOST_CHECK( !text->GetText().Contains( wxT( "PROPERTY." ) ) );
3550 BOOST_CHECK_EQUAL( text->GetShownText( FOR_CANVAS ), shown );
3551}
3552
3553
3554BOOST_AUTO_TEST_CASE( NewExpressionUnitAliasIsBakedWithoutFreezingOtherVariables )
3555{
3556 BOARD board;
3557 FOOTPRINT* fp = new FOOTPRINT( &board );
3558 fp->SetReference( wxT( "U1" ) );
3559 fp->Value().SetText( wxT( "@{1000mils}/${REFERENCE}" ) );
3560 board.Add( fp );
3561 BOOST_CHECK( !ClassifyBoardForDowngrade( &board, kicad10 ).IsBlocked() );
3562 DowngradeBoardInPlace( &board, kicad10 );
3563 BOOST_CHECK_EQUAL( fp->Value().GetText(), wxString( "25.4/${REFERENCE}" ) );
3564}
3565
3566
3567BOOST_AUTO_TEST_CASE( UnresolvedAffectedExpressionsBlockBothPolicies )
3568{
3569 for( const wxString& raw :
3570 { wxT( "@{${PROPERTY.Reference}+${MISSING}}" ), wxT( "@{unknown_function(${PROPERTY.Reference})}" ) } )
3571 {
3572 BOARD board;
3573 FOOTPRINT* fp = new FOOTPRINT( &board );
3574 fp->SetReference( wxT( "42" ) );
3575 fp->SetCustomProperty( wxT( "Serial" ), wxT( "42" ) );
3576 fp->Value().SetText( raw );
3577 board.Add( fp );
3578
3579 for( bool drop : { false, true } )
3580 BOOST_CHECK( ClassifyBoardForDowngrade( &board, kicad10, drop ).IsBlocked() );
3581 }
3582}
3583
3584
3585BOOST_AUTO_TEST_CASE( EscapedTextControlsBlockKicad9AndRemainLiteralInKicad10 )
3586{
3587 for( bool inProperty : { false, true } )
3588 {
3589 BOARD board;
3590 FOOTPRINT* fp = new FOOTPRINT( &board );
3591 fp->SetReference( wxT( "U1" ) );
3592 const wxString literal = wxT( "\\${REFERENCE}/\\@{1+2}" );
3593 fp->SetKeywords( literal );
3594 fp->Value().SetText( inProperty ? wxString( wxT( "${PROPERTY.Keywords}" ) ) : literal );
3595 board.Add( fp );
3596 const wxString shown = fp->Value().GetShownText( FOR_CANVAS );
3597
3598 for( bool drop : { false, true } )
3599 {
3600 BOOST_CHECK( ClassifyBoardForDowngrade( &board, kicad9, drop ).IsBlocked() );
3601 BOOST_CHECK( !ClassifyBoardForDowngrade( &board, kicad10, drop ).IsBlocked() );
3602 }
3603
3604 DowngradeBoardInPlace( &board, kicad10 );
3606 }
3607}
3608
3609
3610BOOST_AUTO_TEST_CASE( TitleBlockExpressionsBakeBeforeDependentFootprintText )
3611{
3612 for( bool drop : { false, true } )
3613 {
3614 BOARD board;
3615 board.GetTitleBlock().SetTitle( wxT( "@{1000mils}" ) );
3616 FOOTPRINT* fp = new FOOTPRINT( &board );
3617 fp->SetReference( wxT( "U1" ) );
3618 fp->Value().SetText( wxT( "${TITLE}/${REFERENCE}" ) );
3619 board.Add( fp );
3620 const auto report = ClassifyBoardForDowngrade( &board, kicad10, drop );
3621 BOOST_CHECK( !report.IsBlocked() );
3622 checkFeature( report, wxT( "Post-target text variables" ), DOWNGRADE_BUCKET::DROP, 2 );
3623 DowngradeBoardInPlace( &board, kicad10, drop );
3624 BOOST_CHECK_EQUAL( board.GetTitleBlock().GetTitle(), wxString( "25.4" ) );
3625 BOOST_CHECK_EQUAL( fp->Value().GetText(), wxString( "25.4/${REFERENCE}" ) );
3626 }
3627}
3628
3629
3630BOOST_AUTO_TEST_CASE( TitleBlockUnresolvablePropertyContextBlocksBothPolicies )
3631{
3632 BOARD board;
3633 board.GetTitleBlock().SetComment( 0, wxT( "${PROPERTY.Reference}" ) );
3634
3635 for( bool drop : { false, true } )
3636 BOOST_CHECK( ClassifyBoardForDowngrade( &board, kicad10, drop ).IsBlocked() );
3637}
3638
3639
3640BOOST_AUTO_TEST_CASE( TitleBlockSelfReferenceDoesNotHideUsedProjectDependencies )
3641{
3642 KI_TEST::TEMPORARY_DIRECTORY temporary( "kicad_qa_title_dependencies" );
3643 const wxString projectPath = ( temporary.GetPath() / "source.kicad_pro" ).string();
3644
3645 {
3646 std::ofstream project( projectPath.ToStdString() );
3647 project << R"({"meta":{"version":4},"text_variables":{"TITLE":"${PROPERTY.Reference}"}})";
3648 }
3649
3650 SETTINGS_MANAGER settings;
3651 BOOST_REQUIRE( settings.LoadProject( projectPath, false ) );
3652 BOARD board;
3653 board.SetProject( settings.GetProject( projectPath ), true );
3654 board.GetTitleBlock().SetTitle( wxT( "${TITLE}" ) );
3655 FOOTPRINT* fp = new FOOTPRINT( &board );
3656 fp->SetReference( wxT( "42" ) );
3657 fp->Value().SetText( wxT( "${TITLE}" ) );
3658 board.Add( fp );
3659 BOOST_CHECK_EQUAL( fp->Value().GetShownText( FOR_CANVAS ), wxString( "42" ) );
3660
3661 for( bool drop : { false, true } )
3662 BOOST_CHECK( ClassifyBoardForDowngrade( &board, kicad10, drop ).IsBlocked() );
3663
3664 board.GetTitleBlock().SetTitle( wxT( "Ordinary title" ) );
3665 const auto report = ClassifyBoardForDowngrade( &board, kicad10 );
3666 BOOST_CHECK( !report.IsBlocked() );
3667 BOOST_CHECK( !report.IsLossy() );
3668}
3669
3670
3671BOOST_AUTO_TEST_CASE( AffectedTableExpressionsUseNativeCellCoordinates )
3672{
3673 BOARD board;
3674 FOOTPRINT* fp = new FOOTPRINT( &board );
3675 fp->SetReference( wxT( "42" ) );
3676 board.Add( fp );
3677 PCB_TABLE* table = new PCB_TABLE( fp );
3678 table->SetColCount( 1 );
3679 table->SetColWidth( 0, 10000000 );
3680 table->SetRowHeight( 0, 6000000 );
3681 PCB_TABLECELL* cell = new PCB_TABLECELL( table );
3682 cell->SetText( wxT( "@{${PROPERTY.Reference}+${ROW}}/${COL}/${REFERENCE}" ) );
3683 table->AddCell( cell );
3684 fp->Add( table );
3685 BOOST_CHECK( !ClassifyBoardForDowngrade( &board, kicad10 ).IsBlocked() );
3686 DowngradeBoardInPlace( &board, kicad10 );
3687 BOOST_CHECK_EQUAL( cell->GetText(), wxString( "43/${COL}/${REFERENCE}" ) );
3688}
3689
3690
3691BOOST_AUTO_TEST_CASE( FootprintFileUsesTheSamePostMasterPoliciesAsBoardFootprints )
3692{
3693 SETTINGS_MANAGER settingsManager;
3694 KI_TEST::TEMPORARY_DIRECTORY temporary( "kicad_qa_fp_post_master" );
3695 const wxString source = ( temporary.GetPath() / "source.kicad_mod" ).string();
3696 FOOTPRINT footprint( nullptr );
3697 footprint.SetReference( wxT( "42" ) );
3698 footprint.Value().SetText( wxT( "${PROPERTY.Reference}/${REFERENCE}" ) );
3699 footprint.SetCustomProperty( wxT( "Owner" ), wxT( "QA" ) );
3700 footprint.SetExcludedFromSim( true );
3701 PCB_SHAPE* line = new PCB_SHAPE( &footprint, SHAPE_T::SEGMENT );
3702 line->SetStart( VECTOR2I( 0, 0 ) );
3703 line->SetEnd( VECTOR2I( 10000000, 0 ) );
3704 line->SetWidth( 200000 );
3705 line->SetLayer( F_SilkS );
3707 footprint.Add( line );
3708 PCB_CONSTRAINT* constraint = new PCB_CONSTRAINT( &footprint, PCB_CONSTRAINT_TYPE::HORIZONTAL );
3709 constraint->AddMember( line->m_Uuid );
3710 footprint.Add( constraint );
3712 io.FootprintSave( source, &footprint );
3713
3714 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
3715 {
3716 for( bool drop : { false, true } )
3717 {
3718 const wxString destination = ( temporary.GetPath() / "out.kicad_mod" ).string();
3719 BOOST_REQUIRE( DowngradeFootprintFileToTemp( source, destination, target, drop )
3721 wxString name;
3722 auto converted = io.ImportFootprint( destination, name );
3723 BOOST_REQUIRE( converted );
3724 BOOST_CHECK( converted->Constraints().empty() );
3725 BOOST_CHECK( !converted->HasCustomProperties() );
3726 BOOST_CHECK( !converted->IsExcludedFromSim() );
3727 BOOST_CHECK_EQUAL( converted->Value().GetText(), wxString( "42/${REFERENCE}" ) );
3728 BOOST_REQUIRE( !converted->GraphicalItems().empty() );
3729
3730 for( BOARD_ITEM* item : converted->GraphicalItems() )
3731 {
3732 BOOST_REQUIRE( item->Type() == PCB_SHAPE_T );
3733 const PCB_SHAPE* shape = static_cast<PCB_SHAPE*>( item );
3734 BOOST_CHECK( shape->GetEndEndingStyle() == LINE_ENDING_STYLE::NONE );
3735 BOOST_CHECK( shape->GetShape() == ( drop ? SHAPE_T::SEGMENT : SHAPE_T::POLY ) );
3736 }
3737 }
3738 }
3739}
3740
3741
3742BOOST_AUTO_TEST_CASE( SimulationExclusionsFollowSelectedVariantAndReportTheirLoss )
3743{
3744 for( bool drop : { false, true } )
3745 {
3746 BOARD board;
3747 FOOTPRINT* fp = new FOOTPRINT( &board );
3748 fp->SetExcludedFromSim( false );
3749 fp->AddVariant( wxT( "selected" ) )->SetExcludedFromSim( true );
3750 board.Add( fp );
3751 FlattenBoardVariant( &board, wxT( "selected" ) );
3752 BOOST_CHECK( fp->IsExcludedFromSim() );
3753 const auto report = ClassifyBoardForDowngrade( &board, kicad10, drop );
3754 checkFeature( report, wxT( "Footprint simulation exclusions" ), DOWNGRADE_BUCKET::DROP, 1 );
3755 DowngradeBoardInPlace( &board, kicad10, drop );
3756 BOOST_CHECK( !fp->IsExcludedFromSim() );
3757 for( const auto& [name, variant] : fp->GetVariants() )
3758 BOOST_CHECK( !variant.GetExcludedFromSim() );
3759 }
3760}
3761
3762
3763BOOST_AUTO_TEST_CASE( LegacyBoldWidthsPreserveNestedTextAndDoNotMutateSource )
3764{
3765 SETTINGS_MANAGER settingsManager;
3766 BOARD board;
3767 FOOTPRINT* fp = new FOOTPRINT( &board );
3768 board.Add( fp );
3769 std::vector<EDA_TEXT*> bold;
3770 PCB_TEXT* text = new PCB_TEXT( &board );
3771 text->SetText( wxT( "board bold" ) );
3772 board.Add( text );
3773 bold.push_back( text );
3774 fp->Value().SetText( wxT( "footprint bold" ) );
3775 bold.push_back( &fp->Value() );
3776 PCB_TEXT* fpText = new PCB_TEXT( fp );
3777 fpText->SetText( wxT( "graphic bold" ) );
3778 fp->Add( fpText );
3779 bold.push_back( fpText );
3780 PCB_TABLE* table = new PCB_TABLE( fp, 100000 );
3781 table->SetColCount( 1 );
3782 table->SetColWidth( 0, 10000000 );
3783 table->SetRowHeight( 0, 5000000 );
3784 PCB_TABLECELL* cell = new PCB_TABLECELL( table );
3785 cell->SetText( wxT( "cell bold" ) );
3786 table->AddCell( cell );
3787 fp->Add( table );
3788 bold.push_back( cell );
3789 PCB_DIM_ALIGNED* dimension = new PCB_DIM_ALIGNED( &board );
3790 dimension->SetStart( VECTOR2I( 0, 0 ) );
3791 dimension->SetEnd( VECTOR2I( 10000000, 0 ) );
3792 dimension->SetHeight( 2000000 );
3793 dimension->ChangeOverrideText( wxT( "dimension bold" ) );
3794 board.Add( dimension );
3795 bold.push_back( dimension );
3796
3797 for( EDA_TEXT* item : bold )
3798 {
3799 item->SetBold( true );
3800 item->SetTextThickness( 100000 );
3801 }
3802
3803 PCB_TEXT* automatic = new PCB_TEXT( &board );
3804 automatic->SetText( wxT( "auto bold" ) );
3805 automatic->SetBold( true );
3806 automatic->SetTextThickness( 0 );
3807 board.Add( automatic );
3808 PCB_TEXT* outline = new PCB_TEXT( &board );
3809 outline->SetText( wxT( "outline bold" ) );
3810 outline->SetFont( KIFONT::FONT::GetFont( wxT( "DejaVu Sans" ), true ) );
3811 outline->SetBold( true );
3812 outline->SetTextThickness( 100000 );
3813 board.Add( outline );
3814 const bool outlineAvailable = outline->GetFont() && outline->GetFont()->IsOutline();
3815 KI_TEST::TEMPORARY_DIRECTORY temporary( "kicad_qa_legacy_bold_widths" );
3816
3817 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
3818 {
3819 const std::string first = ( temporary.GetPath() / ( target.m_id.ToStdString() + "-first.kicad_pcb" ) ).string();
3820 const std::string second =
3821 ( temporary.GetPath() / ( target.m_id.ToStdString() + "-second.kicad_pcb" ) ).string();
3822 SaveBoardForTarget( &board, first, target );
3823 SaveBoardForTarget( &board, second, target );
3824 const std::string content = KI_TEST::ReadGoldenText( first );
3825 BOOST_CHECK( content.find( "(thickness 0.16)" ) != std::string::npos );
3826 BOOST_CHECK( content == KI_TEST::ReadGoldenText( second ) );
3827
3828 for( EDA_TEXT* item : bold )
3829 BOOST_CHECK_EQUAL( item->GetTextThickness(), 100000 );
3830
3831 BOOST_CHECK_EQUAL( automatic->GetTextThickness(), 0 );
3832 BOOST_CHECK_EQUAL( outline->GetTextThickness(), 100000 );
3834 auto reloaded = io.LoadBoard( first );
3835 BOOST_REQUIRE( reloaded );
3836 size_t checked = 0;
3837 reloaded->RunOnChildren(
3838 [&]( BOARD_ITEM* item )
3839 {
3840 EDA_TEXT* reloadedText = dynamic_cast<EDA_TEXT*>( item );
3841
3842 if( !reloadedText || !reloadedText->IsBold() )
3843 return;
3844
3845 if( reloadedText->GetText() == wxT( "auto bold" ) )
3846 BOOST_CHECK_EQUAL( reloadedText->GetTextThickness(), 0 );
3847 else if( reloadedText->GetText() != wxT( "outline bold" ) || outlineAvailable )
3848 BOOST_CHECK_EQUAL( reloadedText->GetTextThickness(), 100000 );
3849
3850 ++checked;
3851 },
3853 BOOST_CHECK_EQUAL( checked, bold.size() + 2 );
3854 }
3855}
3856
3857
3858BOOST_AUTO_TEST_CASE( ComplexPadstackFrontAndInnerCustomOptionsRoundTripToKicad10 )
3859{
3860 SETTINGS_MANAGER settingsManager;
3861 BOARD board;
3862 board.SetCopperLayerCount( 4 );
3863 FOOTPRINT* fp = new FOOTPRINT( &board );
3864 PAD* pad = new PAD( fp );
3865 pad->SetAttribute( PAD_ATTRIB::PTH );
3866 pad->SetLayerSet( LSET::AllCuMask() );
3867 pad->Padstack().SetMode( PADSTACK::MODE::CUSTOM );
3868 pad->SetShape( F_Cu, PAD_SHAPE::OVAL );
3869 pad->SetSize( F_Cu, VECTOR2I( 2000000, 1000000 ) );
3870 pad->SetShape( In1_Cu, PAD_SHAPE::CUSTOM );
3871 pad->SetSize( In1_Cu, VECTOR2I( 500000, 500000 ) );
3872 pad->SetCustomShapeInZoneOpt( CUSTOM_SHAPE_ZONE_MODE::CONVEXHULL );
3873 PCB_SHAPE* primitive = new PCB_SHAPE( pad, SHAPE_T::SEGMENT );
3874 primitive->SetStart( VECTOR2I( -1000000, 0 ) );
3875 primitive->SetEnd( VECTOR2I( 1000000, 0 ) );
3876 primitive->SetWidth( 200000 );
3877 pad->AddPrimitive( In1_Cu, primitive );
3878 pad->SetDrillSize( VECTOR2I( 300000, 300000 ) );
3879 fp->Add( pad );
3880 board.Add( fp );
3881 KI_TEST::TEMPORARY_DIRECTORY temporary( "kicad_qa_complex_pad_front" );
3882 const std::string path = ( temporary.GetPath() / "out.kicad_pcb" ).string();
3883 SaveBoardForTarget( &board, path, kicad10 );
3885 auto reloaded = io.LoadBoard( path );
3886 BOOST_REQUIRE( reloaded && reloaded->Footprints().size() == 1 );
3887 BOOST_REQUIRE_EQUAL( reloaded->Footprints()[0]->Pads().size(), 1 );
3888 const PAD* result = reloaded->Footprints()[0]->Pads()[0];
3889 BOOST_CHECK( result->GetShape( F_Cu ) == PAD_SHAPE::OVAL );
3890 BOOST_CHECK( result->GetSize( F_Cu ) == VECTOR2I( 2000000, 1000000 ) );
3891 BOOST_CHECK( result->GetShape( In1_Cu ) == PAD_SHAPE::CUSTOM );
3892 BOOST_REQUIRE_EQUAL( result->GetPrimitives( In1_Cu ).size(), 1 );
3893 BOOST_CHECK( result->GetCustomShapeInZoneOpt() == CUSTOM_SHAPE_ZONE_MODE::CONVEXHULL );
3894}
3895
3896
3897BOOST_AUTO_TEST_CASE( CurrentProjectSchemasAndSupportedDrcExclusionsMigrateForBothTargets )
3898{
3899 const nlohmann::json exclusion = { { "marker",
3900 { { "error_type", "DRCET_CLEARANCE" },
3901 { "position", { { "x_nm", "123" }, { "y_nm", "-456" } } },
3902 { "items",
3903 { { { "value", "11111111-1111-1111-1111-111111111111" } },
3904 { { "value", "22222222-2222-2222-2222-222222222222" } } } } } },
3905 { "comment", "accepted" } };
3906
3907 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
3908 {
3909 nlohmann::json doc = {
3910 { "meta", { { "version", 4 } } },
3911 { "board",
3912 { { "design_settings", { { "meta", { { "version", 3 } } }, { "drc_exclusions", { exclusion } } } } } }
3913 };
3914 COMPATIBILITY_REPORT report;
3915 DowngradeProjectFileJson( doc, target, report );
3916 BOOST_CHECK_EQUAL( doc["meta"]["version"].get<int>(), 3 );
3917 const auto& settings = doc["board"]["design_settings"];
3918 BOOST_CHECK_EQUAL( settings["meta"]["version"].get<int>(), 2 );
3919 BOOST_REQUIRE_EQUAL( settings["drc_exclusions"].size(), 1 );
3921 settings["drc_exclusions"][0][0].get<std::string>(),
3922 "clearance|123|-456|11111111-1111-1111-1111-111111111111|22222222-2222-2222-2222-222222222222" );
3923 BOOST_CHECK_EQUAL( settings["drc_exclusions"][0][1].get<std::string>(), "accepted" );
3924 BOOST_CHECK( !report.IsLossy() );
3925 const auto migrated = doc;
3926 COMPATIBILITY_REPORT second;
3927 DowngradeProjectFileJson( doc, target, second );
3928 BOOST_CHECK( doc == migrated );
3929 BOOST_CHECK( !second.IsLossy() );
3930 }
3931}
3932
3933
3934BOOST_AUTO_TEST_CASE( UnrepresentableDrcExclusionsDropWithReports )
3935{
3936 nlohmann::json multiple = { { "marker",
3937 { { "error_type", "DRCET_CLEARANCE" },
3938 { "position", { { "x_nm", "123" }, { "y_nm", "-456" } } },
3939 { "items",
3940 { { { "value", "11111111-1111-1111-1111-111111111111" } },
3941 { { "value", "22222222-2222-2222-2222-222222222222" } },
3942 { { "value", "33333333-3333-3333-3333-333333333333" } } } } } },
3943 { "comment", "three items" } };
3944 nlohmann::json unknown = multiple;
3945 unknown["marker"]["error_type"] = "DRCET_MICROVIA_STACK_DEPTH";
3946 unknown["marker"]["items"].erase( 2 );
3947
3948 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
3949 {
3950 nlohmann::json doc = {
3951 { "board",
3952 { { "design_settings",
3953 { { "meta", { { "version", 3 } } }, { "drc_exclusions", { multiple, unknown, "malformed" } } } } } }
3954 };
3955 COMPATIBILITY_REPORT report;
3956 DowngradeProjectFileJson( doc, target, report );
3957 BOOST_CHECK( doc["board"]["design_settings"]["drc_exclusions"].empty() );
3959 }
3960}
3961
3962
3963BOOST_AUTO_TEST_CASE( PostMasterProjectSettingsReportOnlyMeaningfulLosses )
3964{
3965 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
3966 {
3967 nlohmann::json doc;
3968 doc["net_settings"]["net_chain_netclasses"] = { { { "name", "fast" } } };
3969 doc["board"]["design_settings"]["via_stack_presets"] = { { { "name", "fanout" } } };
3970 doc["board"]["design_settings"]["teardrop_parameters"] = {
3971 { { "td_target_name", "td_track_end" }, { "td_enabled", true }, { "td_max_len", 1.0 } },
3972 { { "td_enabled", false } }
3973 };
3974 COMPATIBILITY_REPORT report;
3975 DowngradeProjectFileJson( doc, target, report );
3976 BOOST_CHECK( !doc["net_settings"].contains( "net_chain_netclasses" ) );
3977 const auto& settings = doc["board"]["design_settings"];
3978 BOOST_CHECK( !settings.contains( "via_stack_presets" ) );
3979 BOOST_CHECK( !settings["teardrop_parameters"][0].contains( "td_enabled" ) );
3980 BOOST_CHECK( !settings["teardrop_parameters"][1].contains( "td_enabled" ) );
3981 BOOST_CHECK_EQUAL( settings["teardrop_parameters"][0]["td_max_len"].get<double>(), 1.0 );
3983 COMPATIBILITY_REPORT second;
3984 DowngradeProjectFileJson( doc, target, second );
3985 BOOST_CHECK( !second.IsLossy() );
3986
3987 nlohmann::json defaults;
3988 defaults["net_settings"]["net_chain_netclasses"] = nlohmann::json::array();
3989 defaults["board"]["design_settings"]["via_stack_presets"] = nlohmann::json::array();
3990 defaults["board"]["design_settings"]["teardrop_parameters"] = { { { "td_enabled", false } } };
3991 COMPATIBILITY_REPORT silent;
3992 DowngradeProjectFileJson( defaults, target, silent );
3993 BOOST_CHECK( !silent.IsLossy() );
3994 }
3995}
3996
3997
3998BOOST_AUTO_TEST_CASE( NewTuningBridgeDelayIsRemovedAndReportedForKicad10 )
3999{
4000 nlohmann::json doc = {
4001 { "tuning_profiles",
4002 { { "meta", { { "version", 2 } } },
4003 { "tuning_profiles_impedance_geometric",
4004 { { { "net_chain_bridge_prop_delay", 25 } }, { { "net_chain_bridge_prop_delay", 0 } } } } } }
4005 };
4006 COMPATIBILITY_REPORT report;
4007 DowngradeProjectFileJson( doc, kicad10, report );
4008 BOOST_CHECK_EQUAL( doc["tuning_profiles"]["meta"]["version"].get<int>(), 0 );
4010
4011 for( const auto& profile : doc["tuning_profiles"]["tuning_profiles_impedance_geometric"] )
4012 BOOST_CHECK( !profile.contains( "net_chain_bridge_prop_delay" ) );
4013}
4014
4015
4016BOOST_AUTO_TEST_CASE( NativeNetChainClassMapReportsOnlyNonemptyMappings )
4017{
4018 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
4019 {
4020 nlohmann::json doc;
4021 doc["net_settings"]["net_chain_classes"] = { { "clock", "Fast" } };
4022 COMPATIBILITY_REPORT report;
4023 DowngradeProjectFileJson( doc, target, report );
4024 BOOST_CHECK( !doc["net_settings"].contains( "net_chain_classes" ) );
4026 COMPATIBILITY_REPORT second;
4027 DowngradeProjectFileJson( doc, target, second );
4028 BOOST_CHECK( !second.IsLossy() );
4029 doc["net_settings"]["net_chain_classes"] = nlohmann::json::object();
4031 DowngradeProjectFileJson( doc, target, empty );
4032 BOOST_CHECK( !empty.IsLossy() );
4033 }
4034}
4035
4036
4037BOOST_AUTO_TEST_CASE( NewDrcConstraintsAndFunctionsAreFilteredWithoutLiteralFalsePositives )
4038{
4039 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
4040 {
4041 for( const wxString& keyword : { wxT( "microvia_aspect_ratio" ), wxT( "microvia_stack_depth" ) } )
4042 {
4043 const wxString rule = "(version 1)(rule \"new\" (constraint " + keyword + " (max 2)))";
4044 const auto result = FilterDrcRulesForTarget( rule, target );
4045 BOOST_CHECK( result.m_error.IsEmpty() );
4046 BOOST_CHECK_EQUAL( result.m_dropped.size(), 1 );
4047 }
4048
4049 for( const wxString& call :
4050 { wxT( "A.intersectsKeepout('x')" ), wxT( "A.intersectsArea('x')" ), wxT( "A.isStackedVia()" ),
4051 wxT( "A.ISSTACKEDVIA()" ), wxT( "A.customProperty('Owner') == 'Team'" ),
4052 wxT( "A.hasCustomProperty('Owner')" ) } )
4053 {
4054 for( const wxString& expression : { wxT( "condition" ), wxT( "assertion" ) } )
4055 {
4056 const wxString rule =
4057 expression == wxT( "condition" )
4058 ? "(rule \"new\" (condition \"" + call + "\") (constraint clearance (min 0.2mm)))"
4059 : "(rule \"new\" (constraint assertion \"" + call + "\"))";
4060 const auto result = FilterDrcRulesForTarget( wxT( "(version 1)" ) + rule, target );
4061 BOOST_CHECK( result.m_error.IsEmpty() );
4062 BOOST_CHECK_EQUAL( result.m_dropped.size(), 1 );
4063 }
4064 }
4065
4066 const wxString kept = wxT( "(rule \"customProperty\" (condition \"A.NetName == 'isStackedVia()'\") "
4067 "(constraint clearance (min 0.2mm)))\n"
4068 "(rule \"literal\" (condition \"A.NetName == 'intersectsArea()'\") "
4069 "(constraint clearance (min 0.2mm)))\n"
4070 "(rule \"uppercase literal\" (condition \"A.NetName == 'ISSTACKEDVIA()'\") "
4071 "(constraint clearance (min 0.2mm)))\n"
4072 "(rule \"legacy\" (condition \"A.insideArea('x')\") "
4073 "(constraint clearance (min 0.2mm)))\n"
4074 "(rule \"position\" (condition \"A.Start_X > 1mm && A.Start_Y > 1mm\") "
4075 "(constraint clearance (min 0.2mm)))" );
4076 const auto result = FilterDrcRulesForTarget( wxT( "(version 1)\n" ) + kept, target );
4077 BOOST_CHECK( result.m_error.IsEmpty() );
4078 BOOST_CHECK( result.m_dropped.empty() );
4079 BOOST_CHECK( result.m_text.Contains( kept ) );
4080 }
4081}
4082
4083
4084BOOST_AUTO_TEST_CASE( DrcPostTargetPropertiesAreGuardedInConditionsAndAssertions )
4085{
4086 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
4087 {
4088 for( const wxString& kind : { wxT( "condition" ), wxT( "assertion" ) } )
4089 {
4090 const wxString expression = wxT( "A.Parent == A.Parent && A.NetName != 'A.parent.Reference'" );
4091 const wxString body =
4092 kind == wxT( "condition" )
4093 ? wxT( "(condition \"" ) + expression + wxT( "\") (constraint clearance (min 0.2mm))" )
4094 : wxT( "(constraint assertion \"" ) + expression + wxT( "\")" );
4095 const auto parent =
4096 FilterDrcRulesForTarget( wxT( "(version 1)(rule \"parent property\" " ) + body + ")", target );
4097 BOOST_CHECK( parent.m_error.IsEmpty() );
4098 BOOST_CHECK( parent.m_dropped.empty() );
4099 }
4100
4101 for( const wxString& property :
4102 { wxT( "A.Start_Shape" ), wxT( "A.start_shape" ), wxT( "B.End_Width" ), wxT( "AB.Scale_X" ),
4103 wxT( "A.parent.Reference" ), wxT( "A . PARENT . Reference" ), wxT( "A.Major_Radius" ),
4104 wxT( "A.Exclude_From_Simulation" ), wxT( "A.Automatically_Update_Net" ), wxT( "A.Grid_Type" ) } )
4105 {
4106 for( const wxString& kind : { wxT( "condition" ), wxT( "assertion" ) } )
4107 {
4108 const wxString expression =
4109 property + ( property.Contains( wxT( "Reference" ) ) ? wxT( " == 'U1'" ) : wxT( " == 1" ) );
4110 const wxString body =
4111 kind == wxT( "condition" )
4112 ? wxT( "(condition \"" ) + expression + wxT( "\") (constraint clearance (min 0.2mm))" )
4113 : wxT( "(constraint assertion \"" ) + expression + wxT( "\")" );
4114 const auto result =
4115 FilterDrcRulesForTarget( wxT( "(version 1)(rule \"props\" " ) + body + ")", target );
4116 BOOST_CHECK( result.m_error.IsEmpty() );
4117 BOOST_CHECK_EQUAL( result.m_dropped.size(), 1 );
4118 }
4119 }
4120
4121 for( const wxString& property :
4122 { wxT( "A.Corner_Radius" ), wxT( "A.Auto_Thickness" ), wxT( "A.Target_Delay" ) } )
4123 {
4124 for( const wxString& kind : { wxT( "condition" ), wxT( "assertion" ) } )
4125 {
4126 const wxString expression = property + wxT( " == 1" );
4127 const wxString body =
4128 kind == wxT( "condition" )
4129 ? wxT( "(condition \"" ) + expression + wxT( "\") (constraint clearance (min 0.2mm))" )
4130 : wxT( "(constraint assertion \"" ) + expression + wxT( "\")" );
4131 const auto result =
4132 FilterDrcRulesForTarget( wxT( "(version 1)(rule \"older\" " ) + body + ")", target );
4133 BOOST_CHECK( result.m_error.IsEmpty() );
4134 BOOST_CHECK_EQUAL( result.m_dropped.size(), target.m_boardVersion == kicad9.m_boardVersion ? 1 : 0 );
4135 }
4136 }
4137
4138 const wxString kept = wxT( "(rule \"literal\" (condition \"A.NetName == 'A.Start_Shape' && A.Start_X > 1mm "
4139 "&& A.End_Y > 1mm && A.Radius > 1mm\") (constraint clearance (min 0.2mm)))" );
4140 const auto result = FilterDrcRulesForTarget( wxT( "(version 1)" ) + kept, target );
4141 BOOST_CHECK( result.m_error.IsEmpty() );
4142 BOOST_CHECK( result.m_dropped.empty() );
4143 BOOST_CHECK( result.m_text.Contains( kept ) );
4144 }
4145}
4146
4147
const char * name
@ ERROR_INSIDE
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:128
COMPATIBILITY_REPORT ClassifyBoardForDowngrade(const BOARD *aBoard, const DOWNGRADE_TARGET &aTarget, bool aDropInsteadOfApproximate)
Report what a board would lose if exported to an older KiCad.
void SaveFootprintForTarget(FOOTPRINT *aFootprint, const wxString &aPath, const DOWNGRADE_TARGET &aTarget)
Save a single .kicad_mod footprint file for the target.
int BoardDowngradeCoveredVersion()
The format the denylist was generated against. A format bump forces a review.
void SaveBoardForTarget(BOARD *aBoard, const wxString &aPath, const DOWNGRADE_TARGET &aTarget)
Save a board stamped for the target, using the writer for that version.
void FlattenBoardVariant(BOARD *aBoard, const wxString &aVariantName)
Bake a variant's overrides into the base footprint attributes.
bool ExportBoardToOlderVersion(const wxString &aSrcFile, const wxString &aDestFile, const DOWNGRADE_TARGET &aTarget, COMPATIBILITY_REPORT &aReport, wxString *aUnsupportedToken, const wxString &aVariant, bool aDropInsteadOfApproximate)
Load a board, downgrade the copy, and write it stamped for the target.
DOWNGRADE_FILE_RESULT DowngradeFootprintFileToTemp(const wxString &aSrcFile, const wxString &aTmpFile, const DOWNGRADE_TARGET &aTarget, bool aDropInsteadOfApproximate)
Load one .kicad_mod, classify it against the rule table, and write the downgraded copy to aTmpFile.
void DowngradeBoardInPlace(BOARD *aBoard, const DOWNGRADE_TARGET &aTarget, bool aDropInsteadOfApproximate)
Approximate or remove, in place, what the target cannot represent.
void DowngradeFootprintInPlace(FOOTPRINT *aFootprint, const DOWNGRADE_TARGET &aTarget, bool aDropInsteadOfApproximate)
Footprint-scoped downgrade for standalone .kicad_mod files the board pass never sees.
wxString FindUnsupportedBoardToken(const wxString &aSerialized, const DOWNGRADE_TARGET &aTarget)
Scan serialized board text for tokens the target cannot parse.
@ BS_ITEM_TYPE_DIELECTRIC
int GetPPI() const
int GetLegacyPPI() const
Recompute the PPI the way it was computed before the pixels/cm truncation fix.
PCB_LAYER_ID GetLayer() const override
Return the primary layer this item is on.
std::map< PCB_LAYER_ID, ZONE_LAYER_PROPERTIES > m_ZoneLayerProperties
DRILL_SYMBOL_PROFILE & GetDrillSymbolProfile()
BOARD_STACKUP & GetStackupDescriptor()
Abstract interface for BOARD_ITEMs capable of storing other items inside.
virtual void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false)=0
Adds an item to the container.
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:84
virtual bool IsKnockout() const
Definition board_item.h:414
virtual PCB_LAYER_ID GetLayer() const
Return the primary layer this item is on.
virtual void SetIsKnockout(bool aKnockout)
Definition board_item.h:415
Manage one layer needed to make a physical board.
DIELECTRIC_MODEL GetDielectricModel(int aDielectricSubLayer=0) const
void SetDielectricModel(DIELECTRIC_MODEL aModel, int aDielectricSubLayer=0)
void Add(BOARD_STACKUP_ITEM *aItem)
Add a new item in stackup layer.
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:1521
const GENERATORS & Generators() const
Definition board.h:484
void SetProperties(const std::map< wxString, wxString > &aProps)
Definition board.h:526
NETINFO_ITEM * FindNet(int aNetcode) const
Search for a net with the given netcode.
Definition board.cpp:2998
TITLE_BLOCK & GetTitleBlock()
Definition board.h:1024
const TRACKS & Tracks() const
Definition board.h:462
const CONSTRAINTS & Constraints() const
Geometric constraints (#2329) owned by this board.
Definition board.h:521
void SetCopperLayerCount(int aCount)
Definition board.cpp:1141
void SetProject(PROJECT *aProject, bool aReferenceOnly=false)
Link a board to a given project.
Definition board.cpp:377
wxString GroupsSanityCheck(bool repair=false)
Consistency check of internal m_groups structure.
Definition board.cpp:4100
BOARD_DESIGN_SETTINGS & GetDesignSettings() const
Definition board.cpp:1298
void SetEnabledLayers(const LSET &aLayerMask)
A proxy function that calls the correspondent function in m_BoardSettings.
Definition board.cpp:1202
const DRAWINGS & Drawings() const
Definition board.h:466
What an export to a given target will keep, approximate, drop, or block.
bool IsLossy() const
True if anything is approximated or dropped. This is what triggers the lossy warning.
bool IsBlocked() const
True if some feature makes the target impossible. The user cannot export anyway.
int Count(DOWNGRADE_BUCKET aBucket) const
const std::vector< COMPAT_ENTRY > & Entries() const
Stage related design and library files together, retaining originals until every write is ready.
const wxString & GetRecoveryError() const
Recovery details are nonempty only if restoring an original or removing a new output failed.
wxString Commit()
Replace all destinations, rolling back earlier replacements if a rename fails.
wxString Stage(const wxString &aDestination)
Return an exclusively-created sibling staging file, or empty if it cannot be created.
void Discard(const wxString &aTemporary)
Forget a skipped conversion without exposing its empty staging file as an output.
void Parse(std::vector< std::shared_ptr< DRC_RULE > > &aRules, REPORTER *aReporter)
Grouping rules and symbol assignments, shared by reference so a chart and its map can never disagree ...
void SetName(const wxString &aName)
A set of EDA_ITEMs (i.e., without duplicates).
Definition eda_group.h:43
std::unordered_set< EDA_ITEM * > & GetItems()
Definition eda_group.h:78
wxString GetName() const
Definition eda_group.h:75
void AddItem(EDA_ITEM *aItem)
Add item to group.
Definition eda_group.cpp:58
void SetName(const wxString &aName)
Definition eda_group.h:76
A base class for most all the KiCad significant classes used in schematics and boards.
Definition eda_item.h:98
const KIID m_Uuid
Definition eda_item.h:599
virtual EDA_GROUP * GetParentGroup() const
Definition eda_item.h:116
KICAD_T Type() const
Returns the type of object.
Definition eda_item.h:110
bool HasCustomProperties() const
Definition eda_item.h:269
void SetCustomProperty(const wxString &aKey, const wxString &aValue)
Definition eda_item.h:255
void SetEndEnding(const LINE_ENDING &aEnding)
Definition eda_shape.h:181
FILL_T GetFillMode() const
Definition eda_shape.h:148
void TransformWithLineEndingsToPolygon(SHAPE_POLY_SET &aBuffer, int aClearance, int aError, ERROR_LOC aErrorLoc, bool ignoreLineWidth=false) const
Convert the shape body shortened for line endings plus line-ending geometry to polygons.
void SetLineStyle(const LINE_STYLE aStyle)
SHAPE_POLY_SET & GetPolyShape()
void SetEndEndingStyle(LINE_ENDING_STYLE aStyle)
Definition eda_shape.h:187
SHAPE_T GetShape() const
Definition eda_shape.h:175
LINE_STYLE GetLineStyle() const
LINE_ENDING_STYLE GetEndEndingStyle() const
Definition eda_shape.h:186
int GetCornerRadius() const
void SetFillMode(FILL_T aFill)
A mix-in class (via multiple inheritance) that handles texts such as labels, parts,...
Definition eda_text.h:94
virtual const wxString & GetText() const
Return the string associated with the text object.
Definition eda_text.h:118
KIFONT::FONT * GetFont() const
Definition eda_text.h:286
void SetBold(bool aBold)
Set the text to be bold - this will also update the font if needed.
Definition eda_text.cpp:310
bool IsBold() const
Definition eda_text.h:215
virtual int GetTextThickness() const
Definition eda_text.h:159
virtual void SetText(const wxString &aText)
Definition eda_text.cpp:236
void SetFont(KIFONT::FONT *aFont)
Definition eda_text.cpp:471
Variant information for a footprint.
Definition footprint.h:228
void SetExcludedFromSim(bool aExclude)
Definition footprint.h:249
void SetDNP(bool aDNP)
Definition footprint.h:243
void SetFieldValue(const wxString &aFieldName, const wxString &aValue)
Set a field value override for this variant.
Definition footprint.h:274
bool GetDuplicatePadNumbersAreJumpers() const
Definition footprint.h:1243
const CASE_INSENSITIVE_MAP< FOOTPRINT_VARIANT > & GetVariants() const
Get all variants.
Definition footprint.h:1127
bool IsDNP() const
Definition footprint.h:1063
EDA_ANGLE GetOrientation() const
Definition footprint.h:439
void SetDNP(bool aDNP=true)
Definition footprint.h:1064
void SetStackupMode(FOOTPRINT_STACKUP aMode)
Set the stackup mode for this footprint.
void SetExcludedFromSim(bool aExclude=true)
Definition footprint.h:1055
void SetKeywords(const wxString &aKeywords)
Definition footprint.h:495
PCB_FIELD & Value()
read/write accessors:
Definition footprint.h:947
bool HasExtrudedBody() const
Definition footprint.h:428
void SetDuplicatePadNumbersAreJumpers(bool aEnabled)
Definition footprint.h:1244
CONSTRAINTS & Constraints()
Definition footprint.h:417
PCB_FIELD * GetField(FIELD_T aFieldType)
Return a mandatory field in this footprint.
std::deque< PAD * > & Pads()
Definition footprint.h:405
EXTRUDED_3D_BODY & EnsureExtrudedBody()
void SetReference(const wxString &aReference)
Definition footprint.h:915
bool IsExcludedFromSim() const
Definition footprint.h:1054
void SetTransformScale(double aScaleX, double aScaleY)
PCB_FIELD & Reference()
Definition footprint.h:948
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Adds an item to the container.
FOOTPRINT_VARIANT * AddVariant(const wxString &aVariantName)
Add a new variant with the given name.
const wxString & GetReference() const
Definition footprint.h:909
VECTOR2I GetPosition() const override
Definition footprint.h:436
DRAWINGS & GraphicalItems()
Definition footprint.h:408
Hold an error message and may be used when throwing exceptions containing meaningful error messages.
static FONT * GetFont(const wxString &aFontName=wxEmptyString, bool aBold=false, bool aItalic=false, const std::vector< wxString > *aEmbeddedFiles=nullptr, bool aForDrawingSheet=false)
Definition font.cpp:143
virtual bool IsOutline() const
Definition font.h:102
Definition kiid.h:46
wxString AsString() const
Definition kiid.cpp:264
std::string AsStdString() const
Definition kiid.cpp:270
A temporary directory deleted when it goes out of scope.
const std::filesystem::path & GetPath() const
A logical library item identifier and consists of various portions much like a URI.
Definition lib_id.h:45
Decorative shape (arrowhead, circle, square) at the start or end of a graphic line,...
Definition line_ending.h:62
LSEQ Seq(const LSEQ &aSequence) const
Return an LSEQ from the union of this LSET and a desired sequence.
Definition lset.cpp:309
static LSET AllCuMask(int aCuLayerCount)
Return a mask holding the requested number of Cu PCB_LAYER_IDs.
Definition lset.cpp:595
Handle the data for a net.
Definition netinfo.h:50
int GetNetCode() const
Definition netinfo.h:104
void SetParent(BOARD *aParent)
Definition netinfo.h:176
std::string Quotew(const wxString &aWrapee) const
Definition richio.cpp:505
A PADSTACK defines the characteristics of a single or multi-layer pad, in the IPC sense of the word.
Definition padstack.h:156
MASK_LAYER_PROPS & FrontOuterLayers()
Definition padstack.h:379
void SetUnconnectedLayerMode(UNCONNECTED_LAYER_MODE aMode)
Definition padstack.h:374
UNCONNECTED_LAYER_MODE UnconnectedLayerMode() const
Definition padstack.h:373
DRILL_PROPS & Drill()
Definition padstack.h:356
@ CUSTOM
Shapes can be defined on arbitrary layers.
Definition padstack.h:172
MASK_LAYER_PROPS & BackOuterLayers()
Definition padstack.h:382
Definition pad.h:61
LSET GetLayerSet() const override
Return a std::bitset of all layers on which the item physically resides.
Definition pad.h:557
VECTOR2I GetPosition() const override
Definition pad.cpp:256
double GetRoundRectRadiusRatio(PCB_LAYER_ID aLayer) const
Definition pad.h:811
PAD_SHAPE GetShape(PCB_LAYER_ID aLayer) const
Definition pad.h:205
VECTOR2I GetSize(PCB_LAYER_ID aLayer) const
Definition pad.cpp:298
EDA_ANGLE GetOrientation() const
Return the rotation angle of the pad.
Definition pad.cpp:1757
const VECTOR2I & GetMid() const
Definition pcb_track.h:295
wxString GetShownText(RESOLUTION_CONTEXT aContext) const
void SetShowText(bool aShow)
void SetBarcodeText(const wxString &aText)
void SetWidth(int aWidth)
void SetBarcodeKind(BARCODE_T aKind)
void SetHeight(int aHeight)
void SetLayer(PCB_LAYER_ID aLayer) override
Set the drawing layer for the barcode and its text.
void TransformShapeToPolygon(SHAPE_POLY_SET &aBuffer, PCB_LAYER_ID aLayer, int aClearance, int aMaxError, ERROR_LOC aErrorLoc=ERROR_INSIDE, bool ignoreLineWidth=false) const override
Convert the barcode (text + symbol shapes) to polygonal geometry suitable for filling/collision tests...
A geometric constraint between board items (issue #2329).
void AddMember(const KIID &aItem, CONSTRAINT_ANCHOR aAnchor=CONSTRAINT_ANCHOR::WHOLE, int aIndex=-1)
virtual void SetEnd(const VECTOR2I &aPoint)
virtual void SetStart(const VECTOR2I &aPoint)
void ChangeOverrideText(const wxString &aValue)
For better understanding of the points that make a dimension:
void SetHeight(int aHeight)
Set the distance from the feature points to the crossbar line.
A drill chart placed on the board, kept in step with the holes.
Turns on drill symbols at the holes, for one layer.
wxString GetShownText(RESOLUTION_CONTEXT aContext, int aDepth=0) const override
Return the string actually shown after processing of the base text.
PCB_GENERATOR * DeepClone() const
A set of BOARD_ITEMs (i.e., without duplicates).
Definition pcb_group.h:51
A PCB_IO derivation for saving and loading Pcbnew s-expression formatted files.
std::unique_ptr< FOOTPRINT > ImportFootprint(const wxString &aFootprintPath, wxString &aFootprintNameOut, const std::map< std::string, UTF8 > *aProperties=nullptr) override
Load a single footprint from aFootprintPath and put its name in aFootprintNameOut.
void FootprintSave(const wxString &aLibraryPath, const FOOTPRINT *aFootprint, const std::map< std::string, UTF8 > *aProperties=nullptr) override
Write aFootprint to an existing library located at aLibraryPath.
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 FormatBoardToFormatter(OUTPUTFORMATTER *aOut, BOARD *aBoard, const std::map< std::string, UTF8 > *aProperties=nullptr)
Serialize a BOARD to an OUTPUTFORMATTER without file I/O or Prettify.
std::unique_ptr< BOARD > LoadBoard(const wxString &aFileName, const std::map< std::string, UTF8 > *aProperties=nullptr, PROJECT *aProject=nullptr)
Load information from some input file format that this PCB_IO implementation knows about into new BOA...
Definition pcb_io.cpp:72
Object to handle a bitmap image that can be inserted in a PCB.
REFERENCE_IMAGE & GetReferenceImage()
void SetEllipseCenter(const VECTOR2I &aPt) override
void SetCornerRadius(int aCornerRadius) override
void SetWidth(int aWidth) override
void SetEllipseStartAngle(const EDA_ANGLE &aA) override
void SetEllipseEndAngle(const EDA_ANGLE &aA) override
void SetEnd(const VECTOR2I &aEnd) override
void SetLayer(PCB_LAYER_ID aLayer) override
Set the layer this item is on.
void SetEllipseMinorRadius(int aR) override
void SetStart(const VECTOR2I &aStart) override
void SetEllipseMajorRadius(int aR) override
PCB_LAYER_ID GetLayer() const override
Return the primary layer this item is on.
Definition pcb_shape.h:68
void AddCell(PCB_TABLECELL *aCell)
Definition pcb_table.h:170
void SetColCount(int aCount)
Definition pcb_table.h:125
void SetTextThickness(int aWidth) override
The TextThickness is that set by the user.
Definition pcb_text.cpp:512
VECTOR2I GetPosition() const override
Definition pcb_text.h:100
int GetTextThickness() const override
Definition pcb_text.cpp:497
virtual LSET GetLayerSet() const override
Return a std::bitset of all layers on which the item physically resides.
virtual EDA_ITEM * Clone() const override
Create a duplicate of this item with linked list members set to NULL.
Definition pcb_track.cpp:72
const VECTOR2I & GetStart() const
Definition pcb_track.h:98
const VECTOR2I & GetEnd() const
Definition pcb_track.h:95
virtual int GetWidth() const
Definition pcb_track.h:92
const STRING_ANY_MAP GetProperties() const override
PCB_TUNING_PATTERN(BOARD_ITEM *aParent=nullptr, PCB_LAYER_ID aLayer=F_Cu, LENGTH_TUNING_MODE aMode=LENGTH_TUNING_MODE::SINGLE)
PNS::MEANDER_SETTINGS & GetSettings()
A microvia stack: several single hop microvias, plus connecting traces for staggered stacks,...
VIA_STACK_STYLE GetStyle() const
PCB_LAYER_ID GetStartLayer() const
PCB_LAYER_ID GetEndLayer() const
wxString GetGeneratorType() const override
PCB_VIA_STITCH(BOARD_ITEM *aParent=nullptr)
int GetGuardedNetCode() const
Net being guarded in GUARD mode.
const PADSTACK & Padstack() const
Definition pcb_track.h:427
void SetViaType(VIATYPE aViaType)
Definition pcb_track.h:419
VIATYPE GetViaType() const
Definition pcb_track.h:418
bool m_isTimeDomain
Calculate tuning in the time domain.
A REFERENCE_IMAGE is a wrapper around a BITMAP_IMAGE that is displayed in an editor as a reference fo...
bool ReadImageFile(const wxString &aFullFilename)
Read and store an image file.
const BITMAP_BASE & GetImage() const
Get the underlying image.
double GetImageScale() const
void SetImageScale(double aScale)
Set the image "zoom" value.
bool LoadProject(const wxString &aFullPath, bool aSetActive=true)
Load a project or sets up a new project with a specified path.
PROJECT * GetProject(const wxString &aFullPath) const
Retrieve a loaded project by name.
double Area(bool aAbsolute=true) const
Return the area of this chain.
Represent a set of closed polygons.
double Area()
Return the area of this poly set.
int HoleCount(int aOutline) const
Returns the number of holes in a given outline.
int Append(int x, int y, int aOutline=-1, int aHole=-1, bool aAllowDuplication=false)
Appends a vertex at the end of the given outline/hole (default: the last outline)
void Simplify()
Simplify the polyset (merges overlapping polys, eliminates degeneracy/self-intersections)
SHAPE_LINE_CHAIN & Outline(int aIndex)
Return the reference to aIndex-th outline in the set.
int NewOutline()
Creates a new empty polygon in the set and returns its index.
const SHAPE_LINE_CHAIN & CHole(int aOutline, int aHole) const
int OutlineCount() const
Return the number of outlines in the set.
const SHAPE_LINE_CHAIN & COutline(int aIndex) const
A name/value tuple with unique names and wxAny values.
bool get_to(const std::string &aKey, T &aVar) const
void set(const std::string &aKey, const T &aVar)
Implement an OUTPUTFORMATTER to a memory buffer.
Definition richio.h:430
const std::string & GetString()
Definition richio.h:453
void SetComment(int aIdx, const wxString &aComment)
Definition title_block.h:98
void SetTitle(const wxString &aTitle)
Definition title_block.h:55
const wxString & GetTitle() const
Definition title_block.h:60
A wrapper for reporting to a wxString object.
Definition reporter.h:242
Handle a list of polygons defining a copper zone.
Definition zone.h:70
bool GetIsRuleArea() const
Accessors to parameters used in Rule Area zones:
Definition zone.h:832
bool GetDoNotAllowVias() const
Definition zone.h:843
bool AppendCorner(const VECTOR2I &aPosition, int aHoleIdx, bool aAllowDuplication=false)
Add a new corner to the zone outline (to the main outline or a hole)
Definition zone.cpp:1464
ZONE_LAYER_PROPERTIES & LayerProperties(PCB_LAYER_ID aLayer)
Definition zone.h:146
wxString GetPlacementAreaSource() const
Definition zone.h:837
std::shared_ptr< SHAPE_POLY_SET > GetFilledPolysList(PCB_LAYER_ID aLayer) const
Definition zone.h:699
bool GetDoNotAllowPads() const
Definition zone.h:845
PLACEMENT_SOURCE_T GetPlacementAreaSourceType() const
Definition zone.h:839
bool GetDoNotAllowTracks() const
Definition zone.h:844
bool IsFilled() const
Definition zone.h:306
virtual void SetLayer(PCB_LAYER_ID aLayer) override
Set the layer this item is on.
Definition zone.cpp:640
SHAPE_POLY_SET * Outline()
Definition zone.h:421
bool IsIsland(PCB_LAYER_ID aLayer, int aPolyIdx) const
Check if a given filled polygon is an insulated island.
Definition zone.cpp:1714
bool IsCopperThieving() const
Definition zone.h:349
const wxString & GetZoneName() const
Definition zone.h:160
SHAPE_POLY_SET GetBoardOutline() const
Definition zone.cpp:916
void SetFillMode(ZONE_FILL_MODE aFillMode)
Definition zone.cpp:646
void SetLayerSet(const LSET &aLayerSet) override
Definition zone.cpp:665
bool GetPlacementAreaEnabled() const
Definition zone.h:834
bool GetDoNotAllowFootprints() const
Definition zone.h:846
virtual LSET GetLayerSet() const override
Return a std::bitset of all layers on which the item physically resides.
Definition zone.h:133
bool GetDoNotAllowZoneFills() const
Definition zone.h:842
@ FOR_CANVAS
Definition common.h:90
DOWNGRADE_BUCKET
What happens to a feature when we export to an older KiCad.
@ BLOCK
Cannot be represented at all, export to this target is not possible.
@ LOWER
Approximated down to a simpler equivalent.
@ DROP
No equivalent, left out.
static bool empty(const wxTextEntryBase *aCtrl)
wxString DowngradeLibraryFilesInPlace(const wxString &aDir, const wxString &aGlob, int aStampVersion, const std::function< wxString(const wxString &)> &aFindForbidden, const std::function< DOWNGRADE_FILE_RESULT(const wxString &, const wxString &)> &aConvert, DOWNGRADE_FILE_TRANSACTION *aTransaction=nullptr, wxString *aRecoveryError=nullptr)
Downgrade every library file matching aGlob under aDir in place.
bool IsDowngradeDesignFile(const wxString &aPath)
Files the project export carries over.
DOWNGRADE_FILE_RESULT
What a per-file converter did with one library file.
@ REFUSED
The target cannot represent this file.
@ CONVERTED
The temp file holds the downgraded copy.
@ TRACK_WIDTH_CONSTRAINT
Definition drc_rule.h:61
@ CLEARANCE_CONSTRAINT
Definition drc_rule.h:51
DRC_RULES_FILTER_RESULT FilterDrcRulesForTarget(const wxString &aRulesText, const DOWNGRADE_TARGET &aTarget)
Filter a .kicad_dru file for the target.
static constexpr EDA_ANGLE ANGLE_0
Definition eda_angle.h:448
static constexpr EDA_ANGLE ANGLE_90
Definition eda_angle.h:450
static constexpr EDA_ANGLE ANGLE_360
Definition eda_angle.h:454
@ NO_FILL
Definition eda_fill.h:30
@ HATCH
Definition eda_fill.h:34
@ FILLED_SHAPE
Fill with object color.
Definition eda_fill.h:31
@ RECURSE
Definition eda_item.h:51
SHAPE_T
Definition eda_shape.h:54
@ ELLIPSE
Definition eda_shape.h:62
@ SEGMENT
Definition eda_shape.h:56
@ RECTANGLE
Use RECTANGLE instead of RECT to avoid collision in a Windows header.
Definition eda_shape.h:57
@ ELLIPSE_ARC
Definition eda_shape.h:63
@ CUSTOM_LAYERS
Stackup handling where the footprint can have any number of copper layers, and objects on those layer...
Definition footprint.h:166
std::string source
PCB_LAYER_ID
A quick note on layer IDs:
Definition layer_ids.h:56
@ 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
@ F_Cu
Definition layer_ids.h:60
Test utilities for timestamps.
std::string GetPcbnewTestDataDir()
Utility which returns a path to the data directory where the test board files are stored.
std::string ReadGoldenText(const std::string &aPath)
wxMemoryBuffer MakePngWithFractionalPixelsPerCm()
A tiny PNG whose embedded resolution gives a fractional pixels/cm, so the legacy truncating PPI diffe...
double SerializedImageScale(const std::string &aContent, const std::string &aUuid)
EDA_ANGLE abs(const EDA_ANGLE &aAngle)
Definition eda_angle.h:437
@ PTH
Plated through hole pad.
Definition padstack.h:97
@ PRESSFIT
a PTH with a hole diameter with tight tolerances for press fit pin
Definition padstack.h:122
@ NONE
no special fabrication property
Definition padstack.h:114
BARCODE class definition.
@ HORIZONTAL
A segment (or two points) is horizontal.
Class to handle a set of BOARD_ITEMs.
#define SEXPR_BOARD_FILE_VERSION
Current s-expression file format version. 2 was the last legacy format version.
@ STITCH
Fill the outline with vias with a pattern.
@ GUARD
Place vias to guard a net contained within.
void DowngradeProjectFileJson(nlohmann::json &aDoc, const DOWNGRADE_TARGET &aTarget, COMPATIBILITY_REPORT &aReport)
Migrate a .kicad_pro document down to what the target release expects.
@ RPT_SEVERITY_ERROR
One line in the compatibility report.
An older KiCad release we can export a project back to.
wxString m_text
The filtered rules file text.
wxString m_error
Invalid source syntax; do not write m_text.
std::vector< wxString > m_dropped
Names of the rules the target cannot parse.
std::optional< bool > is_capped
True if the drill hole should be capped.
Definition padstack.h:279
std::optional< bool > is_filled
True if the drill hole should be filled completely.
Definition padstack.h:278
std::optional< bool > has_covering
True if the pad on this side should have covering.
Definition padstack.h:261
std::optional< bool > has_plugging
True if the drill hole should be plugged on this side.
Definition padstack.h:262
std::optional< VECTOR2I > hatching_offset
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_CASE(EmptyBoardIsClean)
static void checkFeature(const COMPATIBILITY_REPORT &aReport, const wxString &aName, DOWNGRADE_BUCKET aBucket, int aCount)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
std::string path
IbisParser parser & reporter
VECTOR3I expected(15, 30, 45)
static const DOWNGRADE_TARGET & kicad10
static const DOWNGRADE_TARGET & kicad9
BOOST_TEST_CONTEXT("Test Clearance")
BOOST_TEST_MESSAGE("Polyline has "<< chain.PointCount()<< " points")
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")
#define M_PI
@ PCB_SHAPE_T
class PCB_SHAPE, a segment not on copper layers
Definition typeinfo.h:80
@ PCB_VIA_T
class PCB_VIA, a via (like a track segment on a copper layer)
Definition typeinfo.h:89
@ PCB_BARCODE_T
class PCB_BARCODE, a barcode (graphic item)
Definition typeinfo.h:93
@ PCB_TRACE_T
class PCB_TRACK, a track segment (segment on a copper layer)
Definition typeinfo.h:88
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:708
Class ZONE_SETTINGS used to handle zones parameters in dialogs.