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#include <qa_utils/file_utils.h>
79
80
81BOOST_AUTO_TEST_SUITE( BoardDowngrade )
82
83static const DOWNGRADE_TARGET& kicad9 = KI_TEST::RequireDowngradeTarget( wxT( "9.0" ) );
84static const DOWNGRADE_TARGET& kicad10 = KI_TEST::RequireDowngradeTarget( wxT( "10.0" ) );
85
86
87// A board with no post-10.0 features exports to 10.0 with nothing lost.
88BOOST_AUTO_TEST_CASE( EmptyBoardIsClean )
89{
90 BOARD board;
91
93
94 BOOST_CHECK( !report.IsLossy() );
95 BOOST_CHECK( !report.IsBlocked() );
96 BOOST_CHECK( report.Entries().empty() );
97}
98
99
100// The ellipse primitive only exists since 20260508, so 10.0 gets an approximation.
101BOOST_AUTO_TEST_CASE( EllipseIsLoweredForKicad10 )
102{
103 BOARD board;
104 board.Add( new PCB_SHAPE( &board, SHAPE_T::ELLIPSE ) );
105
107
108 BOOST_CHECK( report.IsLossy() );
110}
111
112
113// A target that already has the ellipse primitive keeps it. This proves the version gate.
114BOOST_AUTO_TEST_CASE( EllipseKeptForNewerTarget )
115{
116 BOARD board;
117 board.Add( new PCB_SHAPE( &board, SHAPE_T::ELLIPSE ) );
118
119 DOWNGRADE_TARGET future = { wxT( "future" ), wxT( "future" ), 20260623, 20260629, 20260629 };
120 COMPATIBILITY_REPORT report = ClassifyBoardForDowngrade( &board, future );
121
122 BOOST_CHECK( !report.IsLossy() );
123}
124
125
126BOOST_AUTO_TEST_CASE( EllipseTransformedToPolygon )
127{
128 BOARD board;
129 PCB_SHAPE* ellipse = new PCB_SHAPE( &board, SHAPE_T::ELLIPSE );
130 ellipse->SetStart( VECTOR2I( 0, 0 ) );
131 ellipse->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 5 ), 0 ) );
132 board.Add( ellipse );
133
135
136 BOOST_CHECK( ellipse->GetShape() == SHAPE_T::POLY );
137}
138
139
140// An unfilled ellipse must be lowered to its outline polygon, not to stroke debris.
141BOOST_AUTO_TEST_CASE( UnfilledEllipseKeepsOutline )
142{
143 BOARD board;
144 PCB_SHAPE* ellipse = new PCB_SHAPE( &board, SHAPE_T::ELLIPSE );
145 ellipse->SetEllipseCenter( VECTOR2I( 0, 0 ) );
146 ellipse->SetEllipseMajorRadius( pcbIUScale.mmToIU( 5 ) );
147 ellipse->SetEllipseMinorRadius( pcbIUScale.mmToIU( 3 ) );
148 ellipse->SetFillMode( FILL_T::NO_FILL );
149 ellipse->SetWidth( pcbIUScale.mmToIU( 0.2 ) );
150 board.Add( ellipse );
151
153
154 BOOST_REQUIRE( ellipse->GetShape() == SHAPE_T::POLY );
156 BOOST_CHECK( ellipse->GetFillMode() == FILL_T::NO_FILL );
157
158 double areaMM =
159 std::abs( ellipse->GetPolyShape().Outline( 0 ).Area() ) / ( pcbIUScale.IU_PER_MM * pcbIUScale.IU_PER_MM );
160
161 BOOST_CHECK_CLOSE( areaMM, M_PI * 5.0 * 3.0, 2.0 );
162}
163
164
165// An elliptical arc is an open curve, so it is lowered to a filled polygon of its ink.
166BOOST_AUTO_TEST_CASE( EllipseArcKeepsInk )
167{
168 BOARD board;
169 PCB_SHAPE* arc = new PCB_SHAPE( &board, SHAPE_T::ELLIPSE_ARC );
170 arc->SetEllipseCenter( VECTOR2I( 0, 0 ) );
171 arc->SetEllipseMajorRadius( pcbIUScale.mmToIU( 5 ) );
172 arc->SetEllipseMinorRadius( pcbIUScale.mmToIU( 3 ) );
175 arc->SetWidth( pcbIUScale.mmToIU( 0.3 ) );
176 board.Add( arc );
177
179
181
182 // A quarter arc of a 5 mm by 3 mm ellipse is about 6.4 mm of path at 0.3 mm width.
183 double areaMM = std::abs( arc->GetPolyShape().Area() ) / ( pcbIUScale.IU_PER_MM * pcbIUScale.IU_PER_MM );
184
185 BOOST_CHECK_GT( areaMM, 1.5 );
186 BOOST_CHECK_LT( areaMM, 2.6 );
187}
188
189
190// Text box knockout (20250210) postdates 9.0, so the flag is dropped and reported for 9.0.
191BOOST_AUTO_TEST_CASE( KnockoutTextBoxLoweredForKicad9 )
192{
193 BOARD board;
194 PCB_TEXTBOX* textbox = new PCB_TEXTBOX( &board );
195 textbox->SetIsKnockout( true );
196 board.Add( textbox );
197
200
201 DowngradeBoardInPlace( &board, kicad9 );
202
203 BOOST_CHECK( !textbox->IsKnockout() );
204}
205
206
207// Table cell knockout (20260603) postdates both targets, so the flag is dropped for 10.0.
208BOOST_AUTO_TEST_CASE( TableCellKnockoutLoweredForKicad10 )
209{
210 BOARD board;
211 PCB_TABLE* table = new PCB_TABLE( &board, pcbIUScale.mmToIU( 0.1 ) );
212 table->SetColCount( 1 );
213
214 PCB_TABLECELL* cell = new PCB_TABLECELL( table );
215 cell->SetIsKnockout( true );
216 table->AddCell( cell );
217 board.Add( table );
218
219 // A table inside a footprint carries the same flag and must be counted and lowered too.
220 FOOTPRINT* fp = new FOOTPRINT( &board );
221 PCB_TABLE* fpTable = new PCB_TABLE( fp, pcbIUScale.mmToIU( 0.1 ) );
222 fpTable->SetColCount( 1 );
223
224 PCB_TABLECELL* fpCell = new PCB_TABLECELL( fpTable );
225 fpCell->SetIsKnockout( true );
226 fpTable->AddCell( fpCell );
227 fp->Add( fpTable );
228 board.Add( fp );
229
231
233
234 BOOST_CHECK( !cell->IsKnockout() );
235 BOOST_CHECK( !fpCell->IsKnockout() );
236}
237
238
239// A knockout cell in a library footprint goes through the footprint-scoped rule.
240BOOST_AUTO_TEST_CASE( FootprintTableCellKnockoutLowered )
241{
242 FOOTPRINT* fp = new FOOTPRINT( nullptr );
243 PCB_TABLE* table = new PCB_TABLE( fp, pcbIUScale.mmToIU( 0.1 ) );
244 table->SetColCount( 1 );
245
246 PCB_TABLECELL* cell = new PCB_TABLECELL( table );
247 cell->SetIsKnockout( true );
248 table->AddCell( cell );
249 fp->Add( table );
250
252
253 BOOST_CHECK( !cell->IsKnockout() );
254 delete fp;
255}
256
257
258// Rounded rectangles (20250829) postdate 9.0, so 9.0 gets a polygon that keeps the rounding.
259BOOST_AUTO_TEST_CASE( RoundedRectangleLoweredForKicad9 )
260{
261 BOARD board;
262 PCB_SHAPE* rect = new PCB_SHAPE( &board, SHAPE_T::RECTANGLE );
263 rect->SetStart( VECTOR2I( 0, 0 ) );
264 rect->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 6 ) ) );
265 rect->SetCornerRadius( pcbIUScale.mmToIU( 1 ) );
267 rect->SetWidth( pcbIUScale.mmToIU( 0.2 ) );
268 board.Add( rect );
269
272
273 DowngradeBoardInPlace( &board, kicad9 );
274
277
278 // A 10 mm by 6 mm rectangle loses ( 4 - pi ) r^2 to the rounded corners.
279 double areaMM =
280 std::abs( rect->GetPolyShape().Outline( 0 ).Area() ) / ( pcbIUScale.IU_PER_MM * pcbIUScale.IU_PER_MM );
281
282 BOOST_CHECK_CLOSE( areaMM, 60.0 - ( 4.0 - M_PI ), 2.0 );
283}
284
285
286// A custom footprint stackup (20250818) remaps inner layers. 9.0 would expand them and change
287// the connectivity, so the export must block.
288BOOST_AUTO_TEST_CASE( CustomFootprintStackupBlocksKicad9 )
289{
290 BOARD board;
291 FOOTPRINT* fp = new FOOTPRINT( &board );
293 board.Add( fp );
294
297}
298
299
300// Per-zone layer properties (20250302) postdate 9.0, so they are dropped like the board-level
301// zone defaults.
302BOOST_AUTO_TEST_CASE( ZoneLayerPropertiesDroppedForKicad9 )
303{
304 BOARD board;
305 ZONE* zone = new ZONE( &board );
306 zone->LayerProperties()[F_Cu].hatching_offset = VECTOR2I( 100, 100 );
307 board.Add( zone );
308
309 // A zone inside a footprint carries the same properties and must be counted and dropped too.
310 FOOTPRINT* fp = new FOOTPRINT( &board );
311 ZONE* fpZone = new ZONE( fp );
312 fpZone->LayerProperties()[F_Cu].hatching_offset = VECTOR2I( 100, 100 );
313 fp->Add( fpZone );
314 board.Add( fp );
315
317
318 DowngradeBoardInPlace( &board, kicad9 );
319
320 BOOST_CHECK( zone->LayerProperties().empty() );
321 BOOST_CHECK( fpZone->LayerProperties().empty() );
322}
323
324
325BOOST_AUTO_TEST_CASE( BoardZoneDefaultsRoundTripAndDowngrade )
326{
327 SETTINGS_MANAGER settingsManager;
328 BOARD board;
329 board.GetDesignSettings().m_ZoneLayerProperties[F_Cu].hatching_offset = VECTOR2I( 100000, 200000 );
330 board.GetDesignSettings().m_ZoneLayerProperties[B_Cu].hatching_offset = VECTOR2I( 300000, 400000 );
331
332 KI_TEST::SCOPED_TEMP_DIR tmp( "kicad_qa_board_zone_defaults" );
333 const std::string src = ( tmp.Path() / "source.kicad_pcb" ).string();
335 io.SaveBoard( src, board );
336 const std::string original = KI_TEST::ReadGoldenText( src );
337 std::unique_ptr<BOARD> current( io.LoadBoard( src, nullptr ) );
338 BOOST_REQUIRE( current );
339 const auto& loadedDefaults = current->GetDesignSettings().m_ZoneLayerProperties;
340 BOOST_REQUIRE_EQUAL( loadedDefaults.size(), 2 );
341 BOOST_CHECK( loadedDefaults.at( F_Cu ).hatching_offset == VECTOR2I( 100000, 200000 ) );
342 BOOST_CHECK( loadedDefaults.at( B_Cu ).hatching_offset == VECTOR2I( 300000, 400000 ) );
343 BOOST_CHECK( current->GetDesignSettings().GetDefaultZoneSettings().m_LayerProperties.empty() );
344
345 for( const auto& target : { kicad9, kicad10 } )
346 {
348 const std::string dest = ( tmp.Path() / ( target.m_id.ToStdString() + ".kicad_pcb" ) ).string();
349 BOOST_REQUIRE( ExportBoardToOlderVersion( src, dest, target, report ) );
350 std::unique_ptr<BOARD> reloaded( io.LoadBoard( dest, nullptr ) );
351 BOOST_REQUIRE( reloaded );
352 const auto& defaults = reloaded->GetDesignSettings().m_ZoneLayerProperties;
353
354 if( target.m_id == wxT( "9.0" ) )
355 {
357 BOOST_CHECK( defaults.empty() );
358 BOOST_CHECK( KI_TEST::ReadGoldenText( dest ).find( "zone_defaults" ) == std::string::npos );
359 }
360 else
361 {
362 BOOST_CHECK( !report.IsLossy() );
363 BOOST_REQUIRE_EQUAL( defaults.size(), 2 );
364 BOOST_CHECK( defaults.at( F_Cu ).hatching_offset == loadedDefaults.at( F_Cu ).hatching_offset );
365 BOOST_CHECK( defaults.at( B_Cu ).hatching_offset == loadedDefaults.at( B_Cu ).hatching_offset );
366 }
367
368 BOOST_CHECK( KI_TEST::ReadGoldenText( src ) == original );
369 }
370}
371
372
373// The old (at) header cannot represent a scaled footprint, so the export must block.
374BOOST_AUTO_TEST_CASE( ScaledFootprintBlocksExport )
375{
376 BOARD board;
377 FOOTPRINT* fp = new FOOTPRINT( &board );
378 fp->SetTransformScale( 2.0, 1.0 );
379 board.Add( fp );
380
382
383 BOOST_CHECK( report.IsBlocked() );
385}
386
387
388// IPC-4761 via protection postdates 9.0, so it is dropped and reported for 9.0.
389BOOST_AUTO_TEST_CASE( ViaProtectionDroppedForKicad9 )
390{
391 BOARD board;
392 PCB_VIA* via = new PCB_VIA( &board );
393 via->Padstack().Drill().is_capped = true;
394 board.Add( via );
395
397
398 DowngradeBoardInPlace( &board, kicad9 );
399
400 BOOST_CHECK( !via->Padstack().Drill().is_capped.has_value() );
401}
402
403
404// Frequency-dependent dielectric models postdate both targets, so they are dropped for 10.0.
418
419
420// Group design block links postdate 9.0, so they are dropped and reported for 9.0.
421BOOST_AUTO_TEST_CASE( GroupDesignBlockLinkDroppedForKicad9 )
422{
423 BOARD board;
424 PCB_GROUP* group = new PCB_GROUP( &board );
425 group->SetDesignBlockLibId( LIB_ID( wxT( "Lib" ), wxT( "Block" ) ) );
426 board.Add( group );
427
429
430 DowngradeBoardInPlace( &board, kicad9 );
431
432 BOOST_CHECK( !group->HasDesignBlockLink() );
433}
434
435
436// Extruded 3D bodies (20260410) have no equivalent in 10.0, so they are dropped.
437BOOST_AUTO_TEST_CASE( ExtrudedBodyDroppedForKicad10 )
438{
439 BOARD board;
440 FOOTPRINT* fp = new FOOTPRINT( &board );
441 fp->EnsureBody();
442 board.Add( fp );
443
445
447
448 BOOST_CHECK( !fp->HasBody() );
449}
450
451
452// A refill in the old version would turn a thieving pattern into solid copper, quietly.
453// That is silent corruption, so the export must block.
454BOOST_AUTO_TEST_CASE( ThievingZoneBlocksKicad10 )
455{
456 BOARD board;
457 ZONE* zone = new ZONE( &board );
459 board.Add( zone );
460
462
464
465 BOOST_CHECK( zone->IsCopperThieving() );
466}
467
468
469// Backdrilling changes copper connectivity between layers, so the export must block for 9.0.
470BOOST_AUTO_TEST_CASE( BackdrillBlocksKicad9 )
471{
472 BOARD board;
473 PCB_VIA* via = new PCB_VIA( &board );
474 via->SetSecondaryDrillSize( VECTOR2I( pcbIUScale.mmToIU( 0.3 ), pcbIUScale.mmToIU( 0.3 ) ) );
475 board.Add( via );
476
479}
480
481
482// The headline fail-closed promise: a blocked board export returns failure and writes nothing.
483// The staging name must not be guessable, because the writer resolves symlinks on purpose.
484BOOST_AUTO_TEST_CASE( BoardExportIgnoresAPredictableStagingSymlink )
485{
486 SETTINGS_MANAGER settingsManager;
487
488 const std::string src = KI_TEST::GetPcbnewTestDataDir() + "via_off_center.kicad_pcb";
489
490 KI_TEST::SCOPED_TEMP_DIR tmp( "kicad_qa_staging_symlink", "" );
491 const wxString dest = ( tmp.Path() / "out.kicad_pcb" ).wstring();
492 const wxString victim = ( tmp.Path() / "victim.txt" ).wstring();
493
494 {
495 wxFFile file( victim, wxT( "wb" ) );
496 BOOST_REQUIRE( file.IsOpened() );
497 file.Write( wxT( "untouched" ) );
498 }
499
500 const wxString decoy =
501 dest + wxString::Format( wxT( ".downgrade_tmp.%lu" ), (unsigned long) wxGetProcessId() );
502 std::error_code ec;
503 std::filesystem::create_symlink( victim.ToStdString(), decoy.ToStdString(), ec );
504 BOOST_REQUIRE( !ec );
505
507 BOOST_CHECK( ExportBoardToOlderVersion( src, dest, kicad9, report ) );
508
509 wxString after;
510 {
511 wxFFile file( victim, wxT( "rb" ) );
512 BOOST_REQUIRE( file.IsOpened() && file.ReadAll( &after ) );
513 }
514
515 BOOST_CHECK( after == wxT( "untouched" ) );
516
517 // A normal save in the same directory shares this umask, so the modes must agree.
518 const wxString reference = ( tmp.Path() / "reference.kicad_pcb" ).wstring();
520 std::unique_ptr<BOARD> board( io.LoadBoard( dest, nullptr ) );
521 BOOST_REQUIRE( board );
522 io.SaveBoard( reference, *board );
523
524 BOOST_CHECK( std::filesystem::status( dest.ToStdString() ).permissions()
525 == std::filesystem::status( reference.ToStdString() ).permissions() );
526}
527
528
529BOOST_AUTO_TEST_CASE( BlockedBoardExportWritesNothing )
530{
531 SETTINGS_MANAGER settingsManager;
532
533 BOARD board;
534 ZONE* zone = new ZONE( &board );
535 zone->SetLayer( F_Cu );
536 zone->AppendCorner( VECTOR2I( 0, 0 ), -1 );
537 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 10 ), 0 ), -1 );
538 zone->AppendCorner( VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 10 ) ), -1 );
540 board.Add( zone );
541
542 KI_TEST::SCOPED_TEMP_DIR tmp( "kicad_qa_blocked_export", "" );
543 const wxString src = ( tmp.Path() / "src.kicad_pcb" ).wstring();
544 const wxString dest = ( tmp.Path() / "out.kicad_pcb" ).wstring();
545
547 io.SaveBoard( src, board );
548
550 BOOST_CHECK( !ExportBoardToOlderVersion( src, dest, kicad10, report ) );
551 BOOST_CHECK( report.IsBlocked() );
552 BOOST_CHECK( !wxFileExists( dest ) );
553}
554
555
556// Pad simulation electrical types (20260521) are dropped for 10.0.
557BOOST_AUTO_TEST_CASE( PadSimTypeDroppedForKicad10 )
558{
559 BOARD board;
560 FOOTPRINT* fp = new FOOTPRINT( &board );
561 PAD* pad = new PAD( fp );
562 pad->SetSimElectricalType( PAD_SIM_ELECTRICAL_TYPE::SOURCE );
563 fp->Add( pad );
564 board.Add( fp );
565
567
569
570 BOOST_CHECK( pad->GetSimElectricalType() == PAD_SIM_ELECTRICAL_TYPE::NONE );
571}
572
573
574// Counterbore / countersink is a post-9.0 feature 9.0 cannot parse, so it is dropped for 9.0.
575BOOST_AUTO_TEST_CASE( PostMachiningDroppedForKicad9 )
576{
577 BOARD board;
578 FOOTPRINT* fp = new FOOTPRINT( &board );
579 PAD* pad = new PAD( fp );
580 pad->Padstack().FrontPostMachining().mode = PAD_DRILL_POST_MACHINING_MODE::COUNTERBORE;
581 fp->Add( pad );
582 board.Add( fp );
583
585
586 DowngradeBoardInPlace( &board, kicad9 );
587
588 BOOST_CHECK( !pad->Padstack().FrontPostMachining().mode.has_value() );
589}
590
591
592// Press-fit is a post-9.0 pad property 9.0 cannot parse, so it is dropped for 9.0.
593BOOST_AUTO_TEST_CASE( PressFitDroppedForKicad9 )
594{
595 BOARD board;
596 FOOTPRINT* fp = new FOOTPRINT( &board );
597 PAD* pad = new PAD( fp );
598 pad->SetProperty( PAD_PROP::PRESSFIT );
599 fp->Add( pad );
600 board.Add( fp );
601
603
604 DowngradeBoardInPlace( &board, kicad9 );
605
606 BOOST_CHECK( pad->GetProperty() == PAD_PROP::NONE );
607}
608
609
610// Skip vias are post-9.0, so they are approximated by the nearest mode 9.0 understands.
611BOOST_AUTO_TEST_CASE( SkipViaLoweredForKicad9 )
612{
613 BOARD board;
614 PCB_VIA* via = new PCB_VIA( &board );
615 via->Padstack().SetUnconnectedLayerMode( UNCONNECTED_LAYER_MODE::START_END_ONLY );
616 board.Add( via );
617
619
620 DowngradeBoardInPlace( &board, kicad9 );
621
622 BOOST_CHECK( via->Padstack().UnconnectedLayerMode() == UNCONNECTED_LAYER_MODE::REMOVE_EXCEPT_START_AND_END );
623}
624
625
626// The v10 writer must not move anything. Every footprint and pad position in the export must match
627// the source exactly. This guards the extracted writer against silent model drift.
628BOOST_AUTO_TEST_CASE( ExportToKicad10PreservesGeometry )
629{
630 SETTINGS_MANAGER settingsManager;
631
632 const std::string src = KI_TEST::GetPcbnewTestDataDir() + "via_off_center.kicad_pcb";
633
634 KI_TEST::SCOPED_TEMP_DIR tmp( "kicad_qa_downgrade_geom", "" );
635 const std::string dest = ( tmp.Path() / "out.kicad_pcb" ).string();
636
638 BOOST_REQUIRE( ExportBoardToOlderVersion( src, dest, kicad10, report ) );
639
641 std::unique_ptr<BOARD> source( io.LoadBoard( src, nullptr ) );
642 std::unique_ptr<BOARD> exported( io.LoadBoard( dest, nullptr ) );
643
644 BOOST_REQUIRE( source && exported );
645 BOOST_REQUIRE_EQUAL( source->Footprints().size(), exported->Footprints().size() );
646
647 for( size_t i = 0; i < source->Footprints().size(); i++ )
648 {
649 FOOTPRINT* a = source->Footprints()[i];
650 FOOTPRINT* b = exported->Footprints()[i];
651
653 BOOST_CHECK( a->GetPosition() == b->GetPosition() );
655
656 BOOST_REQUIRE_EQUAL( a->Pads().size(), b->Pads().size() );
657
658 for( size_t p = 0; p < a->Pads().size(); p++ )
659 BOOST_CHECK( a->Pads()[p]->GetPosition() == b->Pads()[p]->GetPosition() );
660 }
661}
662
663
664// Same geometry guard for the v9 writer.
665BOOST_AUTO_TEST_CASE( ExportToKicad9PreservesGeometry )
666{
667 SETTINGS_MANAGER settingsManager;
668
669 const std::string src = KI_TEST::GetPcbnewTestDataDir() + "via_off_center.kicad_pcb";
670
671 KI_TEST::SCOPED_TEMP_DIR tmp( "kicad_qa_downgrade_geom9", "" );
672 const std::string dest = ( tmp.Path() / "out.kicad_pcb" ).string();
673
675 BOOST_REQUIRE( ExportBoardToOlderVersion( src, dest, kicad9, report ) );
676
678 std::unique_ptr<BOARD> source( io.LoadBoard( src, nullptr ) );
679 std::unique_ptr<BOARD> exported( io.LoadBoard( dest, nullptr ) );
680
681 BOOST_REQUIRE( source && exported );
682 BOOST_REQUIRE_EQUAL( source->Footprints().size(), exported->Footprints().size() );
683
684 for( size_t i = 0; i < source->Footprints().size(); i++ )
685 {
686 FOOTPRINT* a = source->Footprints()[i];
687 FOOTPRINT* b = exported->Footprints()[i];
688
689 BOOST_CHECK( a->GetPosition() == b->GetPosition() );
690
691 BOOST_REQUIRE_EQUAL( a->Pads().size(), b->Pads().size() );
692
693 for( size_t pd = 0; pd < a->Pads().size(); pd++ )
694 BOOST_CHECK( a->Pads()[pd]->GetPosition() == b->Pads()[pd]->GetPosition() );
695 }
696}
697
698
699// Buried vias postdate 9.0's blind/buried split, so they are written with the old combined type.
700BOOST_AUTO_TEST_CASE( BuriedViaLoweredForKicad9 )
701{
702 BOARD board;
703 PCB_VIA* via = new PCB_VIA( &board );
704 via->SetViaType( VIATYPE::BURIED );
705 board.Add( via );
706
708
709 DowngradeBoardInPlace( &board, kicad9 );
710
711 BOOST_CHECK( via->GetViaType() == VIATYPE::BLIND );
712}
713
714
715// Per-layer zone defaults are a post-9.0 setup block, so they are dropped for 9.0.
716BOOST_AUTO_TEST_CASE( ZoneDefaultsDroppedForKicad9 )
717{
718 BOARD board;
720
722
723 DowngradeBoardInPlace( &board, kicad9 );
724
726}
727
728
729// A barcode is fabricated ink, so it becomes its rendered polygons instead of vanishing.
730BOOST_AUTO_TEST_CASE( BarcodeLoweredToPolygonsForKicad9 )
731{
732 BOARD board;
733 PCB_BARCODE* barcode = new PCB_BARCODE( &board );
734 barcode->SetLayer( F_SilkS );
735 barcode->SetWidth( pcbIUScale.mmToIU( 10 ) );
736 barcode->SetHeight( pcbIUScale.mmToIU( 10 ) );
737 barcode->SetBarcodeText( wxT( "TEST123" ) );
738 board.Add( barcode );
739
742
743 // The net printed area, holes subtracted. A finder ring must not become a solid square.
745 barcode->TransformShapeToPolygon( expected, barcode->GetLayer(), 0, 0, ERROR_INSIDE );
746 expected.Simplify();
747 expected.Fracture();
748 double expectedMM = expected.Area() / ( pcbIUScale.IU_PER_MM * pcbIUScale.IU_PER_MM );
749
750 DowngradeBoardInPlace( &board, kicad9 );
751
752 int polyCount = 0;
753 double areaMM = 0;
754
755 for( BOARD_ITEM* item : board.Drawings() )
756 {
757 BOOST_REQUIRE( item->Type() != PCB_BARCODE_T );
758
759 if( item->Type() == PCB_SHAPE_T )
760 {
761 PCB_SHAPE* shape = static_cast<PCB_SHAPE*>( item );
762 BOOST_REQUIRE( shape->GetShape() == SHAPE_T::POLY );
763 BOOST_CHECK( shape->GetLayer() == F_SilkS );
764 polyCount++;
765 areaMM += std::abs( shape->GetPolyShape().Area() ) / ( pcbIUScale.IU_PER_MM * pcbIUScale.IU_PER_MM );
766 }
767 }
768
769 // The printed ink survives: many module polygons with a real total area.
770 BOOST_CHECK_GT( polyCount, 1 );
771 BOOST_CHECK_GT( areaMM, 5.0 );
772 BOOST_CHECK_CLOSE( areaMM, expectedMM, 0.1 );
773}
774
775
776BOOST_AUTO_TEST_CASE( BarcodeProjectVariablesResolvedBeforeExport )
777{
778 KI_TEST::SCOPED_TEMP_DIR tmp( "kicad_qa_barcode_project" );
779 wxString projectPath = ( tmp.Path() / "source.kicad_pro" ).string();
780 wxString boardPath = ( tmp.Path() / "source.kicad_pcb" ).string();
781 wxString outputPath = ( tmp.Path() / "output.kicad_pcb" ).string();
782
783 {
784 std::ofstream project( projectPath.ToStdString() );
785 project << R"({"meta":{"version":3},"text_variables":{"SERIAL":"SERIAL123"}})";
786 }
787
788 SETTINGS_MANAGER settings;
789 BOOST_REQUIRE( settings.LoadProject( projectPath, false ) );
790 BOARD board;
791 board.SetProject( settings.GetProject( projectPath ), true );
792 PCB_BARCODE* barcode = new PCB_BARCODE( &board );
793 barcode->SetLayer( F_SilkS );
795 barcode->SetWidth( pcbIUScale.mmToIU( 10 ) );
796 barcode->SetHeight( pcbIUScale.mmToIU( 10 ) );
797 barcode->SetBarcodeText( wxT( "${SERIAL}" ) );
798 barcode->SetShowText( false );
799 board.Add( barcode );
800 BOOST_REQUIRE_EQUAL( barcode->GetShownText( FOR_CANVAS ), wxString( "SERIAL123" ) );
801
804 expected.Simplify();
805 expected.Fracture();
806 double expectedArea = expected.Area();
807
809 io.SaveBoard( boardPath, board );
811 BOOST_REQUIRE( ExportBoardToOlderVersion( boardPath, outputPath, kicad9, report ) );
812 std::unique_ptr<BOARD> result( io.LoadBoard( outputPath, nullptr ) );
814 int polygonCount = 0;
815 double resultArea = 0;
816
817 for( BOARD_ITEM* item : result->Drawings() )
818 {
819 BOOST_REQUIRE( item->Type() != PCB_BARCODE_T );
820
821 if( item->Type() == PCB_SHAPE_T )
822 {
823 PCB_SHAPE* shape = static_cast<PCB_SHAPE*>( item );
824 BOOST_REQUIRE( shape->GetShape() == SHAPE_T::POLY );
825 polygonCount++;
826 resultArea += std::abs( shape->GetPolyShape().Area() );
827 }
828 }
829
830 BOOST_CHECK_EQUAL( polygonCount, expected.OutlineCount() );
831 BOOST_CHECK_CLOSE( resultArea, expectedArea, 0.01 );
832}
833
834
835// A barcode inside a footprint takes the same conversion through the footprint path.
836BOOST_AUTO_TEST_CASE( FootprintBarcodeLoweredForKicad9 )
837{
838 BOARD board;
839 FOOTPRINT* fp = new FOOTPRINT( &board );
840 PCB_BARCODE* barcode = new PCB_BARCODE( fp );
841 barcode->SetLayer( F_SilkS );
842 barcode->SetWidth( pcbIUScale.mmToIU( 10 ) );
843 barcode->SetHeight( pcbIUScale.mmToIU( 10 ) );
844 barcode->SetBarcodeText( wxT( "TEST123" ) );
845 fp->Add( barcode );
846 board.Add( fp );
847
849
850 DowngradeBoardInPlace( &board, kicad9 );
851
852 int polyCount = 0;
853
854 for( BOARD_ITEM* item : fp->GraphicalItems() )
855 {
856 BOOST_REQUIRE( item->Type() != PCB_BARCODE_T );
857
858 if( item->Type() == PCB_SHAPE_T && static_cast<PCB_SHAPE*>( item )->GetShape() == SHAPE_T::POLY )
859 polyCount++;
860 }
861
862 BOOST_CHECK_GT( polyCount, 1 );
863}
864
865
866// Exercise the real .kicad_mod write path used to downgrade a footprint library, and confirm the
867// result carries the target stamp and reloads cleanly.
868BOOST_AUTO_TEST_CASE( FootprintFileDowngradeRoundTrips )
869{
870 const std::string src = KI_TEST::GetPcbnewTestDataDir() + "issue24045/QFN-24_L4.0-W4.0-P0.50-BL-EP2.6.kicad_mod";
871
872 PCB_IO_KICAD_SEXPR loader;
873 wxString nameOut;
874 std::unique_ptr<FOOTPRINT> fp( loader.ImportFootprint( src, nameOut ) );
875 BOOST_REQUIRE( fp != nullptr );
876
878
879 KI_TEST::SCOPED_TEMP_DIR tmp( "kicad_qa_fp_downgrade", "" );
880 const wxString dest = ( tmp.Path() / "out.kicad_mod" ).wstring();
881
882 SaveFootprintForTarget( fp.get(), dest, kicad9 );
883
884 // The stamp proves the dispatch picked the right writer, not just a parseable one.
885 std::ifstream out( dest.ToStdString() );
886 std::string content( ( std::istreambuf_iterator<char>( out ) ), std::istreambuf_iterator<char>() );
887 BOOST_CHECK( content.find( "(version " + std::to_string( kicad9.m_boardVersion ) + ")" ) != std::string::npos );
888
889 PCB_IO_KICAD_SEXPR reader;
890 wxString reloadName;
891 std::unique_ptr<FOOTPRINT> reloaded( reader.ImportFootprint( dest, reloadName ) );
892 BOOST_CHECK( reloaded != nullptr );
893}
894
895
896// The v10 footprint file path gets the same round-trip check as v9.
897BOOST_AUTO_TEST_CASE( FootprintFileV10RoundTrips )
898{
899 SETTINGS_MANAGER settingsManager;
900
901 const std::string src = KI_TEST::GetPcbnewTestDataDir() + "issue24045/QFN-24_L4.0-W4.0-P0.50-BL-EP2.6.kicad_mod";
902
903 PCB_IO_KICAD_SEXPR loader;
904 wxString nameOut;
905 std::unique_ptr<FOOTPRINT> fp( loader.ImportFootprint( src, nameOut ) );
906 BOOST_REQUIRE( fp != nullptr );
907
909
910 KI_TEST::SCOPED_TEMP_DIR tmp( "kicad_qa_fp_downgrade10", "" );
911 const wxString dest = ( tmp.Path() / "out.kicad_mod" ).wstring();
912
913 SaveFootprintForTarget( fp.get(), dest, kicad10 );
914
915 std::ifstream out( dest.ToStdString() );
916 std::string content( ( std::istreambuf_iterator<char>( out ) ), std::istreambuf_iterator<char>() );
917 BOOST_CHECK( content.find( "(version " + std::to_string( kicad10.m_boardVersion ) + ")" ) != std::string::npos );
918
919 PCB_IO_KICAD_SEXPR reader;
920 wxString reloadName;
921 std::unique_ptr<FOOTPRINT> reloaded( reader.ImportFootprint( dest, reloadName ) );
922 BOOST_CHECK( reloaded != nullptr );
923}
924
925
926// A library footprint carries the same features but is downgraded through its own path.
927BOOST_AUTO_TEST_CASE( FootprintPressFitDroppedForKicad9 )
928{
929 BOARD board;
930 FOOTPRINT* fp = new FOOTPRINT( &board );
931 PAD* pad = new PAD( fp );
932 pad->SetProperty( PAD_PROP::PRESSFIT );
933 fp->Add( pad );
934
936
937 BOOST_CHECK( pad->GetProperty() == PAD_PROP::NONE );
938
939 delete fp;
940}
941
942
943// Saving with a target version writes the older stamp, and reloading reads it back.
944BOOST_AUTO_TEST_CASE( SaveWithTargetVersionStampsOlder )
945{
946 SETTINGS_MANAGER settingsManager;
947
948 BOARD board;
949
950 KI_TEST::SCOPED_TEMP_DIR tmp( "kicad_qa_downgrade", "" );
951 std::string dest = ( tmp.Path() / "out.kicad_pcb" ).string();
952
953 SaveBoardForTarget( &board, dest, kicad10 );
954
955 PCB_IO_KICAD_SEXPR reader;
956 std::unique_ptr<BOARD> reloaded( reader.LoadBoard( dest, nullptr ) );
957
958 BOOST_REQUIRE( reloaded != nullptr );
959 BOOST_CHECK_EQUAL( reloaded->GetFileFormatVersionAtLoad(), kicad10.m_boardVersion );
960}
961
962
963// The fields deque can hold null slots. The writer must skip them like the current one does.
964BOOST_AUTO_TEST_CASE( NullFieldSlotDoesNotCrashV9Writer )
965{
966 SETTINGS_MANAGER settingsManager;
967
968 std::unique_ptr<FOOTPRINT> fp = std::make_unique<FOOTPRINT>( nullptr );
969 fp->GetFields().push_back( nullptr );
970
971 KI_TEST::SCOPED_TEMP_DIR tmp( "kicad_qa_v9_nullfield", "" );
972 const wxString dest = ( tmp.Path() / "out.kicad_mod" ).wstring();
973
974 SaveFootprintForTarget( fp.get(), dest, kicad9 );
975
976 BOOST_CHECK( std::filesystem::exists( dest.ToStdString() ) );
977}
978
979
980// The 10.0 release writer skips group members that are no longer on the board, so the v10
981// snapshot must too. The 9.0 release writer had no such check and its snapshot stays faithful.
982BOOST_AUTO_TEST_CASE( StaleGroupMemberNotWrittenForV10 )
983{
984 SETTINGS_MANAGER settingsManager;
985
986 BOARD board;
987 PCB_SHAPE* onBoard = new PCB_SHAPE( &board, SHAPE_T::SEGMENT );
988 board.Add( onBoard );
989
990 PCB_SHAPE* stale = new PCB_SHAPE( &board, SHAPE_T::SEGMENT );
991
992 PCB_GROUP* group = new PCB_GROUP( &board );
993 group->AddItem( onBoard );
994 group->AddItem( stale );
995 board.Add( group );
996
997 KI_TEST::SCOPED_TEMP_DIR tmp( "kicad_qa_v10_stalegroup", "" );
998 const std::string dest = ( tmp.Path() / "out.kicad_pcb" ).string();
999
1000 SaveBoardForTarget( &board, dest, kicad10 );
1001
1002 std::ifstream out( dest );
1003 std::string content( ( std::istreambuf_iterator<char>( out ) ), std::istreambuf_iterator<char>() );
1004
1005 BOOST_CHECK( content.find( onBoard->m_Uuid.AsString().ToStdString() ) != std::string::npos );
1006 BOOST_CHECK( content.find( stale->m_Uuid.AsString().ToStdString() ) == std::string::npos );
1007
1008 group->RemoveAll();
1009 delete stale;
1010}
1011
1012
1013// A via mode the era writer predates must be written as the nearest era mode, not silently
1014// fall through to keep-all.
1015BOOST_AUTO_TEST_CASE( SkipViaModeWrittenForV9 )
1016{
1017 SETTINGS_MANAGER settingsManager;
1018
1019 BOARD board;
1020 PCB_VIA* via = new PCB_VIA( &board );
1021 via->Padstack().SetUnconnectedLayerMode( UNCONNECTED_LAYER_MODE::START_END_ONLY );
1022 board.Add( via );
1023
1024 KI_TEST::SCOPED_TEMP_DIR tmp( "kicad_qa_v9_skipvia", "" );
1025 const std::string dest = ( tmp.Path() / "out.kicad_pcb" ).string();
1026
1027 SaveBoardForTarget( &board, dest, kicad9 );
1028
1029 std::ifstream out( dest );
1030 std::string content( ( std::istreambuf_iterator<char>( out ) ), std::istreambuf_iterator<char>() );
1031
1032 BOOST_CHECK( content.find( "(remove_unused_layers yes)" ) != std::string::npos );
1033 BOOST_CHECK( content.find( "(keep_end_layers yes)" ) != std::string::npos );
1034
1035 PCB_IO_KICAD_SEXPR reader;
1036 std::unique_ptr<BOARD> reloaded( reader.LoadBoard( dest, nullptr ) );
1037 BOOST_REQUIRE( reloaded );
1038 BOOST_REQUIRE_EQUAL( reloaded->Tracks().size(), 1 );
1039 BOOST_REQUIRE( reloaded->Tracks().front()->Type() == PCB_VIA_T );
1040 const PCB_VIA* reloadedVia = static_cast<const PCB_VIA*>( reloaded->Tracks().front() );
1041 BOOST_CHECK( reloadedVia->Padstack().UnconnectedLayerMode()
1043}
1044
1045
1046// The target computes the image PPI the old truncating way, so the stored scale must be
1047// migrated back or the image renders at the wrong size there.
1048BOOST_AUTO_TEST_CASE( ImageScaleMigratedForV9 )
1049{
1050 SETTINGS_MANAGER settingsManager;
1051
1052 BOARD board;
1053 PCB_REFERENCE_IMAGE* otherImage = new PCB_REFERENCE_IMAGE( &board );
1054 wxMemoryBuffer buf = KI_TEST::MakePngWithFractionalPixelsPerCm();
1055 BOOST_REQUIRE( otherImage->GetReferenceImage().ReadImageFile( buf ) );
1056 otherImage->GetReferenceImage().SetImageScale( 0.5 );
1057 board.Add( otherImage );
1058
1060 REFERENCE_IMAGE& ref = image->GetReferenceImage();
1061
1062 BOOST_REQUIRE( ref.ReadImageFile( buf ) );
1063 ref.SetImageScale( 2.0 );
1064 board.Add( image );
1065
1066 int ppi = ref.GetImage().GetPPI();
1067 int legacyPPI = ref.GetImage().GetLegacyPPI();
1068 BOOST_REQUIRE( legacyPPI > 0 && ppi != legacyPPI );
1069
1070 double originalScale = ref.GetImageScale();
1071
1072 KI_TEST::SCOPED_TEMP_DIR tmp( "kicad_qa_v9_imgscale", "" );
1073 const std::string dest = ( tmp.Path() / "out.kicad_pcb" ).string();
1074
1075 DowngradeBoardInPlace( &board, kicad9 );
1076 SaveBoardForTarget( &board, dest, kicad9 );
1077
1078 std::ifstream out( dest );
1079 std::string content( ( std::istreambuf_iterator<char>( out ) ), std::istreambuf_iterator<char>() );
1080
1081 const double written = KI_TEST::SerializedImageScale( content, image->m_Uuid.AsStdString() );
1082 BOOST_CHECK_CLOSE( written, originalScale * legacyPPI / ppi, 0.1 );
1083 BOOST_CHECK_CLOSE( KI_TEST::SerializedImageScale( content, otherImage->m_Uuid.AsStdString() ),
1084 0.5 * legacyPPI / ppi, 0.1 );
1085}
1086
1087
1088// The target has no way to store an image rotation, so an angled image blocks the export.
1089BOOST_AUTO_TEST_CASE( RotatedReferenceImageBlocks )
1090{
1091 BOARD board;
1093 image->GetReferenceImage().SetOrientation( EDA_ANGLE( 45.0, DEGREES_T ) );
1094 board.Add( image );
1095
1097
1098 BOOST_CHECK( report.IsBlocked() );
1100
1101 // An upright image is still exportable.
1102 image->GetReferenceImage().SetOrientation( ANGLE_0 );
1103 BOOST_CHECK( !ClassifyBoardForDowngrade( &board, kicad10 ).IsBlocked() );
1104}
1105
1106
1107// The target has no way to store an image opacity, so the export proceeds with full opacity.
1108BOOST_AUTO_TEST_CASE( ReferenceImageOpacityIsDropped )
1109{
1110 BOARD board;
1112 image->GetReferenceImage().SetOpacity( 0.4 );
1113 board.Add( image );
1114
1116
1117 BOOST_CHECK( !report.IsBlocked() );
1119
1120 DowngradeBoardInPlace( &board, kicad10 );
1121
1122 BOOST_CHECK_EQUAL( image->GetReferenceImage().GetOpacity(), 1.0 );
1124}
1125
1126
1127// The fail-closed gate: a post-10.0 token surviving in the output is caught, and lookalikes are not.
1128BOOST_AUTO_TEST_CASE( VerifyGateCatchesUnsupportedTokens )
1129{
1130 // An unstripped feature (e.g. an affine transform) must be caught.
1131 BOOST_CHECK( FindUnsupportedBoardToken( wxT( "(footprint (transform (translate 1 2)))" ), kicad10 )
1132 == wxT( "transform" ) );
1133 BOOST_CHECK( FindUnsupportedBoardToken( wxT( "(net_chain \"n\")" ), kicad10 ) == wxT( "net_chain" ) );
1134
1135 // A clean board, and lookalike prefixes, must pass.
1136 BOOST_CHECK( FindUnsupportedBoardToken( wxT( "(kicad_pcb (net 0 \"n\") (gr_line ...))" ), kicad10 ).IsEmpty() );
1137
1138 // Value tokens the node scan cannot see must still be caught for 9.0, but not inside strings.
1139
1140 BOOST_CHECK( FindUnsupportedBoardToken( wxT( "(via buried (at 1 1))" ), kicad9 ) == wxT( "buried" ) );
1141 BOOST_CHECK( FindUnsupportedBoardToken( wxT( "(gr_text \"buried treasure\")" ), kicad9 ).IsEmpty() );
1142}
1143
1144
1145// One unknown keyword makes an old KiCad drop the whole rules file, so the export must
1146// comment out rules the target cannot parse and keep the rest byte-identical.
1147BOOST_AUTO_TEST_CASE( DrcRulesFilterForKicad9 )
1148{
1149 const wxString rules = wxT( "(version 2)\n"
1150 "# keep me\n"
1151 "(rule \"clearance_ok\"\n"
1152 " # chamfer runs at 45deg here\n"
1153 " (constraint clearance (min 0.2mm))\n"
1154 " (condition \"A.NetClass == 'HV'\"))\n"
1155 "(rule \"mask_check\"\n"
1156 " (constraint bridged_mask))\n"
1157 "(rule \"chain_check\"\n"
1158 " (condition \"inNetChain('x')\")\n"
1159 " (constraint clearance (min 0.1mm)))\n"
1160 "(rule \"delay_check\"\n"
1161 " (constraint skew (max 5ps)))\n" );
1162
1164
1165 BOOST_REQUIRE_EQUAL( result.m_dropped.size(), 3 );
1166 BOOST_CHECK_EQUAL( result.m_dropped[0], wxString( "mask_check" ) );
1167 BOOST_CHECK_EQUAL( result.m_dropped[1], wxString( "chain_check" ) );
1168 BOOST_CHECK_EQUAL( result.m_dropped[2], wxString( "delay_check" ) );
1169
1170 BOOST_CHECK( result.m_text.Contains( wxT( "(version 1)" ) ) );
1171 BOOST_CHECK( result.m_text.Contains( wxT( "# keep me" ) ) );
1172 BOOST_CHECK( result.m_text.Contains( wxT( "(rule \"clearance_ok\"" ) ) );
1173 BOOST_CHECK( result.m_text.Contains( wxT( "A.NetClass == 'HV'" ) ) );
1174 BOOST_CHECK( result.m_text.Contains( wxT( "# Not supported by KiCad 9.0:\n"
1175 "# (rule \"mask_check\"\n# (constraint bridged_mask))\n" ) ) );
1176 BOOST_CHECK( result.m_text.Contains( wxT( "# (rule \"chain_check\"\n# (condition \"inNetChain('x')\")\n" ) ) );
1177 BOOST_CHECK( result.m_text.Contains( wxT( "# (rule \"delay_check\"\n# (constraint skew (max 5ps)))\n" ) ) );
1178}
1179
1180
1181static size_t ParsedDrcRuleCount( const wxString& aText, const wxString& aSource )
1182{
1183 DRC_RULES_PARSER parser( aText, aSource );
1185 std::vector<std::shared_ptr<DRC_RULE>> rules;
1186 parser.Parse( rules, &reporter );
1187 BOOST_CHECK_MESSAGE( !reporter.HasMessageOfSeverity( RPT_SEVERITY_ERROR ), reporter.GetMessages() );
1188 return rules.size();
1189}
1190
1191
1192BOOST_AUTO_TEST_CASE( DroppedRulesStayInTheFileAsComments )
1193{
1194 const wxString rules = wxT( "(version 2)\n"
1195 "(rule \"keep\" (constraint clearance (min 0.2mm)))\n"
1196 "# why the chain rule exists\n"
1197 "(rule \"chain\"\n"
1198 " (constraint net_chain_length (max 10mm)))\n"
1199 "(rule \"glued\" (constraint net_chain_length (max 1mm)))"
1200 "(rule \"after\" (constraint clearance (min 0.1mm)))\n" );
1201
1202 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
1203 {
1205 BOOST_REQUIRE( result.m_error.IsEmpty() );
1206 BOOST_REQUIRE_EQUAL( result.m_dropped.size(), 2 );
1207
1208 const wxString marker = wxT( "# Not supported by " ) + target.m_name + wxT( ":\n" );
1209 const wxString expected = wxT( "(version 1)\n"
1210 "(rule \"keep\" (constraint clearance (min 0.2mm)))\n"
1211 "# why the chain rule exists\n" )
1212 + marker
1213 + wxT( "# (rule \"chain\"\n"
1214 "# (constraint net_chain_length (max 10mm)))\n" )
1215 + marker
1216 + wxT( "# (rule \"glued\" (constraint net_chain_length (max 1mm)))\n"
1217 "(rule \"after\" (constraint clearance (min 0.1mm)))\n" );
1219 BOOST_CHECK_EQUAL( ParsedDrcRuleCount( result.m_text, wxT( "downgrade_commented_rules" ) ), 2 );
1220 }
1221}
1222
1223
1224BOOST_AUTO_TEST_CASE( CommentedRuleStartsAndEndsOnItsOwnLine )
1225{
1226 const wxString rules = wxT( "(version 2)"
1227 "(rule \"first\" (constraint net_chain_length (max 1mm)))"
1228 "(rule \"kept\" (constraint clearance (min 0.2mm)))"
1229 "(rule \"last\" (constraint net_chain_length (max 1mm)))" );
1230
1231 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
1232 {
1234 BOOST_REQUIRE( result.m_error.IsEmpty() );
1235
1236 const wxString marker = wxT( "# Not supported by " ) + target.m_name + wxT( ":\n" );
1237 const wxString expected = wxT( "(version 1)\n" ) + marker
1238 + wxT( "# (rule \"first\" (constraint net_chain_length (max 1mm)))\n"
1239 "(rule \"kept\" (constraint clearance (min 0.2mm)))\n" )
1240 + marker + wxT( "# (rule \"last\" (constraint net_chain_length (max 1mm)))\n" );
1242 BOOST_CHECK_EQUAL( ParsedDrcRuleCount( result.m_text, wxT( "downgrade_glued_rules" ) ), 1 );
1243 }
1244}
1245
1246
1247BOOST_AUTO_TEST_CASE( CommentedRuleKeepsItsOwnCommentsAndLineEndings )
1248{
1249 const wxString rules = wxT( "(version 2)\r\n"
1250 " (rule \"messy\"\r\n"
1251 " # ) \" ( unmatched in a comment\r\n"
1252 " (condition \"A.NetName == 'a'\")\r\n"
1253 " (constraint net_chain_length (max 1mm)))\r\n"
1254 "(rule \"kept\" (constraint clearance (min 0.2mm)))\r\n" );
1255
1256 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
1257 {
1259 BOOST_REQUIRE( result.m_error.IsEmpty() );
1260 BOOST_REQUIRE_EQUAL( result.m_dropped.size(), 1 );
1261 BOOST_CHECK( result.m_dropped.front() == wxT( "messy" ) );
1262
1263 const wxString expected = wxT( "(version 1)\r\n # Not supported by " ) + target.m_name
1264 + wxT( ":\r\n"
1265 "# (rule \"messy\"\r\n"
1266 "# # ) \" ( unmatched in a comment\r\n"
1267 "# (condition \"A.NetName == 'a'\")\r\n"
1268 "# (constraint net_chain_length (max 1mm)))\r\n"
1269 "(rule \"kept\" (constraint clearance (min 0.2mm)))\r\n" );
1271 BOOST_CHECK_EQUAL( ParsedDrcRuleCount( result.m_text, wxT( "downgrade_crlf_rules" ) ), 1 );
1272 }
1273}
1274
1275
1276BOOST_AUTO_TEST_CASE( UnknownTopLevelBlockIsCommentedOut )
1277{
1278 const wxString rules = wxT( "(version 2)\n"
1279 "(future_block \"x\" (constraint clearance (min 1mm)))\n"
1280 "(rule \"kept\" (constraint clearance (min 0.2mm)))\n" );
1281
1282 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
1283 {
1285 BOOST_REQUIRE( result.m_error.IsEmpty() );
1286 BOOST_CHECK_EQUAL( result.m_dropped.size(), 1 );
1287 BOOST_CHECK( result.m_text.Contains( wxT( "# (future_block \"x\" (constraint clearance (min 1mm)))\n"
1288 "(rule \"kept\"" ) ) );
1289 BOOST_CHECK_EQUAL( ParsedDrcRuleCount( result.m_text, wxT( "downgrade_unknown_block" ) ), 1 );
1290 }
1291}
1292
1293
1294BOOST_AUTO_TEST_CASE( ExportedRulesFileDowngradesAgainUnchanged )
1295{
1296 const wxString rules = wxT( "(version 2)\n"
1297 "# why the chain rule exists\n"
1298 "(rule \"chain\"\n"
1299 " # ) \" (\n"
1300 " (constraint net_chain_length (max 10mm)))\n"
1301 "(rule \"after\" (constraint clearance (min 0.1mm)))"
1302 "(rule \"last\" (constraint net_chain_length (max 1mm)))\n"
1303 "# trailing note\n" );
1304
1305 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
1306 {
1307 DRC_RULES_FILTER_RESULT first = FilterDrcRulesForTarget( rules, target );
1308 BOOST_REQUIRE( first.m_error.IsEmpty() );
1309 BOOST_REQUIRE_EQUAL( first.m_dropped.size(), 2 );
1310 BOOST_CHECK( first.m_text.EndsWith( wxT( "# (rule \"last\" (constraint net_chain_length (max 1mm)))\n"
1311 "# trailing note\n" ) ) );
1312
1313 DRC_RULES_FILTER_RESULT second = FilterDrcRulesForTarget( first.m_text, target );
1314 BOOST_CHECK( second.m_error.IsEmpty() );
1315 BOOST_CHECK( second.m_dropped.empty() );
1316 BOOST_CHECK_EQUAL( second.m_text, first.m_text );
1317 }
1318}
1319
1320
1321BOOST_AUTO_TEST_CASE( DrcRuleNamesAndExpressionLiteralsDoNotTriggerDrops )
1322{
1323 const wxString rules = wxT( "(version 1)\n"
1324 "(rule \"return_path\" (constraint clearance (min 0.2mm)))\n"
1325 "(rule inNetChain (constraint clearance (min 0.2mm)))\n"
1326 "(rule \"literal\" (condition \"A.NetName == 'hasNetChain() 100 ps .Parent.'\")\n"
1327 " (constraint clearance (min 0.2mm)))\n" );
1328
1329 for( const DOWNGRADE_TARGET* target : { &kicad9, &kicad10 } )
1330 {
1332 BOOST_CHECK( result.m_error.IsEmpty() );
1333 BOOST_CHECK( result.m_dropped.empty() );
1334 BOOST_CHECK_EQUAL( result.m_text.Mid( result.m_text.Find( "(rule" ) ), rules.Mid( rules.Find( "(rule" ) ) );
1335 }
1336}
1337
1338
1339BOOST_AUTO_TEST_CASE( SpacedTimeAndAngleUnitsAreDroppedForKicad9 )
1340{
1341 const wxString rules = wxT( "(version 1)\n"
1342 "(rule \"time\" (constraint length (min \"100 ps\")))\n"
1343 "(rule \"femtoseconds\" (constraint length (max \"2 * 100 fs\")))\n"
1344 "(rule \"angle\" (constraint track_angle (max \"90 deg\")))\n"
1345 "(rule \"ordinary\" (constraint clearance (min \"0.2 mm\")))\n" );
1346
1348 BOOST_CHECK( result9.m_error.IsEmpty() );
1349 BOOST_CHECK_EQUAL( result9.m_dropped.size(), 3 );
1350 BOOST_CHECK( result9.m_text.Contains( "ordinary" ) );
1351
1353 BOOST_CHECK( result10.m_error.IsEmpty() );
1354 BOOST_CHECK( result10.m_dropped.empty() );
1355 BOOST_CHECK_EQUAL( result10.m_text.Mid( result10.m_text.Find( "(rule" ) ), rules.Mid( rules.Find( "(rule" ) ) );
1356}
1357
1358
1359// 10.0 knows the mask constraints and time units. Only post-10.0 syntax drops for it.
1360BOOST_AUTO_TEST_CASE( DrcRulesFilterForKicad10 )
1361{
1362 const wxString rules = wxT( "(version 2)\n"
1363 "(rule \"mask_check\" (constraint bridged_mask))\n"
1364 "(rule \"delay_check\" (constraint skew (max 5ps)))\n"
1365 "(rule \"chain_check\" (constraint net_chain_length (max 10mm)))\n" );
1366
1368
1369 BOOST_REQUIRE_EQUAL( result.m_dropped.size(), 1 );
1370 BOOST_CHECK_EQUAL( result.m_dropped[0], wxString( "chain_check" ) );
1371 BOOST_CHECK( result.m_text.Contains( wxT( "bridged_mask" ) ) );
1372 BOOST_CHECK( result.m_text.Contains( wxT( "5ps" ) ) );
1373}
1374
1375
1376// Comment syntax must never participate in balancing a rule. Parentheses or quotes
1377// in a full-line comment used to truncate or swallow otherwise valid rules.
1378BOOST_AUTO_TEST_CASE( DrcRulesWithCommentsRemainIntact )
1379{
1380 const wxString kept = wxT( "(rule \"clearance # literal\"\n"
1381 " # unmatched: ) (( \" \\\n"
1382 " (condition \"A.NetName == 'SIGNAL#1'\")\n"
1383 " # inNetChain('x')\n"
1384 " (constraint clearance (min 0.2mm))\n"
1385 " # ) \" (\n"
1386 ")\n"
1387 "(rule \"second\" (constraint track_width (min 0.25mm)))" );
1388 const wxString input = wxT( "(version 2)\n" ) + kept
1389 + wxT( "\n(rule \"unsupported\"\n # ) \" (\n"
1390 " (constraint net_chain_length (max 10mm)))\n" );
1391
1392 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
1393 {
1395 BOOST_REQUIRE( result.m_error.IsEmpty() );
1396 BOOST_REQUIRE_EQUAL( result.m_dropped.size(), 1 );
1397 BOOST_CHECK( result.m_dropped.front() == wxT( "unsupported" ) );
1398 BOOST_CHECK( result.m_text.Contains( kept ) );
1399
1400 DRC_RULES_PARSER parser( result.m_text, wxT( "downgrade_inline_comments" ) );
1402 std::vector<std::shared_ptr<DRC_RULE>> rules;
1403 parser.Parse( rules, &reporter );
1404 BOOST_CHECK_MESSAGE( !reporter.HasMessageOfSeverity( RPT_SEVERITY_ERROR ), reporter.GetMessages() );
1405 BOOST_REQUIRE_EQUAL( rules.size(), 2 );
1406 BOOST_CHECK( rules[0]->FindConstraint( CLEARANCE_CONSTRAINT ).has_value() );
1407 BOOST_CHECK( rules[1]->FindConstraint( TRACK_WIDTH_CONSTRAINT ).has_value() );
1408 }
1409}
1410
1411
1412BOOST_AUTO_TEST_CASE( CommentsAboveTheVersionHeaderAreKept )
1413{
1414 const wxString rules = wxT( "# project rules\n"
1415 "(version 2)\n"
1416 "(rule \"a\" (constraint clearance (min 0.2mm)))\n" );
1417
1419 BOOST_REQUIRE( result.m_error.IsEmpty() );
1420 BOOST_CHECK_EQUAL( result.m_text, wxString( "# project rules\n"
1421 "(version 1)\n"
1422 "(rule \"a\" (constraint clearance (min 0.2mm)))\n" ) );
1423 BOOST_CHECK_EQUAL( ParsedDrcRuleCount( result.m_text, wxT( "downgrade_comment_above_header" ) ), 1 );
1424}
1425
1426
1427BOOST_AUTO_TEST_CASE( MissingVersionHeaderIsWrittenOnItsOwnLine )
1428{
1429 const wxString rules = wxT( "# intro\n"
1430 "(rule \"a\" (constraint net_chain_length (max 1mm)))\n" );
1431
1433 BOOST_REQUIRE( result.m_error.IsEmpty() );
1434 BOOST_CHECK_EQUAL( result.m_text, wxString( "(version 1)\n"
1435 "# intro\n"
1436 "# Not supported by KiCad 9.0:\n"
1437 "# (rule \"a\" (constraint net_chain_length (max 1mm)))\n" ) );
1438 BOOST_CHECK_EQUAL( ParsedDrcRuleCount( result.m_text, wxT( "downgrade_missing_header" ) ), 0 );
1439}
1440
1441
1442BOOST_AUTO_TEST_CASE( HeaderGluedToTheFirstRuleStaysGlued )
1443{
1444 const wxString rules = wxT( "(version 2)(rule \"kept\" (constraint clearance (min 0.2mm)))" );
1445
1447 BOOST_REQUIRE( result.m_error.IsEmpty() );
1448 BOOST_CHECK_EQUAL( result.m_text, wxString( "(version 1)(rule \"kept\" (constraint clearance (min 0.2mm)))\n" ) );
1449 BOOST_CHECK_EQUAL( ParsedDrcRuleCount( result.m_text, wxT( "downgrade_glued_header" ) ), 1 );
1450}
1451
1452
1453BOOST_AUTO_TEST_CASE( InlineCommentBetweenRulesFailsClosed )
1454{
1455 const wxString rules = wxT( "(version 1) # note\n(rule \"a\" (constraint clearance (min 0.2mm)))\n" );
1456
1457 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
1458 BOOST_CHECK( !FilterDrcRulesForTarget( rules, target ).m_error.IsEmpty() );
1459}
1460
1461
1462BOOST_AUTO_TEST_CASE( UnitInsideAConditionIsChecked )
1463{
1464 const wxString rules = wxT( "(version 1)(rule \"c\" (condition \"A.Width > 5ps\") "
1465 "(constraint clearance (min 0.2mm)))" );
1466
1467 BOOST_CHECK_EQUAL( FilterDrcRulesForTarget( rules, kicad9 ).m_dropped.size(), 1 );
1468 BOOST_CHECK_EQUAL( FilterDrcRulesForTarget( rules, kicad10 ).m_dropped.size(), 0 );
1469}
1470
1471
1472BOOST_AUTO_TEST_CASE( TextVariableInsideACommentIsIgnored )
1473{
1474 const wxString rules = wxT( "(version 1)\n"
1475 "(rule \"a\"\n"
1476 " # see ${GAP}\n"
1477 " (constraint clearance (min 0.2mm)))\n" );
1478
1479 BOOST_CHECK( FilterDrcRulesForTarget( rules, kicad9 ).m_dropped.empty() );
1480}
1481
1482
1483BOOST_AUTO_TEST_CASE( FunctionNamesAreMatchedWithoutCase )
1484{
1485 const wxString rules = wxT( "(version 1)(rule \"f\" (condition \"A.InsideCourtyard('x')\") "
1486 "(constraint clearance (min 0.2mm)))" );
1487
1488 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
1489 BOOST_CHECK( FilterDrcRulesForTarget( rules, target ).m_dropped.empty() );
1490}
1491
1492
1493BOOST_AUTO_TEST_CASE( ComponentClassSelectorIsDroppedForEveryTarget )
1494{
1495 const wxString rules = wxT( "(version 1)(rule \"hv\" (condition \"A.memberOfFootprint('${Class:HV}')\") "
1496 "(constraint clearance (min 1mm)))" );
1497
1498 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
1499 BOOST_CHECK_EQUAL( FilterDrcRulesForTarget( rules, target ).m_dropped.size(), 1 );
1500}
1501
1502
1503BOOST_AUTO_TEST_CASE( UnquotedExpressionsAreScanned )
1504{
1505 const wxString parent = wxT( "(version 1)(rule \"x\" (condition A.Parent.Reference==B.Reference) "
1506 "(constraint clearance (min 1mm)))" );
1507 const wxString assertion = wxT( "(version 1)(rule \"x\" (constraint assertion A.Parent.Reference==B.Reference))" );
1508 const wxString plain = wxT( "(version 1)(rule \"x\" (condition A.NetClass=='HV') "
1509 "(constraint clearance (min 1mm)))" );
1510
1511 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
1512 {
1513 BOOST_CHECK_EQUAL( FilterDrcRulesForTarget( parent, target ).m_dropped.size(), 1 );
1514 BOOST_CHECK_EQUAL( FilterDrcRulesForTarget( assertion, target ).m_dropped.size(), 1 );
1515 BOOST_CHECK( FilterDrcRulesForTarget( plain, target ).m_dropped.empty() );
1516 }
1517}
1518
1519
1520BOOST_AUTO_TEST_CASE( QuotedConstraintItemsAreChecked )
1521{
1522 const wxString newItem = wxT( "(version 1)(rule \"y\" (constraint disallow \"blind_via\"))" );
1523 const wxString oldItem = wxT( "(version 1)(rule \"y\" (constraint disallow \"via\"))" );
1524 const wxString connection = wxT( "(version 1)(rule \"z\" (constraint zone_connection \"thermal_reliefs\"))" );
1525
1526 BOOST_CHECK_EQUAL( FilterDrcRulesForTarget( newItem, kicad9 ).m_dropped.size(), 1 );
1527 BOOST_CHECK( FilterDrcRulesForTarget( newItem, kicad10 ).m_dropped.empty() );
1528
1529 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
1530 {
1531 BOOST_CHECK( FilterDrcRulesForTarget( oldItem, target ).m_dropped.empty() );
1532 BOOST_CHECK( FilterDrcRulesForTarget( connection, target ).m_dropped.empty() );
1533 }
1534}
1535
1536
1537BOOST_AUTO_TEST_CASE( StringSpanningLinesFailsClosed )
1538{
1539 const wxString rules = wxT( "(version 1)\n(rule \"a\" (condition \"A.NetClass ==\n 'HV'\") "
1540 "(constraint clearance (min 0.2mm)))\n" );
1541
1542 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
1543 BOOST_CHECK( !FilterDrcRulesForTarget( rules, target ).m_error.IsEmpty() );
1544}
1545
1546
1547BOOST_AUTO_TEST_CASE( CommentBetweenParenthesisAndHeadIsKept )
1548{
1549 const wxString rules = wxT( "(version 1)\n"
1550 "(\n"
1551 "# the head comes after this comment\n"
1552 "rule \"a\" (constraint clearance (min 0.2mm)))\n"
1553 "(\n"
1554 "# so does this one\n"
1555 "rule \"b\" (constraint net_chain_length (max 1mm)))\n" );
1556
1558 BOOST_REQUIRE( result.m_error.IsEmpty() );
1559 BOOST_REQUIRE_EQUAL( result.m_dropped.size(), 1 );
1560 BOOST_CHECK_EQUAL( result.m_dropped.front(), wxString( "b" ) );
1561 BOOST_CHECK( result.m_text.Contains( wxT( "\nrule \"a\"" ) ) );
1562 BOOST_CHECK_EQUAL( ParsedDrcRuleCount( result.m_text, wxT( "downgrade_comment_before_head" ) ), 1 );
1563}
1564
1565
1566BOOST_AUTO_TEST_CASE( InvalidDrcCommentAndUnbalancedSyntaxFailClosed )
1567{
1568 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
1569 {
1570 for( const wxString& input : { wxT( "(version 1)\n(rule \"inline\" # )\n (constraint clearance (min 0.2mm)))" ),
1571 wxT( "(version 1)\n(rule \"missing_close\" (constraint clearance (min 0.2mm))" ),
1572 wxT( "(version 1)\n(rule \"missing_quote" ) } )
1573 {
1574 BOOST_CHECK( !FilterDrcRulesForTarget( input, target ).m_error.IsEmpty() );
1575 }
1576 }
1577}
1578
1579
1580BOOST_AUTO_TEST_CASE( DrcRulesMatchIndependentOlderReferences )
1581{
1582 const std::string fixture = KI_TEST::GetPcbnewTestDataDir() + "../downgrade/golden/";
1583 const wxString input = wxString::FromUTF8( KI_TEST::ReadGoldenText( fixture + "current/rules.kicad_dru" ) );
1584
1585 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
1586 {
1588 BOOST_REQUIRE( result.m_error.IsEmpty() );
1589 const std::string release = target.m_id == wxT( "9.0" ) ? "v9" : "v10";
1590 BOOST_CHECK_EQUAL( result.m_text.ToStdString(),
1591 KI_TEST::ReadGoldenText( fixture + release + "/rules.kicad_dru" ) );
1592 BOOST_CHECK_EQUAL( result.m_dropped.size(), release == "v9" ? 2 : 1 );
1593 BOOST_CHECK_EQUAL( ParsedDrcRuleCount( result.m_text, wxT( "downgrade_drc_golden" ) ),
1594 release == "v9" ? 1 : 2 );
1595 }
1596}
1597
1598
1599// Flattening bakes the chosen variant's overrides into the base attributes, so the exported
1600// board matches that variant after the registry is dropped.
1601BOOST_AUTO_TEST_CASE( VariantFlattenedIntoBoard )
1602{
1603 BOARD board;
1604 FOOTPRINT* fp = new FOOTPRINT( &board );
1605 fp->SetDNP( false );
1606
1607 FOOTPRINT_VARIANT* variant = fp->AddVariant( wxT( "V1" ) );
1608 variant->SetDNP( true );
1609 variant->SetFieldValue( wxT( "Value" ), wxT( "10k" ) );
1610 board.Add( fp );
1611
1612 FlattenBoardVariant( &board, wxT( "V1" ) );
1613
1614 BOOST_CHECK( fp->IsDNP() );
1615 BOOST_REQUIRE( fp->GetField( wxT( "Value" ) ) != nullptr );
1616 BOOST_CHECK( fp->GetField( wxT( "Value" ) )->GetText() == wxT( "10k" ) );
1617}
1618
1619
1620// The project file is migrated down: newer settings are removed, nested schema versions are
1621// rewound, and meaningful losses are reported.
1622BOOST_AUTO_TEST_CASE( ProjectFileDowngradeForKicad9 )
1623{
1624 nlohmann::json doc = {
1625 { "meta", { { "filename", "p.kicad_pro" }, { "version", 3 } } },
1626 { "board", { { "layer_presets", { { { "name", "P" }, { "renderLayers", { "tracks", "vias" } } } } } } },
1627 { "net_settings",
1628 { { "meta", { { "version", 5 } } },
1629 { "classes", { { { "name", "Default" }, { "priority", 0 }, { "tuning_profile", "tp" } } } },
1630 { "net_chain_classes", { { { "name", "chainA" } } } } } },
1631 { "tuning_profiles",
1632 { { "meta", { { "version", 1 } } },
1633 { "tuning_profiles_impedance_geometric",
1634 { { { "name", "tp" }, { "frequency", 1e9 }, { "model_solder_mask", false } } } } } },
1635 { "component_class_settings", { { "meta", { { "version", 0 } } } } },
1636 { "schematic",
1637 { { "top_level_sheets", { "aaaa" } },
1638 { "bus_aliases", nlohmann::json::array() },
1639 { "variants", nlohmann::json::array() } } },
1640 };
1641
1642 COMPATIBILITY_REPORT report;
1643 DowngradeProjectFileJson( doc, kicad9, report );
1644
1645 // 9.0 shipped project schema 3 and reads render layers by name, skipping names it does
1646 // not know. So the version and the preset strings must pass through untouched.
1647 BOOST_CHECK_EQUAL( doc["meta"]["version"].get<int>(), 3 );
1648
1649 const nlohmann::json& preset = doc["board"]["layer_presets"][0];
1650 BOOST_REQUIRE( preset["renderLayers"].is_array() );
1651 BOOST_REQUIRE_EQUAL( preset["renderLayers"].size(), 2 );
1652 BOOST_CHECK_EQUAL( preset["renderLayers"][0].get<std::string>(), "tracks" );
1653 BOOST_CHECK_EQUAL( preset["renderLayers"][1].get<std::string>(), "vias" );
1654
1655 BOOST_CHECK_EQUAL( doc["net_settings"]["meta"]["version"].get<int>(), 4 );
1656 BOOST_CHECK( !doc["net_settings"]["classes"][0].contains( "tuning_profile" ) );
1657 BOOST_CHECK( doc["net_settings"]["classes"][0].contains( "name" ) );
1658 BOOST_CHECK( !doc["net_settings"].contains( "net_chain_classes" ) );
1659 BOOST_CHECK( !doc.contains( "tuning_profiles" ) );
1660 BOOST_CHECK( !doc.contains( "component_class_settings" ) );
1661 BOOST_CHECK( !doc["schematic"].contains( "top_level_sheets" ) );
1662 BOOST_CHECK( !doc["schematic"].contains( "bus_aliases" ) );
1663 BOOST_CHECK( !doc["schematic"].contains( "variants" ) );
1664
1665 // The meaningful losses show in the report. Hygiene removals stay silent.
1666 BOOST_CHECK( report.IsLossy() );
1668}
1669
1670
1671BOOST_AUTO_TEST_CASE( EmptyNativeProjectSectionsDoNotRequireConsent )
1672{
1673 const nlohmann::json empty = nlohmann::json::array();
1674 const std::vector<nlohmann::json> tuningDefaults = {
1675 nullptr,
1676 nlohmann::json::object(),
1677 { { "meta", { { "version", 2 } } } },
1678 { { "meta", { { "version", 2 } } }, { "tuning_profiles_impedance_geometric", empty } },
1679 };
1680 const std::vector<nlohmann::json> componentDefaults = {
1681 nullptr,
1682 nlohmann::json::object(),
1683 { { "meta", { { "version", 0 } } } },
1684 { { "assignments", empty }, { "sheet_component_classes", nlohmann::json::object() } },
1685 { { "meta", { { "version", 0 } } },
1686 { "assignments", empty },
1687 { "sheet_component_classes", { { "enabled", false } } } },
1688 };
1689
1690 for( const auto& target : { kicad9, kicad10 } )
1691 {
1692 for( const auto& tuning : tuningDefaults )
1693 {
1694 for( const auto& components : componentDefaults )
1695 {
1696 nlohmann::json doc = {
1697 { "meta", { { "version", 4 } } },
1698 { "board", { { "design_settings", { { "meta", { { "version", 3 } } } } } } },
1699 { "erc", { { "meta", { { "version", 1 } } }, { "erc_exclusions", empty } } },
1700 { "net_settings",
1701 { { "meta", { { "version", 5 } } },
1702 { "classes", { { { "name", "Default" }, { "tuning_profile", "" } } } } } },
1703 { "tuning_profiles", tuning },
1704 { "component_class_settings", components },
1705 };
1706
1707 COMPATIBILITY_REPORT report;
1708 DowngradeProjectFileJson( doc, target, report );
1709
1710 BOOST_CHECK( !report.IsBlocked() );
1711 BOOST_CHECK( !report.IsLossy() );
1713 BOOST_CHECK_EQUAL( doc["meta"]["version"].get<int>(), 3 );
1714 BOOST_CHECK_EQUAL( doc["board"]["design_settings"]["meta"]["version"].get<int>(), 2 );
1715 BOOST_CHECK_EQUAL( doc["erc"]["meta"]["version"].get<int>(), 0 );
1716 BOOST_CHECK_EQUAL( doc.contains( "tuning_profiles" ), target.m_id == wxT( "10.0" ) );
1717 BOOST_CHECK_EQUAL( doc.contains( "component_class_settings" ), target.m_id == wxT( "10.0" ) );
1718
1719 const nlohmann::json once = doc;
1720 COMPATIBILITY_REPORT repeated;
1721 DowngradeProjectFileJson( doc, target, repeated );
1722 BOOST_CHECK( doc == once );
1723 BOOST_CHECK( !repeated.IsLossy() );
1724 }
1725 }
1726 }
1727}
1728
1729
1730BOOST_AUTO_TEST_CASE( SavedProjectTuningAndClassChoicesStillRequireConsent )
1731{
1732 const nlohmann::json empty = nlohmann::json::array();
1733 const nlohmann::json profile = {
1734 { "profile_name", "Default" },
1735 { "frequency", 1e9 },
1736 { "model_solder_mask", false },
1737 { "net_chain_bridge_prop_delay", 0 },
1738 };
1739 const std::vector<nlohmann::json> meaningful = {
1740 { { "tuning_profiles", { { "tuning_profiles_impedance_geometric", { profile } } } } },
1741 { { "net_settings", { { "classes", { { { "tuning_profile", "Saved" } } } } } } },
1742 { { "tuning_profiles", { { "tuning_profiles_impedance_geometric", empty } } },
1743 { "net_settings", { { "classes", { { { "tuning_profile", "Saved" } } } } } } },
1744 { { "tuning_profiles", { { "tuning_profiles_impedance_geometric", { profile } } } },
1745 { "net_settings",
1746 { { "classes", { { { "tuning_profile", "Default" } }, { { "tuning_profile", "Default" } } } } } } },
1747 { { "component_class_settings",
1748 { { "assignments",
1749 { { { "component_class", "Fast" }, { "conditions_operator", "ALL" }, { "conditions", empty } } } },
1750 { "sheet_component_classes", { { "enabled", false } } } } } },
1751 { { "component_class_settings",
1752 { { "assignments", empty }, { "sheet_component_classes", { { "enabled", true } } } } } },
1753 };
1754
1755 for( const auto& source : meaningful )
1756 {
1757 for( const auto& target : { kicad9, kicad10 } )
1758 {
1759 nlohmann::json doc = source;
1760 COMPATIBILITY_REPORT report;
1761 DowngradeProjectFileJson( doc, target, report );
1762 BOOST_CHECK( !report.IsBlocked() );
1763 BOOST_CHECK_EQUAL( report.Count( DOWNGRADE_BUCKET::DROP ), target.m_id == wxT( "9.0" ) ? 1 : 0 );
1764 BOOST_CHECK_EQUAL( report.IsLossy(), target.m_id == wxT( "9.0" ) );
1765
1766 const nlohmann::json once = doc;
1767 COMPATIBILITY_REPORT repeated;
1768 DowngradeProjectFileJson( doc, target, repeated );
1769 BOOST_CHECK( doc == once );
1770 BOOST_CHECK( !repeated.IsLossy() );
1771 }
1772 }
1773}
1774
1775
1776BOOST_AUTO_TEST_CASE( FutureComponentClassSchemaBlocksWithoutMutation )
1777{
1778 const nlohmann::json source = {
1779 { "meta", { { "version", 4 } } },
1780 { "component_class_settings", { { "meta", { { "version", 1 } } } } },
1781 };
1782
1783 for( const auto& target : { kicad9, kicad10 } )
1784 {
1785 nlohmann::json doc = source;
1786 COMPATIBILITY_REPORT report;
1787 DowngradeProjectFileJson( doc, target, report );
1788 BOOST_CHECK( report.IsBlocked() );
1789 BOOST_CHECK( doc == source );
1790 }
1791}
1792
1793
1794// A structurally odd but valid-JSON project file must not throw. Classification swallows
1795// exceptions, so a throw here would surface only mid-export and break fail-closed.
1796BOOST_AUTO_TEST_CASE( WrongShapeProjectFileDoesNotThrow )
1797{
1798 nlohmann::json metaIsString = {
1799 { "meta", "not an object" },
1800 { "net_settings", { { "meta", "also a string" }, { "classes", "not an array" } } },
1801 { "schematic", "a string" },
1802 };
1803
1804 nlohmann::json profilesWrong = {
1805 { "tuning_profiles",
1806 { { "meta", 7 }, { "tuning_profiles_impedance_geometric", { "just", "strings" } } } },
1807 };
1808
1809 COMPATIBILITY_REPORT report;
1810 BOOST_CHECK_NO_THROW( DowngradeProjectFileJson( metaIsString, kicad9, report ) );
1811 BOOST_CHECK_NO_THROW( DowngradeProjectFileJson( profilesWrong, kicad10, report ) );
1812}
1813
1814
1815// An empty net chain class array is removed as hygiene, not reported as a loss.
1816BOOST_AUTO_TEST_CASE( EmptyNetChainClassesSilent )
1817{
1818 nlohmann::json doc = {
1819 { "net_settings", { { "net_chain_classes", nlohmann::json::array() } } },
1820 };
1821
1822 COMPATIBILITY_REPORT report;
1823 DowngradeProjectFileJson( doc, kicad10, report );
1824
1825 BOOST_CHECK( !doc["net_settings"].contains( "net_chain_classes" ) );
1827}
1828
1829
1830BOOST_AUTO_TEST_CASE( NonDefaultTuningModelSettingsAreReportedForKicad10 )
1831{
1832 nlohmann::json doc = {
1833 { "tuning_profiles",
1834 { { "meta", { { "version", 1 } } },
1835 { "tuning_profiles_impedance_geometric",
1836 { { { "profile_name", "5 GHz" }, { "frequency", 5e9 }, { "model_solder_mask", true } },
1837 { { "profile_name", "Default" }, { "frequency", 1e9 }, { "model_solder_mask", false } } } } } }
1838 };
1839
1840 COMPATIBILITY_REPORT report;
1841 DowngradeProjectFileJson( doc, kicad10, report );
1842 BOOST_CHECK( report.IsLossy() );
1844
1845 for( const auto& profile : doc["tuning_profiles"]["tuning_profiles_impedance_geometric"] )
1846 {
1847 BOOST_CHECK( !profile.contains( "frequency" ) );
1848 BOOST_CHECK( !profile.contains( "model_solder_mask" ) );
1849 }
1850
1851 COMPATIBILITY_REPORT secondReport;
1852 DowngradeProjectFileJson( doc, kicad10, secondReport );
1853 BOOST_CHECK( !secondReport.IsLossy() );
1854}
1855
1856
1857BOOST_AUTO_TEST_CASE( ProjectFileDowngradeForKicad10 )
1858{
1859 nlohmann::json doc = {
1860 { "meta", { { "version", 3 } } },
1861 { "net_settings",
1862 { { "meta", { { "version", 5 } } },
1863 { "classes", { { { "name", "Default" }, { "priority", 0 }, { "tuning_profile", "tp" } } } },
1864 { "net_chain_classes", { { { "name", "chainA" } } } } } },
1865 { "tuning_profiles",
1866 { { "meta", { { "version", 1 } } },
1867 { "tuning_profiles_impedance_geometric",
1868 { { { "name", "tp" }, { "frequency", 1e9 }, { "model_solder_mask", false } } } } } },
1869 };
1870
1871 COMPATIBILITY_REPORT report;
1872 DowngradeProjectFileJson( doc, kicad10, report );
1873
1874 // 10.0 knows tuning profiles and reads the per-class reference unguarded, so both stay.
1875 BOOST_CHECK_EQUAL( doc["meta"]["version"].get<int>(), 3 );
1876 BOOST_CHECK_EQUAL( doc["net_settings"]["meta"]["version"].get<int>(), 5 );
1877 BOOST_CHECK( doc["net_settings"]["classes"][0].contains( "tuning_profile" ) );
1878 BOOST_CHECK( !doc["net_settings"].contains( "net_chain_classes" ) );
1879
1880 BOOST_CHECK_EQUAL( doc["tuning_profiles"]["meta"]["version"].get<int>(), 0 );
1881 BOOST_CHECK( !doc["tuning_profiles"]["tuning_profiles_impedance_geometric"][0].contains( "frequency" ) );
1882 BOOST_CHECK( !doc["tuning_profiles"]["tuning_profiles_impedance_geometric"][0].contains( "model_solder_mask" ) );
1883
1885}
1886
1887
1888// A near-closed thick elliptical arc strokes to a ring. The writers save one outline per
1889// shape, so the lowered polygon must carry no holes.
1890BOOST_AUTO_TEST_CASE( EllipseArcRingHasNoHoles )
1891{
1892 BOARD board;
1893 PCB_SHAPE* arc = new PCB_SHAPE( &board, SHAPE_T::ELLIPSE_ARC );
1894 arc->SetEllipseCenter( VECTOR2I( 0, 0 ) );
1895 arc->SetEllipseMajorRadius( pcbIUScale.mmToIU( 5 ) );
1896 arc->SetEllipseMinorRadius( pcbIUScale.mmToIU( 3 ) );
1899 arc->SetWidth( pcbIUScale.mmToIU( 0.3 ) );
1900 board.Add( arc );
1901
1902 DowngradeBoardInPlace( &board, kicad10 );
1903
1905
1906 SHAPE_POLY_SET& poly = arc->GetPolyShape();
1907
1908 for( int ii = 0; ii < poly.OutlineCount(); ++ii )
1909 BOOST_CHECK_EQUAL( poly.HoleCount( ii ), 0 );
1910
1911 // The ring's ink, nowhere near the filled disk's 47 square mm.
1912 double areaMM = std::abs( poly.Area() ) / ( pcbIUScale.IU_PER_MM * pcbIUScale.IU_PER_MM );
1913
1914 BOOST_CHECK_GT( areaMM, 4.0 );
1915 BOOST_CHECK_LT( areaMM, 15.0 );
1916}
1917
1918
1919// The scale compensation must reach images inside footprints on the board.
1920BOOST_AUTO_TEST_CASE( FootprintImageScaleCompensatedOnBoard )
1921{
1922 BOARD board;
1923 FOOTPRINT* fp = new FOOTPRINT( &board );
1925 REFERENCE_IMAGE& ref = image->GetReferenceImage();
1926
1927 wxMemoryBuffer buf = KI_TEST::MakePngWithFractionalPixelsPerCm();
1928 BOOST_REQUIRE( ref.ReadImageFile( buf ) );
1929 ref.SetImageScale( 2.0 );
1930 fp->Add( image );
1931 board.Add( fp );
1932
1933 int ppi = ref.GetImage().GetPPI();
1934 int legacyPPI = ref.GetImage().GetLegacyPPI();
1935 BOOST_REQUIRE( legacyPPI > 0 && ppi != legacyPPI );
1936
1937 DowngradeBoardInPlace( &board, kicad9 );
1938
1939 BOOST_CHECK_CLOSE( ref.GetImageScale(), 2.0 * legacyPPI / ppi, 0.1 );
1940}
1941
1942
1943// A library footprint goes through the footprint-scoped path and gets the same compensation.
1944BOOST_AUTO_TEST_CASE( FootprintImageScaleCompensatedInLibrary )
1945{
1946 std::unique_ptr<FOOTPRINT> fp = std::make_unique<FOOTPRINT>( nullptr );
1948 REFERENCE_IMAGE& ref = image->GetReferenceImage();
1949
1950 wxMemoryBuffer buf = KI_TEST::MakePngWithFractionalPixelsPerCm();
1951 BOOST_REQUIRE( ref.ReadImageFile( buf ) );
1952 ref.SetImageScale( 2.0 );
1953 fp->Add( image );
1954
1955 int ppi = ref.GetImage().GetPPI();
1956 int legacyPPI = ref.GetImage().GetLegacyPPI();
1957 BOOST_REQUIRE( legacyPPI > 0 && ppi != legacyPPI );
1958
1959 DowngradeFootprintInPlace( fp.get(), kicad9 );
1960
1961 BOOST_CHECK_CLOSE( ref.GetImageScale(), 2.0 * legacyPPI / ppi, 0.1 );
1962}
1963
1964
1965// The duplicate-pad-numbers jumper flag is jumper data too, so 9.0 must drop and report it.
1966BOOST_AUTO_TEST_CASE( DuplicatePadJumperFlagDroppedForKicad9 )
1967{
1968 BOARD board;
1969 FOOTPRINT* fp = new FOOTPRINT( &board );
1971 board.Add( fp );
1972
1974
1975 DowngradeBoardInPlace( &board, kicad9 );
1976
1978}
1979
1980
1981// Downgrading a library file must not clip zone layers to a scratch board's layer set. The
1982// source file is its own oracle: the layer set must survive the round trip unchanged.
1983BOOST_AUTO_TEST_CASE( FootprintFileKeepsZoneLayers )
1984{
1985 SETTINGS_MANAGER settingsManager;
1986
1987 KI_TEST::SCOPED_TEMP_DIR tmp( "kicad_qa_fp_zone_layers", "" );
1988 const wxString src = ( tmp.Path() / "src.kicad_mod" ).wstring();
1989 const wxString dest = ( tmp.Path() / "out.kicad_mod" ).wstring();
1990
1991 {
1992 std::unique_ptr<FOOTPRINT> fp = std::make_unique<FOOTPRINT>( nullptr );
1993 ZONE* zone = new ZONE( fp.get() );
1994
1995 zone->SetLayerSet( LSET::AllCuMask() );
1996 zone->Outline()->NewOutline();
1997 zone->Outline()->Append( 0, 0 );
1998 zone->Outline()->Append( pcbIUScale.mmToIU( 1 ), 0 );
1999 zone->Outline()->Append( pcbIUScale.mmToIU( 1 ), pcbIUScale.mmToIU( 1 ) );
2000 zone->Outline()->Append( 0, pcbIUScale.mmToIU( 1 ) );
2001 fp->Add( zone );
2002
2003 SaveFootprintForTarget( fp.get(), src, kicad10 );
2004 }
2005
2007
2008 PCB_IO_KICAD_SEXPR reader;
2009 wxString nameOut;
2010 std::unique_ptr<FOOTPRINT> srcAgain( reader.ImportFootprint( src, nameOut ) );
2011 std::unique_ptr<FOOTPRINT> converted( reader.ImportFootprint( dest, nameOut ) );
2012
2013 BOOST_REQUIRE( srcAgain && converted );
2014 BOOST_REQUIRE_EQUAL( srcAgain->Zones().size(), 1 );
2015 BOOST_REQUIRE_EQUAL( converted->Zones().size(), 1 );
2016
2017 BOOST_CHECK_EQUAL( converted->Zones()[0]->GetLayerSet().count(), srcAgain->Zones()[0]->GetLayerSet().count() );
2018}
2019
2020
2021// The staged library downgrade converts and verifies every file before any original is
2022// replaced, and a refusal leaves the folder untouched with no temp files behind.
2023BOOST_AUTO_TEST_CASE( LibraryFilesDowngradeFailsClosed )
2024{
2025 KI_TEST::SCOPED_TEMP_DIR tmp( "kicad_qa_lib_staged", "" );
2026 const wxString dir = tmp.Path().wstring();
2027
2028 auto writeFile = [&]( const wxString& aName, const wxString& aContent )
2029 {
2030 wxFFile file( dir + wxFileName::GetPathSeparator() + aName, wxT( "wb" ) );
2031 BOOST_REQUIRE( file.IsOpened() );
2032 file.Write( aContent );
2033 };
2034
2035 writeFile( wxT( "a.fake_mod" ), wxT( "old a" ) );
2036 writeFile( wxT( "b.fake_mod" ), wxT( "old b" ) );
2037
2038 auto noForbidden = []( const wxString& )
2039 {
2040 return wxString();
2041 };
2042
2043 auto convertOk = [&]( const wxString& aFile, const wxString& aTmp ) -> DOWNGRADE_FILE_RESULT
2044 {
2045 wxFFile out( aTmp, wxT( "wb" ) );
2046 out.Write( wxT( "(version 123) converted" ) );
2048 };
2049
2050 BOOST_CHECK( DowngradeLibraryFilesInPlace( dir, wxT( "*.fake_mod" ), 123, noForbidden, convertOk ).IsEmpty() );
2051
2052 wxFFile check( dir + wxFileName::GetPathSeparator() + wxT( "a.fake_mod" ), wxT( "rb" ) );
2053 wxString content;
2054 BOOST_REQUIRE( check.IsOpened() && check.ReadAll( &content ) );
2055 BOOST_CHECK( content.Contains( wxT( "converted" ) ) );
2056
2057 // Second pass: the second file refuses, so the first must stay as it is now.
2058 auto convertRefuseB = [&]( const wxString& aFile, const wxString& aTmp ) -> DOWNGRADE_FILE_RESULT
2059 {
2060 if( aFile.Contains( wxT( "b.fake_mod" ) ) )
2062
2063 wxFFile out( aTmp, wxT( "wb" ) );
2064 out.Write( wxT( "(version 123) second pass" ) );
2066 };
2067
2068 wxString bad = DowngradeLibraryFilesInPlace( dir, wxT( "*.fake_mod" ), 123, noForbidden, convertRefuseB );
2069 BOOST_CHECK( bad.Contains( wxT( "b.fake_mod" ) ) );
2070
2071 wxArrayString leftovers;
2072 wxDir::GetAllFiles( dir, &leftovers );
2073 BOOST_CHECK_EQUAL( leftovers.size(), 2 );
2074
2075 wxFFile recheck( dir + wxFileName::GetPathSeparator() + wxT( "a.fake_mod" ), wxT( "rb" ) );
2076 wxString unchanged;
2077 BOOST_REQUIRE( recheck.IsOpened() && recheck.ReadAll( &unchanged ) );
2078 BOOST_CHECK( !unchanged.Contains( wxT( "second pass" ) ) );
2079}
2080
2081
2082// The project export carries design files only. UI state and non-KiCad files stay behind.
2083BOOST_AUTO_TEST_CASE( DesignFileFilter )
2084{
2085 BOOST_CHECK( IsDowngradeDesignFile( wxT( "proj/proj.kicad_pro" ) ) );
2086 BOOST_CHECK( IsDowngradeDesignFile( wxT( "proj/sub/sheet.kicad_sch" ) ) );
2087 BOOST_CHECK( IsDowngradeDesignFile( wxT( "proj/proj.kicad_pcb" ) ) );
2088 BOOST_CHECK( IsDowngradeDesignFile( wxT( "proj/proj.kicad_dru" ) ) );
2089 BOOST_CHECK( IsDowngradeDesignFile( wxT( "proj/frame.kicad_wks" ) ) );
2090 BOOST_CHECK( IsDowngradeDesignFile( wxT( "proj/lib.kicad_sym" ) ) );
2091 BOOST_CHECK( IsDowngradeDesignFile( wxT( "proj/lib.pretty/fp.kicad_mod" ) ) );
2092 BOOST_CHECK( IsDowngradeDesignFile( wxT( "proj/sym-lib-table" ) ) );
2093 BOOST_CHECK( IsDowngradeDesignFile( wxT( "proj/fp-lib-table" ) ) );
2094
2095 BOOST_CHECK( !IsDowngradeDesignFile( wxT( "proj/proj.kicad_prl" ) ) );
2096 BOOST_CHECK( !IsDowngradeDesignFile( wxT( "proj/datasheet.pdf" ) ) );
2097 BOOST_CHECK( !IsDowngradeDesignFile( wxT( "proj/out/proj-F_Cu.gbr" ) ) );
2098 BOOST_CHECK( !IsDowngradeDesignFile( wxT( "proj/proj.kicad_jobset" ) ) );
2099 BOOST_CHECK( !IsDowngradeDesignFile( wxT( "proj/notes.txt" ) ) );
2100}
2101
2102
2103// When this fails a format bump happened. Review the new keywords, update the denylist, then bump
2104// BoardDowngradeCoveredVersion.
2109
2110
2111// Every graphical LOWER rule has an omission path, both on boards and in footprints.
2112BOOST_AUTO_TEST_CASE( DropApproximationsOmitsGraphicsWithoutReplacements )
2113{
2114 for( bool footprintOnly : { false, true } )
2115 {
2116 BOARD board;
2117 FOOTPRINT* fp = new FOOTPRINT( &board );
2118 board.Add( fp );
2119 BOARD_ITEM_CONTAINER* parent = footprintOnly ? static_cast<BOARD_ITEM_CONTAINER*>( fp ) : &board;
2120 PCB_GROUP* group = new PCB_GROUP( parent );
2121 parent->Add( group );
2122
2124 {
2125 PCB_SHAPE* shape = new PCB_SHAPE( parent, type );
2126 if( type == SHAPE_T::RECTANGLE )
2127 {
2128 shape->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 6 ) ) );
2129 shape->SetCornerRadius( pcbIUScale.mmToIU( 1 ) );
2130 }
2131 parent->Add( shape );
2132 group->AddItem( shape );
2133 }
2134
2135 PCB_BARCODE* barcode = new PCB_BARCODE( parent );
2136 parent->Add( barcode );
2137 group->AddItem( barcode );
2138 PCB_TEXTBOX* textbox = new PCB_TEXTBOX( parent );
2139 textbox->SetIsKnockout( true );
2140 textbox->SetText( wxT( "Do not retain as plain text" ) );
2141 parent->Add( textbox );
2142 group->AddItem( textbox );
2143
2144 PCB_SHAPE* supported = new PCB_SHAPE( parent, SHAPE_T::CIRCLE );
2145 parent->Add( supported );
2146 group->AddItem( supported );
2147 const KIID supportedUuid = supported->m_Uuid;
2148 const DOWNGRADE_TARGET& target = kicad9;
2149 auto normal = ClassifyBoardForDowngrade( &board, target );
2150 auto omit = ClassifyBoardForDowngrade( &board, target, true );
2152 BOOST_CHECK_EQUAL( omit.Count( DOWNGRADE_BUCKET::LOWER ), 0 );
2153 BOOST_CHECK_EQUAL( omit.Count( DOWNGRADE_BUCKET::DROP ), 5 );
2154 BOOST_REQUIRE_EQUAL( normal.Entries().size(), omit.Entries().size() );
2155 for( size_t i = 0; i < normal.Entries().size(); ++i )
2156 {
2157 BOOST_CHECK( normal.Entries()[i].m_feature == omit.Entries()[i].m_feature );
2158 BOOST_CHECK_EQUAL( normal.Entries()[i].m_count, omit.Entries()[i].m_count );
2159 BOOST_CHECK( normal.Entries()[i].m_detail != omit.Entries()[i].m_detail );
2160 BOOST_CHECK( !omit.Entries()[i].m_detail.IsEmpty() );
2161 }
2162
2163 // Classification is read-only.
2164 BOOST_CHECK_EQUAL( group->GetItems().size(), 6 );
2165 if( footprintOnly )
2166 DowngradeFootprintInPlace( fp, target, true );
2167 else
2168 DowngradeBoardInPlace( &board, target, true );
2169 BOOST_REQUIRE_EQUAL( group->GetItems().size(), 1 );
2170 BOOST_CHECK( *group->GetItems().begin() == supported );
2171 BOOST_CHECK( supported->m_Uuid == supportedUuid );
2172 BOOST_CHECK( !ClassifyBoardForDowngrade( &board, target, true ).IsLossy() );
2173 }
2174}
2175
2176
2177BOOST_AUTO_TEST_CASE( DropApproximationsOmitsFillAndCellContentsOnly )
2178{
2179 BOARD board;
2180 FOOTPRINT* fp = new FOOTPRINT( &board );
2181 board.Add( fp );
2182 for( BOARD_ITEM_CONTAINER* parent :
2183 { static_cast<BOARD_ITEM_CONTAINER*>( &board ), static_cast<BOARD_ITEM_CONTAINER*>( fp ) } )
2184 {
2185 PCB_SHAPE* hatch = new PCB_SHAPE( parent, SHAPE_T::RECTANGLE );
2186 hatch->SetLayer( F_SilkS );
2187 hatch->SetFillMode( FILL_T::HATCH );
2188 parent->Add( hatch );
2189 PCB_TABLE* table = new PCB_TABLE( parent, pcbIUScale.mmToIU( 0.1 ) );
2190 table->SetColCount( 2 );
2191 PCB_TABLECELL* knockout = new PCB_TABLECELL( table );
2192 knockout->SetIsKnockout( true );
2193 knockout->SetText( wxT( "knockout" ) );
2194 table->AddCell( knockout );
2195 PCB_TABLECELL* plain = new PCB_TABLECELL( table );
2196 plain->SetText( wxT( "supported" ) );
2197 table->AddCell( plain );
2198 parent->Add( table );
2199
2200 if( parent == fp )
2201 DowngradeFootprintInPlace( fp, kicad9, true );
2202 else
2203 DowngradeBoardInPlace( &board, kicad9, true );
2206 BOOST_REQUIRE_EQUAL( table->GetCells().size(), 2 );
2207 BOOST_CHECK( knockout->GetText().IsEmpty() );
2208 BOOST_CHECK( !knockout->IsKnockout() );
2209 BOOST_CHECK( plain->GetText() == wxT( "supported" ) );
2210 }
2211}
2212
2213
2214// Removing a hatch fill on a copper layer would delete copper, and the omission preference is
2215// not an electrical override. Copper keeps a solid fill under both policies. Other layers carry
2216// ink, so there the fill may go.
2217// The report must name the copper case separately. One combined row would label the copper
2218// approximation a drop as soon as the omission preference is set, and hide how many of the
2219// counted shapes are copper.
2220BOOST_AUTO_TEST_CASE( HatchedCopperAndInkFillsAreReportedSeparately )
2221{
2222 BOARD board;
2223
2224 for( int ii = 0; ii < 3; ++ii )
2225 {
2226 PCB_SHAPE* ink = new PCB_SHAPE( &board, SHAPE_T::RECTANGLE );
2227 ink->SetLayer( F_SilkS );
2228 ink->SetFillMode( FILL_T::HATCH );
2229 board.Add( ink );
2230 }
2231
2232 PCB_SHAPE* copper = new PCB_SHAPE( &board, SHAPE_T::RECTANGLE );
2233 copper->SetLayer( F_Cu );
2234 copper->SetFillMode( FILL_T::HATCH );
2235 board.Add( copper );
2236
2237 for( bool drop : { false, true } )
2238 {
2239 BOOST_TEST_CONTEXT( "Drop approximations: " << drop )
2240 {
2241 COMPATIBILITY_REPORT report = ClassifyBoardForDowngrade( &board, kicad9, drop );
2242
2243 BOOST_CHECK_EQUAL( report.Entries().size(), 2 );
2244 BOOST_CHECK_EQUAL( report.Count( DOWNGRADE_BUCKET::LOWER ), drop ? 1 : 4 );
2245 BOOST_CHECK_EQUAL( report.Count( DOWNGRADE_BUCKET::DROP ), drop ? 3 : 0 );
2246 }
2247 }
2248}
2249
2250
2251BOOST_AUTO_TEST_CASE( DropApproximationsKeepsHatchedCopperAsSolidFill )
2252{
2253 for( bool drop : { false, true } )
2254 {
2255 BOOST_TEST_CONTEXT( "Drop approximations: " << drop )
2256 {
2257 BOARD board;
2258 FOOTPRINT* fp = new FOOTPRINT( &board );
2259 board.Add( fp );
2260
2261 std::vector<PCB_SHAPE*> copper;
2262 std::vector<PCB_SHAPE*> silk;
2263
2264 for( BOARD_ITEM_CONTAINER* parent : { static_cast<BOARD_ITEM_CONTAINER*>( &board ),
2265 static_cast<BOARD_ITEM_CONTAINER*>( fp ) } )
2266 {
2267 PCB_SHAPE* onCopper = new PCB_SHAPE( parent, SHAPE_T::RECTANGLE );
2268 onCopper->SetLayer( F_Cu );
2269 onCopper->SetFillMode( FILL_T::HATCH );
2270 parent->Add( onCopper );
2271 copper.push_back( onCopper );
2272
2273 PCB_SHAPE* onSilk = new PCB_SHAPE( parent, SHAPE_T::RECTANGLE );
2274 onSilk->SetLayer( F_SilkS );
2275 onSilk->SetFillMode( FILL_T::HATCH );
2276 parent->Add( onSilk );
2277 silk.push_back( onSilk );
2278 }
2279
2280 DowngradeBoardInPlace( &board, kicad9, drop );
2281
2282 for( PCB_SHAPE* shape : copper )
2283 BOOST_CHECK( shape->GetFillMode() == FILL_T::FILLED_SHAPE );
2284
2285 for( PCB_SHAPE* shape : silk )
2286 BOOST_CHECK( shape->GetFillMode() == ( drop ? FILL_T::NO_FILL : FILL_T::FILLED_SHAPE ) );
2287 }
2288 }
2289}
2290
2291
2292BOOST_AUTO_TEST_CASE( DropApproximationsOmitsRoundedPadPrimitives )
2293{
2294 for( bool footprintOnly : { false, true } )
2295 {
2296 BOARD board;
2297 FOOTPRINT* fp = new FOOTPRINT( &board );
2298 board.Add( fp );
2299 PAD* pad = new PAD( fp );
2300 fp->Add( pad );
2301 pad->SetShape( F_Cu, PAD_SHAPE::CUSTOM );
2302 PCB_SHAPE* rounded = new PCB_SHAPE( fp, SHAPE_T::RECTANGLE );
2303 rounded->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 6 ) ) );
2304 rounded->SetCornerRadius( pcbIUScale.mmToIU( 1 ) );
2305 pad->AddPrimitive( F_Cu, rounded );
2306 PCB_SHAPE* supported = new PCB_SHAPE( fp, SHAPE_T::CIRCLE );
2307 pad->AddPrimitive( F_Cu, supported );
2308 const DOWNGRADE_TARGET& target = kicad9;
2309 BOOST_CHECK_EQUAL( ClassifyBoardForDowngrade( &board, target, true ).Count( DOWNGRADE_BUCKET::DROP ), 1 );
2310 if( footprintOnly )
2311 DowngradeFootprintInPlace( fp, target, true );
2312 else
2313 DowngradeBoardInPlace( &board, target, true );
2314 BOOST_REQUIRE_EQUAL( pad->GetPrimitives( F_Cu ).size(), 1 );
2315 BOOST_CHECK( pad->GetPrimitives( F_Cu ).front()->GetShape() == SHAPE_T::CIRCLE );
2316 BOOST_CHECK_EQUAL( fp->Pads().size(), 1 );
2317 }
2318}
2319
2320
2321// Removing a via deletes copper and splits a net, so the omission preference does not reach it.
2322BOOST_AUTO_TEST_CASE( ViaApproximationsSurviveDropPolicy )
2323{
2324 for( bool drop : { false, true } )
2325 {
2326 BOOST_TEST_CONTEXT( "Drop approximations: " << drop )
2327 {
2328 BOARD board;
2329 PCB_VIA* skip = new PCB_VIA( &board );
2331 board.Add( skip );
2332 PCB_VIA* buried = new PCB_VIA( &board );
2333 buried->SetViaType( VIATYPE::BURIED );
2334 board.Add( buried );
2335 PCB_VIA* supported = new PCB_VIA( &board );
2336 board.Add( supported );
2337
2338 const auto report = ClassifyBoardForDowngrade( &board, kicad9, drop );
2339
2340 BOOST_CHECK_EQUAL( report.Count( DOWNGRADE_BUCKET::LOWER ), 2 );
2341 BOOST_CHECK_EQUAL( report.Count( DOWNGRADE_BUCKET::DROP ), 0 );
2342
2343 DowngradeBoardInPlace( &board, kicad9, drop );
2344
2345 BOOST_REQUIRE_EQUAL( board.Tracks().size(), 3 );
2348 BOOST_CHECK( buried->GetViaType() == VIATYPE::BLIND );
2349 BOOST_CHECK( supported->GetViaType() == VIATYPE::THROUGH );
2350 }
2351 }
2352}
2353
2354
2355BOOST_AUTO_TEST_CASE( DropApproximationsKeepsTargetSupportedFeatures )
2356{
2357 BOARD board;
2358 PCB_SHAPE* rect = new PCB_SHAPE( &board, SHAPE_T::RECTANGLE );
2359 rect->SetEnd( VECTOR2I( pcbIUScale.mmToIU( 10 ), pcbIUScale.mmToIU( 6 ) ) );
2360 rect->SetCornerRadius( pcbIUScale.mmToIU( 1 ) );
2361 rect->SetFillMode( FILL_T::HATCH );
2362 board.Add( rect );
2363 PCB_TEXTBOX* textbox = new PCB_TEXTBOX( &board );
2364 textbox->SetIsKnockout( true );
2365 board.Add( textbox );
2366 board.Add( new PCB_BARCODE( &board ) );
2367 PCB_VIA* buried = new PCB_VIA( &board );
2368 buried->SetViaType( VIATYPE::BURIED );
2369 board.Add( buried );
2370 PCB_VIA* skip = new PCB_VIA( &board );
2372 board.Add( skip );
2373 const auto& target = kicad10;
2374 BOOST_CHECK( !ClassifyBoardForDowngrade( &board, target, true ).IsLossy() );
2375 DowngradeBoardInPlace( &board, target, true );
2376 BOOST_CHECK_EQUAL( board.Drawings().size(), 3 );
2377 BOOST_CHECK_EQUAL( board.Tracks().size(), 2 );
2378 BOOST_CHECK_EQUAL( rect->GetCornerRadius(), pcbIUScale.mmToIU( 1 ) );
2380 BOOST_CHECK( textbox->IsKnockout() );
2381 BOOST_CHECK( buried->GetViaType() == VIATYPE::BURIED );
2383}
2384
2385
2386BOOST_AUTO_TEST_CASE( DropApproximationsCannotBypassBlockRules )
2387{
2388 BOARD board;
2389 ZONE* zone = new ZONE( &board );
2391 board.Add( zone );
2392 BOOST_CHECK( ClassifyBoardForDowngrade( &board, kicad10, true ).IsBlocked() );
2393}
2394
2395
2396BOOST_AUTO_TEST_CASE( LibraryReplacementsWaitForTheDesignTransaction )
2397{
2398 KI_TEST::SCOPED_TEMP_DIR temporary( "kicad_qa_shared_downgrade_transaction" );
2399 const wxString directory = temporary.Path().wstring();
2400 const wxString library = directory + wxT( "/library.fake_mod" );
2401 const wxString board = directory + wxT( "/board.kicad_pcb" );
2402
2403 auto write = []( const wxString& aPath, const wxString& aContent )
2404 {
2405 wxFFile file( aPath, wxT( "wb" ) );
2406 BOOST_REQUIRE( file.IsOpened() && file.Write( aContent ) && file.Close() );
2407 };
2408
2409 auto read = []( const wxString& aPath )
2410 {
2411 wxFFile file( aPath, wxT( "rb" ) );
2412 wxString content;
2413 BOOST_REQUIRE( file.IsOpened() && file.ReadAll( &content ) );
2414 return content;
2415 };
2416
2417 auto noForbidden = []( const wxString& )
2418 {
2419 return wxString();
2420 };
2421 auto convert = [&]( const wxString&, const wxString& aTemporary )
2422 {
2423 write( aTemporary, wxT( "(version 123) converted" ) );
2425 };
2426
2427 write( library, wxT( "original library" ) );
2428 write( board, wxT( "original board" ) );
2429
2430 {
2431 DOWNGRADE_FILE_TRANSACTION transaction;
2433 DowngradeLibraryFilesInPlace( directory, wxT( "*.fake_mod" ), 123, noForbidden, convert, &transaction )
2434 .IsEmpty() );
2435 BOOST_CHECK_EQUAL( read( library ), wxString( wxT( "original library" ) ) );
2436 BOOST_CHECK( transaction.Stage( directory + wxT( "/missing/board.kicad_pcb" ) ).IsEmpty() );
2437 }
2438
2439 BOOST_CHECK_EQUAL( read( library ), wxString( wxT( "original library" ) ) );
2440 BOOST_CHECK_EQUAL( read( board ), wxString( wxT( "original board" ) ) );
2441
2442 {
2443 DOWNGRADE_FILE_TRANSACTION transaction;
2445 DowngradeLibraryFilesInPlace( directory, wxT( "*.fake_mod" ), 123, noForbidden, convert, &transaction )
2446 .IsEmpty() );
2447 wxString stagedBoard = transaction.Stage( board );
2448 BOOST_REQUIRE( !stagedBoard.IsEmpty() );
2449 write( stagedBoard, wxT( "converted board" ) );
2450 BOOST_REQUIRE( transaction.Commit().IsEmpty() );
2451 }
2452
2453 BOOST_CHECK_EQUAL( read( library ), wxString( wxT( "(version 123) converted" ) ) );
2454 BOOST_CHECK_EQUAL( read( board ), wxString( wxT( "converted board" ) ) );
2455 wxArrayString files;
2456 wxDir::GetAllFiles( directory, &files );
2457 BOOST_CHECK_EQUAL( files.size(), 2 );
2458}
2459
2460
2461BOOST_AUTO_TEST_CASE( DirectoryDestinationStopsAllTransactionReplacements )
2462{
2463 KI_TEST::SCOPED_TEMP_DIR temporary( "kicad_qa_downgrade_transaction_directory" );
2464 const wxString directory = temporary.Path().wstring();
2465 const wxString first = directory + wxT( "/first.kicad_pcb" );
2466 const wxString second = directory + wxT( "/second.kicad_pcb" );
2467
2468 {
2469 wxFFile file( first, wxT( "wb" ) );
2470 BOOST_REQUIRE( file.IsOpened() && file.Write( wxT( "original" ) ) && file.Close() );
2471 }
2472
2473 {
2474 DOWNGRADE_FILE_TRANSACTION transaction;
2475 wxString stagedFirst = transaction.Stage( first );
2476 wxString stagedSecond = transaction.Stage( second );
2477 BOOST_REQUIRE( !stagedFirst.IsEmpty() && !stagedSecond.IsEmpty() );
2478
2479 for( const wxString& file : { stagedFirst, stagedSecond } )
2480 {
2481 wxFFile output( file, wxT( "wb" ) );
2482 BOOST_REQUIRE( output.IsOpened() && output.Write( wxT( "converted" ) ) && output.Close() );
2483 }
2484
2485 BOOST_REQUIRE( wxMkdir( second ) );
2486 BOOST_CHECK( wxFileName( transaction.Commit() ).SameAs( wxFileName( second ) ) );
2487 BOOST_CHECK( transaction.GetRecoveryError().IsEmpty() );
2488 }
2489
2490 wxFFile input( first, wxT( "rb" ) );
2491 wxString content;
2492 BOOST_REQUIRE( input.IsOpened() && input.ReadAll( &content ) );
2493 BOOST_CHECK_EQUAL( content, wxString( wxT( "original" ) ) );
2494 BOOST_CHECK( wxDirExists( second ) );
2495 wxArrayString files;
2496 wxDir::GetAllFiles( directory, &files );
2497 BOOST_CHECK_EQUAL( files.size(), 1 );
2498}
2499
2500
2501BOOST_AUTO_TEST_CASE( ConcurrentTransactionsUseDifferentOwnedStagingFiles )
2502{
2503 KI_TEST::SCOPED_TEMP_DIR temporary( "kicad_qa_downgrade_transaction_unique" );
2504 const wxString destination = temporary.Path().wstring() + wxT( "/board.kicad_pcb" );
2505
2506 {
2509 wxString firstTemporary = first.Stage( destination );
2510 wxString secondTemporary = second.Stage( destination );
2511 BOOST_REQUIRE( !firstTemporary.IsEmpty() && !secondTemporary.IsEmpty() );
2512 BOOST_CHECK( firstTemporary != secondTemporary );
2513 BOOST_CHECK( wxFileExists( firstTemporary ) );
2514 BOOST_CHECK( wxFileExists( secondTemporary ) );
2515 first.Discard( firstTemporary );
2516 BOOST_CHECK( !wxFileExists( firstTemporary ) );
2517 BOOST_CHECK( wxFileExists( secondTemporary ) );
2518 BOOST_CHECK( !wxFileExists( destination ) );
2519 }
2520
2521 BOOST_CHECK( std::filesystem::is_empty( temporary.Path() ) );
2522}
2523
2524
2525BOOST_AUTO_TEST_CASE( TransactionSymlinkReplacementLeavesLinkedSourcesUnchanged )
2526{
2527 KI_TEST::SCOPED_TEMP_DIR temporary( "kicad_qa_downgrade_transaction_symlink" );
2528 const auto original = temporary.Path() / u8"original-μ.kicad_mod";
2529 const auto alias = temporary.Path() / "alias.kicad_mod";
2530 const auto danglingAlias = temporary.Path() / "dangling.kicad_mod";
2531
2532 {
2533 wxFFile file( original.wstring(), wxT( "wb" ) );
2534 BOOST_REQUIRE( file.IsOpened() && file.Write( wxT( "original source" ) ) && file.Close() );
2535 }
2536
2537 std::error_code error;
2538 std::filesystem::create_symlink( original.filename(), alias, error );
2539
2540 if( error )
2541 {
2542 BOOST_TEST_MESSAGE( "Symlink creation is unavailable on this system" );
2543 return;
2544 }
2545
2546 std::filesystem::create_symlink( "missing.kicad_mod", danglingAlias, error );
2547 BOOST_REQUIRE( !error );
2548
2549 {
2550 DOWNGRADE_FILE_TRANSACTION transaction;
2551
2552 for( const auto& destination : { alias, danglingAlias } )
2553 {
2554 wxString staged = transaction.Stage( destination.wstring() );
2555 BOOST_REQUIRE( !staged.IsEmpty() );
2556 wxFFile file( staged, wxT( "wb" ) );
2557 BOOST_REQUIRE( file.IsOpened() && file.Write( wxT( "converted output" ) ) && file.Close() );
2558 }
2559
2560 BOOST_REQUIRE( transaction.Commit().IsEmpty() );
2561 }
2562
2563 wxFFile file( original.wstring(), wxT( "rb" ) );
2564 wxString content;
2565 BOOST_REQUIRE( file.IsOpened() && file.ReadAll( &content ) );
2566 BOOST_CHECK_EQUAL( content, wxString( wxT( "original source" ) ) );
2567 BOOST_CHECK( !std::filesystem::is_symlink( alias ) );
2568 BOOST_CHECK( !std::filesystem::is_symlink( danglingAlias ) );
2569 BOOST_CHECK_EQUAL( std::distance( std::filesystem::directory_iterator( temporary.Path() ),
2570 std::filesystem::directory_iterator() ),
2571 3 );
2572}
2573
2574
2575static void checkFeature( const COMPATIBILITY_REPORT& aReport, const wxString& aName, DOWNGRADE_BUCKET aBucket,
2576 int aCount )
2577{
2578 for( const COMPAT_ENTRY& entry : aReport.Entries() )
2579 {
2580 if( entry.m_feature == aName )
2581 {
2582 BOOST_CHECK( entry.m_bucket == aBucket );
2583 BOOST_CHECK_EQUAL( entry.m_count, aCount );
2584 BOOST_CHECK( !entry.m_detail.IsEmpty() );
2585 return;
2586 }
2587 }
2588
2589 BOOST_ERROR( "Missing compatibility feature: " + aName.ToStdString() );
2590}
2591
2592
2593BOOST_AUTO_TEST_CASE( GeometricConstraintsDropWithoutMovingBoardOrFootprintGeometry )
2594{
2595 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
2596 {
2597 for( bool drop : { false, true } )
2598 {
2599 BOARD board;
2600 FOOTPRINT* fp = new FOOTPRINT( &board );
2601 board.Add( fp );
2602
2603 std::vector<PCB_SHAPE*> shapes;
2604
2605 for( BOARD_ITEM_CONTAINER* parent :
2606 { static_cast<BOARD_ITEM_CONTAINER*>( &board ), static_cast<BOARD_ITEM_CONTAINER*>( fp ) } )
2607 {
2608 PCB_SHAPE* shape = new PCB_SHAPE( parent, SHAPE_T::SEGMENT );
2609 shape->SetStart( VECTOR2I( 1000000, 2000000 ) );
2610 shape->SetEnd( VECTOR2I( 7000000, 2000000 ) );
2611 parent->Add( shape );
2612 shapes.push_back( shape );
2614 constraint->AddMember( shape->m_Uuid );
2615 parent->Add( constraint );
2616 }
2617
2618 const auto report = ClassifyBoardForDowngrade( &board, target, drop );
2619 BOOST_CHECK( !report.IsBlocked() );
2620 checkFeature( report, wxT( "Geometric constraints" ), DOWNGRADE_BUCKET::DROP, 2 );
2621 BOOST_CHECK_EQUAL( board.Constraints().size(), 1 );
2622 BOOST_CHECK_EQUAL( fp->Constraints().size(), 1 );
2623 DowngradeBoardInPlace( &board, target, drop );
2624 BOOST_CHECK( board.Constraints().empty() );
2625 BOOST_CHECK( fp->Constraints().empty() );
2626
2627 for( PCB_SHAPE* shape : shapes )
2628 {
2629 BOOST_CHECK( shape->GetStart() == VECTOR2I( 1000000, 2000000 ) );
2630 BOOST_CHECK( shape->GetEnd() == VECTOR2I( 7000000, 2000000 ) );
2631 }
2632 }
2633 }
2634}
2635
2636
2637BOOST_AUTO_TEST_CASE( GridAndDrillDrawingLossesAreReportedBeforeRemoval )
2638{
2639 for( bool drop : { false, true } )
2640 {
2641 BOARD board;
2642 board.Add( new PCB_GRID_ITEM( &board ) );
2643 board.Add( new PCB_DRILL_CHART( &board ) );
2644 board.Add( new PCB_DRILL_MAP( &board ) );
2645 board.GetDesignSettings().GetDrillSymbolProfile().SetName( wxT( "Fabrication symbols" ) );
2646 PCB_VIA* via = new PCB_VIA( &board );
2647 via->SetPosition( VECTOR2I( 1000000, 2000000 ) );
2648 board.Add( via );
2649
2650 const auto report = ClassifyBoardForDowngrade( &board, kicad10, drop );
2651 checkFeature( report, wxT( "Local grids" ), DOWNGRADE_BUCKET::DROP, 1 );
2652 checkFeature( report, wxT( "Drill charts and maps" ), DOWNGRADE_BUCKET::DROP, 2 );
2653 checkFeature( report, wxT( "Drill symbol configuration" ), DOWNGRADE_BUCKET::DROP, 1 );
2654 BOOST_CHECK_EQUAL( board.Drawings().size(), 3 );
2655 DowngradeBoardInPlace( &board, kicad10, drop );
2656 BOOST_CHECK( board.Drawings().empty() );
2657 BOOST_REQUIRE_EQUAL( board.Tracks().size(), 1 );
2658 BOOST_CHECK( via->GetPosition() == VECTOR2I( 1000000, 2000000 ) );
2660 }
2661}
2662
2663
2664BOOST_AUTO_TEST_CASE( NewGeneratorsLoseEditabilityAndKeepCopperAndOuterGroups )
2665{
2666 for( bool drop : { false, true } )
2667 {
2668 BOARD board;
2669 PCB_GROUP* outer = new PCB_GROUP( &board );
2670 board.Add( outer );
2671 PCB_VIA_STITCH* stitch = new PCB_VIA_STITCH( &board );
2672 PCB_VIA_STACK* stack = new PCB_VIA_STACK( &board );
2673 board.Add( stitch );
2674 board.Add( stack );
2675 outer->AddItem( stitch );
2676 outer->AddItem( stack );
2677 std::vector<PCB_VIA*> vias;
2678
2679 for( PCB_GENERATOR* generator :
2680 { static_cast<PCB_GENERATOR*>( stitch ), static_cast<PCB_GENERATOR*>( stack ) } )
2681 {
2682 PCB_VIA* via = new PCB_VIA( &board );
2683 via->SetPosition( VECTOR2I( 1000000 * ( vias.size() + 1 ), 2000000 ) );
2684 via->SetWidth( F_Cu, 600000 );
2685 via->SetDrill( 300000 );
2686 board.Add( via );
2687 generator->AddItem( via );
2688 vias.push_back( via );
2689 }
2690
2691 const auto report = ClassifyBoardForDowngrade( &board, kicad10, drop );
2692 checkFeature( report, wxT( "Via stitching and guarding" ), DOWNGRADE_BUCKET::DROP, 1 );
2693 checkFeature( report, wxT( "Microvia stack generators" ), DOWNGRADE_BUCKET::DROP, 1 );
2694 DowngradeBoardInPlace( &board, kicad10, drop );
2695 BOOST_CHECK( board.Generators().empty() );
2696 BOOST_REQUIRE_EQUAL( board.Tracks().size(), 2 );
2697 BOOST_REQUIRE_EQUAL( outer->GetItems().size(), 2 );
2698
2699 for( size_t ii = 0; ii < vias.size(); ++ii )
2700 {
2701 BOOST_CHECK( vias[ii]->GetPosition() == VECTOR2I( 1000000 * ( ii + 1 ), 2000000 ) );
2702 BOOST_CHECK_EQUAL( vias[ii]->GetWidth( F_Cu ), 600000 );
2703 BOOST_CHECK_EQUAL( vias[ii]->GetDrill(), 300000 );
2704 BOOST_CHECK( vias[ii]->GetParentGroup() == outer );
2705 }
2706 }
2707}
2708
2709
2710BOOST_AUTO_TEST_CASE( NativeViaGeneratorFixtureExportsCopperAndOwnershipForBothTargetsAndPolicies )
2711{
2712 SETTINGS_MANAGER settingsManager;
2713 const std::string src =
2714 KI_TEST::GetPcbnewTestDataDir() + "../downgrade/regressions/via_generators/via_generators.kicad_pcb";
2715 const std::string originalBytes = KI_TEST::ReadGoldenText( src );
2717 std::unique_ptr<BOARD> source( io.LoadBoard( src, nullptr ) );
2719 BOOST_REQUIRE_EQUAL( source->GetCopperLayerCount(), 6 );
2720 BOOST_REQUIRE_EQUAL( source->Zones().size(), 3u );
2721 BOOST_REQUIRE_EQUAL( source->Generators().size(), 3u );
2722 BOOST_REQUIRE_EQUAL( source->Groups().size(), 1u );
2723 BOOST_REQUIRE( source->GroupsSanityCheck().IsEmpty() );
2724
2725 const auto copperById = []( const BOARD& aBoard )
2726 {
2727 std::map<KIID, const PCB_TRACK*> items;
2728
2729 for( const PCB_TRACK* item : aBoard.Tracks() )
2730 BOOST_REQUIRE( items.emplace( item->m_Uuid, item ).second );
2731
2732 return items;
2733 };
2734 const auto memberIds = []( const PCB_GROUP& aGroup )
2735 {
2736 std::set<KIID> ids;
2737
2738 for( const EDA_ITEM* member : aGroup.GetItems() )
2739 BOOST_REQUIRE( ids.insert( member->m_Uuid ).second );
2740
2741 return ids;
2742 };
2743 const auto serializeNative = []( BOARD* aBoard )
2744 {
2745 STRING_FORMATTER formatter;
2746 PCB_IO_KICAD_SEXPR serializer;
2747 serializer.FormatBoardToFormatter( &formatter, aBoard );
2748 return formatter.GetString();
2749 };
2750
2751 const auto sourceCopper = copperById( *source );
2752 BOOST_REQUIRE_EQUAL( sourceCopper.size(), 83u );
2753 std::set<KIID> plainVias;
2754 std::set<KIID> generatedVias;
2755 size_t sourceVias = 0;
2756 size_t sourceSegments = 0;
2757 size_t sourceMicrovias = 0;
2758
2759 for( const auto& [id, item] : sourceCopper )
2760 {
2761 if( const PCB_VIA* via = dynamic_cast<const PCB_VIA*>( item ) )
2762 {
2763 ++sourceVias;
2764
2765 if( via->GetViaType() == VIATYPE::MICROVIA )
2766 {
2767 ++sourceMicrovias;
2768 BOOST_REQUIRE( via->Padstack().Drill().is_filled.value_or( false ) );
2769 }
2770
2771 if( !via->GetParentGroup() )
2772 plainVias.insert( id );
2773 }
2774 else
2775 {
2776 BOOST_REQUIRE( item->Type() == PCB_TRACE_T );
2777 ++sourceSegments;
2778 }
2779 }
2780
2781 BOOST_REQUIRE_EQUAL( sourceVias, 80u );
2782 BOOST_REQUIRE_EQUAL( sourceSegments, 3u );
2783 BOOST_REQUIRE_EQUAL( sourceMicrovias, 2u );
2784 BOOST_REQUIRE_EQUAL( plainVias.size(), 3u );
2785
2786 PCB_VIA_STITCH* stitch = nullptr;
2787 PCB_VIA_STITCH* guard = nullptr;
2788 PCB_VIA_STACK* stack = nullptr;
2789
2790 for( PCB_GENERATOR* generator : source->Generators() )
2791 {
2792 if( PCB_VIA_STITCH* viaStitch = dynamic_cast<PCB_VIA_STITCH*>( generator ) )
2793 {
2794 BOOST_REQUIRE( source->FindNet( viaStitch->GetNetCode() ) );
2795 BOOST_REQUIRE( source->FindNet( viaStitch->GetNetCode() )->GetNetname() == wxT( "GND" ) );
2796
2797 if( viaStitch->GetMode() == PCB_VIA_STITCH_MODE::STITCH )
2798 {
2799 BOOST_REQUIRE( !stitch );
2800 stitch = viaStitch;
2801 }
2802 else
2803 {
2804 BOOST_REQUIRE( viaStitch->GetMode() == PCB_VIA_STITCH_MODE::GUARD );
2805 BOOST_REQUIRE( !guard );
2806 guard = viaStitch;
2807 BOOST_REQUIRE( source->FindNet( guard->GetGuardedNetCode() ) );
2808 BOOST_REQUIRE( source->FindNet( guard->GetGuardedNetCode() )->GetNetname() == wxT( "SIGNAL" ) );
2809 }
2810 }
2811 else
2812 {
2813 BOOST_REQUIRE( !stack );
2814 stack = dynamic_cast<PCB_VIA_STACK*>( generator );
2815 BOOST_REQUIRE( stack );
2817 BOOST_REQUIRE( stack->GetStartLayer() == F_Cu );
2818 BOOST_REQUIRE( stack->GetEndLayer() == In2_Cu );
2819 }
2820
2821 for( BOARD_ITEM* member : generator->GetBoardItems() )
2822 {
2823 BOOST_REQUIRE( member->Type() == PCB_VIA_T );
2824 BOOST_REQUIRE( member->GetParentGroup() == static_cast<EDA_GROUP*>( generator ) );
2825 BOOST_REQUIRE( sourceCopper.count( member->m_Uuid ) == 1u );
2826 BOOST_REQUIRE( generatedVias.insert( member->m_Uuid ).second );
2827 }
2828 }
2829
2830 BOOST_REQUIRE( stitch && guard && stack );
2831 BOOST_REQUIRE_EQUAL( stitch->GetItems().size(), 49u );
2832 BOOST_REQUIRE_EQUAL( guard->GetItems().size(), 26u );
2833 BOOST_REQUIRE_EQUAL( stack->GetItems().size(), 2u );
2834 BOOST_REQUIRE_EQUAL( generatedVias.size(), 77u );
2835 const auto stitchMembers = memberIds( *stitch );
2836 PCB_GROUP* outer = *source->Groups().begin();
2837 const KIID outerId = outer->m_Uuid;
2838 const wxString outerName = outer->GetName();
2839 BOOST_REQUIRE( stitch->GetParentGroup() == static_cast<EDA_GROUP*>( outer ) );
2840 BOOST_REQUIRE( !guard->GetParentGroup() && !stack->GetParentGroup() );
2841 BOOST_REQUIRE( memberIds( *outer ) == std::set<KIID>{ stitch->m_Uuid } );
2842
2843 const std::string sourceModel = serializeNative( source.get() );
2844 KI_TEST::SCOPED_TEMP_DIR tmp( "kicad_qa_native_via_generators", "" );
2845
2846 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
2847 {
2848 const bool preTen = target.m_boardVersion == kicad9.m_boardVersion;
2849
2850 for( bool drop : { false, true } )
2851 {
2852 BOOST_TEST_CONTEXT( target.m_id.ToStdString() << " drop=" << drop )
2853 {
2854 const COMPATIBILITY_REPORT preview = ClassifyBoardForDowngrade( source.get(), target, drop );
2855 BOOST_CHECK( !preview.IsBlocked() );
2857 BOOST_CHECK_EQUAL( preview.Count( DOWNGRADE_BUCKET::DROP ), preTen ? 5 : 3 );
2858 BOOST_CHECK_EQUAL( serializeNative( source.get() ), sourceModel );
2859 const std::string dest =
2860 ( tmp.Path()
2861 / ( target.m_id.ToStdString() + ( drop ? "-drop.kicad_pcb" : "-lower.kicad_pcb" ) ) )
2862 .string();
2863 COMPATIBILITY_REPORT report;
2864 wxString unsupportedToken;
2865 BOOST_REQUIRE( ExportBoardToOlderVersion( src, dest, target, report, &unsupportedToken, wxEmptyString,
2866 drop ) );
2867 BOOST_CHECK( unsupportedToken.IsEmpty() );
2868 BOOST_CHECK( !report.IsBlocked() );
2870 BOOST_CHECK_EQUAL( report.Count( DOWNGRADE_BUCKET::DROP ), preTen ? 5 : 3 );
2871 BOOST_REQUIRE_EQUAL( report.Entries().size(), preTen ? 3u : 2u );
2872 checkFeature( report, wxT( "Via stitching and guarding" ), DOWNGRADE_BUCKET::DROP, 2 );
2873 checkFeature( report, wxT( "Microvia stack generators" ), DOWNGRADE_BUCKET::DROP, 1 );
2874
2875 if( preTen )
2876 checkFeature( report, wxT( "Via protection" ), DOWNGRADE_BUCKET::DROP, 2 );
2877
2878 BOOST_REQUIRE_EQUAL( report.Entries().size(), preview.Entries().size() );
2879
2880 for( size_t ii = 0; ii < preview.Entries().size(); ++ii )
2881 {
2882 BOOST_CHECK( report.Entries()[ii].m_bucket == preview.Entries()[ii].m_bucket );
2883 BOOST_CHECK( report.Entries()[ii].m_feature == preview.Entries()[ii].m_feature );
2884 BOOST_CHECK_EQUAL( report.Entries()[ii].m_count, preview.Entries()[ii].m_count );
2885 }
2886
2887 const std::string outputBytes = KI_TEST::ReadGoldenText( dest );
2888 BOOST_CHECK( FindUnsupportedBoardToken( wxString::FromUTF8( outputBytes ), target ).IsEmpty() );
2889 BOOST_CHECK( outputBytes.find( "(generated" ) == std::string::npos );
2890
2891 if( preTen )
2892 {
2893 for( const char* token : { "(filling", "(capping", "(covering", "(plugging" } )
2894 BOOST_CHECK( outputBytes.find( token ) == std::string::npos );
2895 }
2896
2897 std::unique_ptr<BOARD> exported( io.LoadBoard( dest, nullptr ) );
2898 BOOST_REQUIRE( exported );
2899 BOOST_CHECK_EQUAL( exported->GetFileFormatVersionAtLoad(), target.m_boardVersion );
2900 BOOST_CHECK_EQUAL( exported->GetCopperLayerCount(), 6 );
2901 BOOST_CHECK_EQUAL( exported->Zones().size(), 3u );
2902 BOOST_CHECK( exported->Generators().empty() );
2903 BOOST_REQUIRE_EQUAL( exported->Groups().size(), 1u );
2904 BOOST_CHECK( exported->GroupsSanityCheck().IsEmpty() );
2905
2906 const auto exportedCopper = copperById( *exported );
2907 BOOST_REQUIRE_EQUAL( exportedCopper.size(), sourceCopper.size() );
2908 PCB_GROUP* exportedOuter = *exported->Groups().begin();
2909 BOOST_CHECK( exportedOuter->m_Uuid == outerId );
2910 BOOST_CHECK( exportedOuter->GetName() == outerName );
2911 BOOST_CHECK( memberIds( *exportedOuter ) == stitchMembers );
2912
2913 for( const auto& [id, original] : sourceCopper )
2914 {
2915 const auto found = exportedCopper.find( id );
2916 BOOST_REQUIRE( found != exportedCopper.end() );
2917 const PCB_TRACK* item = found->second;
2918 BOOST_CHECK( item->Type() == original->Type() );
2919 BOOST_CHECK( item->GetStart() == original->GetStart() );
2920 BOOST_CHECK( item->GetEnd() == original->GetEnd() );
2921 BOOST_CHECK( item->GetLayerSet() == original->GetLayerSet() );
2922 BOOST_CHECK_EQUAL( item->GetNetname().ToStdString(), original->GetNetname().ToStdString() );
2923
2924 if( const PCB_VIA* originalVia = dynamic_cast<const PCB_VIA*>( original ) )
2925 {
2926 const PCB_VIA* via = dynamic_cast<const PCB_VIA*>( item );
2927 BOOST_REQUIRE( via );
2928 BOOST_CHECK( via->GetPosition() == originalVia->GetPosition() );
2929 BOOST_CHECK( via->GetViaType() == originalVia->GetViaType() );
2930 BOOST_CHECK( via->TopLayer() == originalVia->TopLayer() );
2931 BOOST_CHECK( via->BottomLayer() == originalVia->BottomLayer() );
2932 BOOST_CHECK_EQUAL( via->GetDrillValue(), originalVia->GetDrillValue() );
2933
2934 for( PCB_LAYER_ID layer : LSET::AllCuMask( 6 ) )
2935 BOOST_CHECK_EQUAL( via->GetWidth( layer ), originalVia->GetWidth( layer ) );
2936
2937 const PADSTACK& padstack = via->Padstack();
2938
2939 if( preTen )
2940 {
2941 // The current reader resolves omitted legacy fields to board defaults.
2942 BOOST_CHECK( !padstack.Drill().is_filled.value_or( false ) );
2943 BOOST_CHECK( !padstack.Drill().is_capped.value_or( false ) );
2944 BOOST_CHECK( !padstack.FrontOuterLayers().has_covering.value_or( false ) );
2945 BOOST_CHECK( !padstack.BackOuterLayers().has_covering.value_or( false ) );
2946 BOOST_CHECK( !padstack.FrontOuterLayers().has_plugging.value_or( false ) );
2947 BOOST_CHECK( !padstack.BackOuterLayers().has_plugging.value_or( false ) );
2948 }
2949 else
2950 {
2951 const PADSTACK& originalStack = originalVia->Padstack();
2952 BOOST_CHECK( padstack.Drill().is_filled == originalStack.Drill().is_filled );
2953 BOOST_CHECK( padstack.Drill().is_capped == originalStack.Drill().is_capped );
2955 == originalStack.FrontOuterLayers().has_covering );
2957 == originalStack.BackOuterLayers().has_covering );
2959 == originalStack.FrontOuterLayers().has_plugging );
2961 == originalStack.BackOuterLayers().has_plugging );
2962 }
2963
2964 if( plainVias.count( id ) )
2965 BOOST_CHECK( !via->GetParentGroup() );
2966 }
2967 else
2968 {
2969 BOOST_CHECK( item->GetLayer() == original->GetLayer() );
2970 BOOST_CHECK_EQUAL( item->GetWidth(), original->GetWidth() );
2971 }
2972
2973 if( stitchMembers.count( id ) )
2974 BOOST_CHECK( item->GetParentGroup() == static_cast<EDA_GROUP*>( exportedOuter ) );
2975 else
2976 BOOST_CHECK( !item->GetParentGroup() );
2977 }
2978
2979 for( BOARD_ITEM* drawing : source->Drawings() )
2980 {
2981 if( const EDA_TEXT* text = dynamic_cast<const EDA_TEXT*>( drawing ) )
2982 {
2983 const auto found = std::find_if( exported->Drawings().begin(), exported->Drawings().end(),
2984 [&]( const BOARD_ITEM* aItem )
2985 {
2986 return aItem->m_Uuid == drawing->m_Uuid;
2987 } );
2988 BOOST_REQUIRE( found != exported->Drawings().end() );
2989 const EDA_TEXT* exportedText = dynamic_cast<const EDA_TEXT*>( *found );
2990 BOOST_REQUIRE( exportedText );
2991 BOOST_CHECK( exportedText->GetText() == text->GetText() );
2992 }
2993 }
2994
2995 BOOST_CHECK_EQUAL( serializeNative( source.get() ), sourceModel );
2996 BOOST_CHECK_EQUAL( KI_TEST::ReadGoldenText( src ), originalBytes );
2997 }
2998 }
2999 }
3000}
3001
3002
3003BOOST_AUTO_TEST_CASE( NativeTuningFixturesPreserveCopperAndEraEditabilityForBothPolicies )
3004{
3005 SETTINGS_MANAGER settingsManager;
3007 const std::string lengthPath = KI_TEST::GetPcbnewTestDataDir() + "tuning_generators_load_save.kicad_pcb";
3008 const std::string timePath = KI_TEST::GetPcbnewTestDataDir() + "issue24772/fw16_MCIO8i.kicad_pcb";
3009 const std::string lengthBytes = KI_TEST::ReadGoldenText( lengthPath );
3010 const std::string timeBytes = KI_TEST::ReadGoldenText( timePath );
3011 std::unique_ptr<BOARD> lengthFixture( io.LoadBoard( lengthPath, nullptr ) );
3012 std::unique_ptr<BOARD> timeFixture( io.LoadBoard( timePath, nullptr ) );
3013 BOOST_REQUIRE( lengthFixture && timeFixture );
3014 BOOST_REQUIRE_EQUAL( lengthFixture->Generators().size(), 1u );
3015 BOOST_REQUIRE_EQUAL( timeFixture->Generators().size(), 16u );
3016 PCB_TUNING_PATTERN* originalTime = nullptr;
3017
3018 for( PCB_GENERATOR* generator : timeFixture->Generators() )
3019 {
3020 if( generator->m_Uuid == KIID( "2acf841f-25ce-450c-b8df-dbbc34cc96ac" ) )
3021 originalTime = dynamic_cast<PCB_TUNING_PATTERN*>( generator );
3022 }
3023
3024 BOOST_REQUIRE( originalTime );
3025 BOOST_REQUIRE( originalTime->GetSettings().m_isTimeDomain );
3026 BOOST_REQUIRE_EQUAL( originalTime->GetItems().size(), 1u );
3027 BOARD board;
3028 board.SetCopperLayerCount( timeFixture->GetCopperLayerCount() );
3029 board.SetEnabledLayers( timeFixture->GetEnabledLayers() );
3030 auto length = dynamic_cast<PCB_TUNING_PATTERN*>( ( *lengthFixture->Generators().begin() )->DeepClone() );
3031 auto time = dynamic_cast<PCB_TUNING_PATTERN*>( originalTime->DeepClone() );
3032 BOOST_REQUIRE( length && time );
3033 BOOST_REQUIRE( !length->GetSettings().m_isTimeDomain );
3034 BOOST_REQUIRE_GT( length->GetItems().size(), 1u );
3035
3036 const auto addCopper = [&]( PCB_TRACK* aTrack )
3037 {
3038 NETINFO_ITEM* net = board.FindNet( aTrack->GetNetname() );
3039
3040 if( !net )
3041 {
3042 net = new NETINFO_ITEM( &board, aTrack->GetNetname() );
3043 board.Add( net );
3044 }
3045
3046 aTrack->SetParent( &board );
3047 aTrack->SetNetCode( net->GetNetCode() );
3048 board.Add( aTrack );
3049 };
3050
3051 for( PCB_TUNING_PATTERN* pattern : { length, time } )
3052 {
3053 for( BOARD_ITEM* member : pattern->GetBoardItems() )
3054 {
3055 PCB_TRACK* track = dynamic_cast<PCB_TRACK*>( member );
3056 BOOST_REQUIRE( track );
3057 addCopper( track );
3058 }
3059
3060 board.Add( pattern );
3061 }
3062
3063 PCB_TRACK* originalControl = nullptr;
3064
3065 for( PCB_TRACK* track : timeFixture->Tracks() )
3066 {
3067 if( track->Type() == PCB_TRACE_T && !dynamic_cast<PCB_GENERATOR*>( track->GetParentGroup() ) )
3068 {
3069 originalControl = track;
3070 break;
3071 }
3072 }
3073
3074 BOOST_REQUIRE( originalControl );
3075 auto control = static_cast<PCB_TRACK*>( originalControl->Clone() );
3076 control->SetParentGroup( nullptr );
3077 addCopper( control );
3078 PCB_GROUP* outer = new PCB_GROUP( &board );
3079 outer->SetName( wxT( "Native tuning ownership" ) );
3080 board.Add( outer );
3081 outer->AddItem( length );
3082 outer->AddItem( time );
3083
3084 // Populate the numeric cache through native APIs without changing fixture copper.
3085 double patternLength = 0;
3086
3087 for( BOARD_ITEM* member : length->GetBoardItems() )
3088 patternLength += static_cast<PCB_TRACK*>( member )->GetLength();
3089
3090 STRING_ANY_MAP lengthProperties = length->GetProperties();
3091
3092 // The native setter expects wxString values for these serialized string enums.
3093 for( const char* key : { "tuning_mode", "initial_side", "last_status" } )
3094 {
3095 std::string value;
3096 BOOST_REQUIRE( lengthProperties.get_to( key, value ) );
3097 lengthProperties[key] = wxAny( wxString::FromUTF8( value ) );
3098 }
3099
3100 lengthProperties["last_tuning_length"] = wxAny( patternLength / pcbIUScale.IU_PER_MM );
3101 length->SetProperties( lengthProperties );
3102 const wxString tuningInfo = length->GetRowData().back().second.GetString();
3103 BOOST_REQUIRE( !tuningInfo.IsEmpty() );
3104 const auto memberIds = []( const PCB_GROUP& aGroup )
3105 {
3106 std::set<KIID> ids;
3107
3108 for( const EDA_ITEM* member : aGroup.GetItems() )
3109 BOOST_REQUIRE( ids.insert( member->m_Uuid ).second );
3110
3111 return ids;
3112 };
3113 const auto serializeNative = []( BOARD* aBoard )
3114 {
3115 STRING_FORMATTER formatter;
3116 PCB_IO_KICAD_SEXPR serializer;
3117 serializer.FormatBoardToFormatter( &formatter, aBoard );
3118 return formatter.GetString();
3119 };
3120 std::map<KIID, const PCB_TRACK*> copper;
3121
3122 for( const PCB_TRACK* track : board.Tracks() )
3123 BOOST_REQUIRE( copper.emplace( track->m_Uuid, track ).second );
3124
3125 const auto lengthMembers = memberIds( *length );
3126 const auto timeMembers = memberIds( *time );
3127 BOOST_REQUIRE_EQUAL( copper.size(), lengthMembers.size() + timeMembers.size() + 1u );
3128 BOOST_REQUIRE( board.GroupsSanityCheck().IsEmpty() );
3129 KI_TEST::SCOPED_TEMP_DIR tmp( "kicad_qa_native_tuning_boundaries" );
3130 const std::string src = ( tmp.Path() / "native-source.kicad_pcb" ).string();
3131 io.SaveBoard( src, board );
3132 const std::string sourceBytes = KI_TEST::ReadGoldenText( src );
3133 const std::string sourceModel = serializeNative( &board );
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( &board, target, drop );
3144 BOOST_CHECK( !preview.IsBlocked() );
3145 BOOST_CHECK_EQUAL( preview.Count( DOWNGRADE_BUCKET::LOWER ), 0 );
3146 BOOST_CHECK_EQUAL( preview.Count( DOWNGRADE_BUCKET::DROP ), preTen ? 1 : 0 );
3147 BOOST_CHECK_EQUAL( serializeNative( &board ), sourceModel );
3148 const std::string dest =
3149 ( tmp.Path()
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() );
3159 BOOST_CHECK_EQUAL( report.Count( DOWNGRADE_BUCKET::DROP ), preTen ? 1 : 0 );
3160 BOOST_REQUIRE_EQUAL( report.Entries().size(), preTen ? 1u : 0u );
3161
3162 if( preTen )
3163 checkFeature( report, wxT( "Time-domain tuning patterns" ), DOWNGRADE_BUCKET::DROP, 1 );
3164
3165 const std::string outputBytes = KI_TEST::ReadGoldenText( dest );
3166 BOOST_CHECK( FindUnsupportedBoardToken( wxString::FromUTF8( outputBytes ), target ).IsEmpty() );
3167
3168 if( preTen )
3169 {
3170 for( const char* token : { "(is_time_domain", "(target_delay", "(last_tuning_length" } )
3171 BOOST_CHECK( outputBytes.find( token ) == std::string::npos );
3172
3173 STRING_FORMATTER quoting;
3174 BOOST_CHECK( outputBytes.find( "(last_tuning " + quoting.Quotew( tuningInfo ) + ")" )
3175 != std::string::npos );
3176 }
3177 else
3178 {
3179 BOOST_CHECK( outputBytes.find( "(is_time_domain yes)" ) != std::string::npos );
3180 BOOST_CHECK( outputBytes.find( "(last_tuning_length " ) != std::string::npos );
3181 BOOST_CHECK( outputBytes.find( "(last_tuning " ) == std::string::npos );
3182 }
3183
3184 std::unique_ptr<BOARD> exported( io.LoadBoard( dest, nullptr ) );
3185 BOOST_REQUIRE( exported );
3186 BOOST_CHECK_EQUAL( exported->GetFileFormatVersionAtLoad(), target.m_boardVersion );
3187 BOOST_REQUIRE_EQUAL( exported->Generators().size(), preTen ? 1u : 2u );
3188 BOOST_REQUIRE_EQUAL( exported->Groups().size(), 1u );
3189 BOOST_CHECK( exported->GroupsSanityCheck().IsEmpty() );
3190 PCB_GROUP* exportedOuter = *exported->Groups().begin();
3191 BOOST_CHECK( exportedOuter->m_Uuid == outer->m_Uuid );
3192 std::set<KIID> expectedOuter{ length->m_Uuid };
3193
3194 if( preTen )
3195 expectedOuter.insert( timeMembers.begin(), timeMembers.end() );
3196 else
3197 expectedOuter.insert( time->m_Uuid );
3198
3199 BOOST_CHECK( memberIds( *exportedOuter ) == expectedOuter );
3200 BOOST_REQUIRE_EQUAL( exported->Tracks().size(), copper.size() );
3201
3202 for( const PCB_TRACK* track : exported->Tracks() )
3203 {
3204 const auto found = copper.find( track->m_Uuid );
3205 BOOST_REQUIRE( found != copper.end() );
3206 const PCB_TRACK* original = found->second;
3207 BOOST_CHECK( track->Type() == original->Type() );
3208 BOOST_CHECK( track->GetStart() == original->GetStart() );
3209 BOOST_CHECK( track->GetEnd() == original->GetEnd() );
3210 BOOST_CHECK( track->GetLayerSet() == original->GetLayerSet() );
3211 BOOST_CHECK_EQUAL( track->GetWidth(), original->GetWidth() );
3212 BOOST_CHECK_EQUAL( track->GetNetname().ToStdString(), original->GetNetname().ToStdString() );
3213
3214 if( const PCB_ARC* arc = dynamic_cast<const PCB_ARC*>( original ) )
3215 {
3216 const PCB_ARC* exportedArc = dynamic_cast<const PCB_ARC*>( track );
3217 BOOST_REQUIRE( exportedArc );
3218 BOOST_CHECK( exportedArc->GetMid() == arc->GetMid() );
3219 }
3220
3221 if( timeMembers.count( track->m_Uuid ) && preTen )
3222 BOOST_CHECK( track->GetParentGroup() == static_cast<EDA_GROUP*>( exportedOuter ) );
3223 else if( track->m_Uuid == control->m_Uuid )
3224 BOOST_CHECK( !track->GetParentGroup() );
3225 }
3226
3227 for( PCB_GENERATOR* generator : exported->Generators() )
3228 {
3229 auto pattern = dynamic_cast<PCB_TUNING_PATTERN*>( generator );
3230 BOOST_REQUIRE( pattern );
3231 BOOST_CHECK( pattern->GetParentGroup() == static_cast<EDA_GROUP*>( exportedOuter ) );
3232 BOOST_CHECK( pattern->m_Uuid == length->m_Uuid || ( !preTen && pattern->m_Uuid == time->m_Uuid ) );
3233 const bool isTime = pattern->m_Uuid == time->m_Uuid;
3234 BOOST_CHECK( pattern->GetSettings().m_isTimeDomain == isTime );
3235 BOOST_CHECK( memberIds( *pattern ) == ( isTime ? timeMembers : lengthMembers ) );
3236 const PCB_TUNING_PATTERN* original = isTime ? time : length;
3237 const auto properties = pattern->GetProperties();
3238 const auto originalProperties = original->GetProperties();
3239
3240 for( const char* key :
3241 { "target_length", "target_length_min", "target_length_max", "target_skew", "target_skew_min",
3242 "target_skew_max", "min_spacing", "max_amplitude", "min_amplitude" } )
3243 {
3244 double value = 0;
3245 double originalValue = 0;
3246 BOOST_REQUIRE( properties.get_to( key, value ) );
3247 BOOST_REQUIRE( originalProperties.get_to( key, originalValue ) );
3248 BOOST_CHECK_EQUAL( value, originalValue );
3249 }
3250
3251 if( !preTen )
3252 {
3253 for( const char* key :
3254 { "target_delay", "target_delay_min", "target_delay_max", "last_tuning_length" } )
3255 {
3256 double value = 0;
3257 double originalValue = 0;
3258 BOOST_REQUIRE( properties.get_to( key, value ) );
3259 BOOST_REQUIRE( originalProperties.get_to( key, originalValue ) );
3260 BOOST_CHECK_EQUAL( value, originalValue );
3261 }
3262 }
3263 }
3264
3265 BOOST_CHECK_EQUAL( serializeNative( &board ), sourceModel );
3266 BOOST_CHECK_EQUAL( KI_TEST::ReadGoldenText( src ), sourceBytes );
3267 BOOST_CHECK_EQUAL( KI_TEST::ReadGoldenText( lengthPath ), lengthBytes );
3268 BOOST_CHECK_EQUAL( KI_TEST::ReadGoldenText( timePath ), timeBytes );
3269 }
3270 }
3271 }
3272}
3273
3274
3275BOOST_AUTO_TEST_CASE( UnreviewedTuningPropertiesBlockBothTargetsAndPolicies )
3276{
3277 class UNREVIEWED_TUNING_PATTERN : public PCB_TUNING_PATTERN
3278 {
3279 public:
3281
3282 const STRING_ANY_MAP GetProperties() const override
3283 {
3285 properties.set( "future_tuning_setting", true );
3286 return properties;
3287 }
3288 };
3289
3290 BOARD board;
3291 auto pattern = new UNREVIEWED_TUNING_PATTERN( &board );
3292 auto track = new PCB_TRACK( &board );
3293 track->SetStart( VECTOR2I( 1000000, 2000000 ) );
3294 track->SetEnd( VECTOR2I( 3000000, 2000000 ) );
3295 board.Add( track );
3296 board.Add( pattern );
3297 pattern->AddItem( track );
3298
3299 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
3300 {
3301 for( bool drop : { false, true } )
3302 {
3303 const auto report = ClassifyBoardForDowngrade( &board, target, drop );
3304 checkFeature( report, wxT( "Unreviewed tuning properties" ), DOWNGRADE_BUCKET::BLOCK, 1 );
3305 BOOST_REQUIRE_EQUAL( board.Generators().size(), 1u );
3306 BOOST_CHECK( track->GetParentGroup() == static_cast<EDA_GROUP*>( pattern ) );
3307 }
3308 }
3309
3310 KI_TEST::SCOPED_TEMP_DIR tmp( "kicad_qa_unreviewed_tuning" );
3311
3312 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
3313 {
3314 const wxString path = ( tmp.Path() / ( target.m_id.ToStdString() + ".kicad_pcb" ) ).wstring();
3315 BOOST_CHECK_THROW( SaveBoardForTarget( &board, path, target ), IO_ERROR );
3316 }
3317}
3318
3319
3320BOOST_AUTO_TEST_CASE( HistoricalTuningWritersRejectUnsafeDirectSerialization )
3321{
3322 BOARD board;
3323 auto pattern = new PCB_TUNING_PATTERN( &board );
3324 pattern->SetTargetDelay( 100000 );
3325 auto track = new PCB_TRACK( &board );
3326 track->SetStart( VECTOR2I( 1000000, 2000000 ) );
3327 track->SetEnd( VECTOR2I( 3000000, 2000000 ) );
3328 board.Add( track );
3329 board.Add( pattern );
3330 pattern->AddItem( track );
3331 KI_TEST::SCOPED_TEMP_DIR tmp( "kicad_qa_direct_time_tuning" );
3332 BOOST_CHECK_THROW( SaveBoardForTarget( &board, ( tmp.Path() / "unsafe.kicad_pcb" ).wstring(), kicad9 ), IO_ERROR );
3333
3334 for( const char* token :
3335 { "is_time_domain", "target_delay", "target_delay_min", "target_delay_max", "last_tuning_length" } )
3336 {
3337 const wxString value = wxString::FromUTF8( "(kicad_pcb (generated (" + std::string( token ) + " 0)))" );
3338 BOOST_CHECK_EQUAL( FindUnsupportedBoardToken( value, kicad9 ), wxString::FromUTF8( token ) );
3339 BOOST_CHECK( FindUnsupportedBoardToken( value, kicad10 ).IsEmpty() );
3340 }
3341}
3342
3343
3344BOOST_AUTO_TEST_CASE( HistoricalWritersRejectDirectViaGeneratorsAtomically )
3345{
3346 KI_TEST::SCOPED_TEMP_DIR tmp( "kicad_qa_direct_via_generators" );
3347
3348 for( bool stack : { false, true } )
3349 {
3350 BOARD board;
3351
3352 if( stack )
3353 board.Add( new PCB_VIA_STACK( &board ) );
3354 else
3355 board.Add( new PCB_VIA_STITCH( &board ) );
3356
3357 const std::string kind = stack ? "via-stack" : "via-stitch";
3358
3359 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
3360 {
3361 BOOST_TEST_CONTEXT( kind << " target=" << target.m_id.ToStdString() )
3362 {
3363 const auto absent = tmp.Path() / ( kind + "-" + target.m_id.ToStdString() + "-absent.kicad_pcb" );
3364 BOOST_CHECK_THROW( SaveBoardForTarget( &board, absent.wstring(), target ), IO_ERROR );
3365 BOOST_CHECK( !wxFileExists( absent.wstring() ) );
3366
3367 const auto existing = tmp.Path() / ( kind + "-" + target.m_id.ToStdString() + "-existing.kicad_pcb" );
3368
3369 {
3370 std::ofstream sentinel( existing, std::ios::binary );
3371 sentinel << "existing destination";
3372 BOOST_REQUIRE( sentinel.good() );
3373 }
3374
3375 BOOST_CHECK_THROW( SaveBoardForTarget( &board, existing.wstring(), target ), IO_ERROR );
3376 BOOST_CHECK_EQUAL( KI_TEST::ReadGoldenText( existing.string() ), "existing destination" );
3377 }
3378 }
3379 }
3380}
3381
3382
3383BOOST_AUTO_TEST_CASE( NativeGroupPlacementFixtureKeepsGeometryAndOwnershipForBothPolicies )
3384{
3385 SETTINGS_MANAGER settingsManager;
3386 const std::string src =
3387 KI_TEST::GetPcbnewTestDataDir() + "../downgrade/regressions/group_placement/group_placement.kicad_pcb";
3388 wxFileName projectFile( wxString::FromUTF8( src ) );
3389 projectFile.SetExt( wxT( "kicad_pro" ) );
3390 BOOST_REQUIRE( wxFileExists( projectFile.GetFullPath() ) );
3391 BOOST_REQUIRE( settingsManager.LoadProject( projectFile.GetFullPath(), false ) );
3392 const std::string sourceBytes = KI_TEST::ReadGoldenText( src );
3394 std::unique_ptr<BOARD> source( io.LoadBoard( src, nullptr ) );
3396 BOOST_REQUIRE_EQUAL( source->Zones().size(), 7u );
3397 BOOST_REQUIRE_EQUAL( source->Groups().size(), 4u );
3398 BOOST_REQUIRE_EQUAL( source->Footprints().size(), 4u );
3399 BOOST_REQUIRE( source->GroupsSanityCheck().IsEmpty() );
3400 std::map<KIID, const PCB_TEXT*> sourceNotes;
3401
3402 for( const BOARD_ITEM* drawing : source->Drawings() )
3403 {
3404 if( const PCB_TEXT* note = dynamic_cast<const PCB_TEXT*>( drawing ) )
3405 BOOST_REQUIRE( sourceNotes.emplace( note->m_Uuid, note ).second );
3406 }
3407
3408 BOOST_REQUIRE_EQUAL( sourceNotes.size(), 1u );
3409 std::map<KIID, const ZONE*> sourceZones;
3410 int placementAreas = 0;
3411 int filledOutlines = 0;
3412
3413 for( const ZONE* zone : source->Zones() )
3414 {
3415 BOOST_REQUIRE( sourceZones.emplace( zone->m_Uuid, zone ).second );
3416
3417 if( zone->GetPlacementAreaSourceType() == PLACEMENT_SOURCE_T::GROUP_PLACEMENT )
3418 {
3419 ++placementAreas;
3420 BOOST_REQUIRE( zone->GetPlacementAreaEnabled() );
3421 BOOST_REQUIRE( !zone->GetPlacementAreaSource().IsEmpty() );
3422 }
3423
3424 for( PCB_LAYER_ID layer : zone->GetLayerSet().Seq() )
3425 filledOutlines += zone->GetFilledPolysList( layer )->OutlineCount();
3426 }
3427
3428 BOOST_REQUIRE_EQUAL( placementAreas, 4 );
3429 BOOST_REQUIRE_GT( filledOutlines, 0 );
3430 const auto memberIds = []( const PCB_GROUP& aGroup )
3431 {
3432 std::set<KIID> ids;
3433
3434 for( const EDA_ITEM* member : aGroup.GetItems() )
3435 BOOST_REQUIRE( ids.insert( member->m_Uuid ).second );
3436
3437 return ids;
3438 };
3439 const auto checkPolygons = []( const SHAPE_POLY_SET& aOriginal, const SHAPE_POLY_SET& aExported )
3440 {
3441 BOOST_REQUIRE_EQUAL( aExported.OutlineCount(), aOriginal.OutlineCount() );
3442
3443 for( int outline = 0; outline < aOriginal.OutlineCount(); ++outline )
3444 {
3445 BOOST_CHECK( aExported.COutline( outline ).CompareGeometry( aOriginal.COutline( outline ) ) );
3446 BOOST_REQUIRE_EQUAL( aExported.HoleCount( outline ), aOriginal.HoleCount( outline ) );
3447
3448 for( int hole = 0; hole < aOriginal.HoleCount( outline ); ++hole )
3449 BOOST_CHECK( aExported.CHole( outline, hole ).CompareGeometry( aOriginal.CHole( outline, hole ) ) );
3450 }
3451 };
3452 const auto serializeNative = []( BOARD* aBoard )
3453 {
3454 STRING_FORMATTER formatter;
3455 PCB_IO_KICAD_SEXPR serializer;
3456 serializer.FormatBoardToFormatter( &formatter, aBoard );
3457 return formatter.GetString();
3458 };
3459 const std::string sourceModel = serializeNative( source.get() );
3460 KI_TEST::SCOPED_TEMP_DIR tmp( "kicad_qa_native_group_placement" );
3461 BOOST_CHECK_THROW( SaveBoardForTarget( source.get(), ( tmp.Path() / "unsafe.kicad_pcb" ).wstring(), kicad9 ),
3462 IO_ERROR );
3463 BOOST_CHECK_EQUAL( serializeNative( source.get() ), sourceModel );
3464
3465 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
3466 {
3467 const bool preTen = target.m_boardVersion == kicad9.m_boardVersion;
3468
3469 for( bool drop : { false, true } )
3470 {
3471 BOOST_TEST_CONTEXT( target.m_id.ToStdString() << " drop=" << drop )
3472 {
3473 const auto preview = ClassifyBoardForDowngrade( source.get(), target, drop );
3474 BOOST_CHECK( !preview.IsBlocked() );
3475 BOOST_CHECK_EQUAL( preview.Count( DOWNGRADE_BUCKET::DROP ), preTen ? 4 : 0 );
3476 BOOST_CHECK_EQUAL( preview.Count( DOWNGRADE_BUCKET::LOWER ), 0 );
3477 BOOST_CHECK_EQUAL( serializeNative( source.get() ), sourceModel );
3478 const std::string dest =
3479 ( tmp.Path()
3480 / ( target.m_id.ToStdString() + ( drop ? "-drop.kicad_pcb" : "-lower.kicad_pcb" ) ) )
3481 .string();
3482 COMPATIBILITY_REPORT report;
3483 wxString unsupported;
3485 ExportBoardToOlderVersion( src, dest, target, report, &unsupported, wxEmptyString, drop ) );
3486 BOOST_CHECK( unsupported.IsEmpty() );
3487 BOOST_CHECK( !report.IsBlocked() );
3488 BOOST_CHECK_EQUAL( report.Count( DOWNGRADE_BUCKET::DROP ), preTen ? 4 : 0 );
3490 BOOST_REQUIRE_EQUAL( report.Entries().size(), preTen ? 1u : 0u );
3491
3492 if( preTen )
3493 checkFeature( report, wxT( "Group placement areas" ), DOWNGRADE_BUCKET::DROP, 4 );
3494
3495 const std::string outputBytes = KI_TEST::ReadGoldenText( dest );
3496 BOOST_CHECK( FindUnsupportedBoardToken( wxString::FromUTF8( outputBytes ), target ).IsEmpty() );
3497 std::unique_ptr<BOARD> exported( io.LoadBoard( dest, nullptr ) );
3498 BOOST_REQUIRE( exported );
3499 BOOST_CHECK_EQUAL( exported->GetFileFormatVersionAtLoad(), target.m_boardVersion );
3500 BOOST_REQUIRE_EQUAL( exported->Zones().size(), source->Zones().size() );
3501 BOOST_REQUIRE_EQUAL( exported->Groups().size(), source->Groups().size() );
3502 BOOST_REQUIRE_EQUAL( exported->Footprints().size(), source->Footprints().size() );
3503 BOOST_CHECK( exported->GroupsSanityCheck().IsEmpty() );
3504
3505 for( const auto& [id, original] : sourceNotes )
3506 {
3507 const auto found = std::find_if( exported->Drawings().begin(), exported->Drawings().end(),
3508 [&]( const BOARD_ITEM* aItem )
3509 {
3510 return aItem->m_Uuid == id;
3511 } );
3512 BOOST_REQUIRE( found != exported->Drawings().end() );
3513 const PCB_TEXT* note = dynamic_cast<const PCB_TEXT*>( *found );
3514 BOOST_REQUIRE( note );
3515 BOOST_CHECK( note->GetText() == original->GetText() );
3516 BOOST_CHECK( note->GetPosition() == original->GetPosition() );
3517 BOOST_CHECK( note->GetLayer() == original->GetLayer() );
3518 }
3519
3520 for( const ZONE* zone : exported->Zones() )
3521 {
3522 const auto found = sourceZones.find( zone->m_Uuid );
3523 BOOST_REQUIRE( found != sourceZones.end() );
3524 const ZONE* original = found->second;
3525 BOOST_CHECK( zone->GetZoneName() == original->GetZoneName() );
3526 BOOST_CHECK( zone->GetLayerSet() == original->GetLayerSet() );
3527 BOOST_CHECK( zone->GetIsRuleArea() == original->GetIsRuleArea() );
3528 BOOST_CHECK( zone->GetDoNotAllowTracks() == original->GetDoNotAllowTracks() );
3529 BOOST_CHECK( zone->GetDoNotAllowVias() == original->GetDoNotAllowVias() );
3530 BOOST_CHECK( zone->GetDoNotAllowPads() == original->GetDoNotAllowPads() );
3531 BOOST_CHECK( zone->GetDoNotAllowZoneFills() == original->GetDoNotAllowZoneFills() );
3532 BOOST_CHECK( zone->GetDoNotAllowFootprints() == original->GetDoNotAllowFootprints() );
3533 BOOST_CHECK( zone->GetNetname() == original->GetNetname() );
3534 BOOST_CHECK( zone->IsFilled() == original->IsFilled() );
3535 checkPolygons( original->GetBoardOutline(), zone->GetBoardOutline() );
3536
3537 for( PCB_LAYER_ID layer : original->GetLayerSet().Seq() )
3538 {
3539 const auto originalFill = original->GetFilledPolysList( layer );
3540 const auto exportedFill = zone->GetFilledPolysList( layer );
3541 checkPolygons( *originalFill, *exportedFill );
3542
3543 for( int outline = 0; outline < originalFill->OutlineCount(); ++outline )
3544 BOOST_CHECK( zone->IsIsland( layer, outline ) == original->IsIsland( layer, outline ) );
3545 }
3546
3548 {
3549 BOOST_CHECK( !zone->GetPlacementAreaEnabled() );
3550 BOOST_CHECK( zone->GetPlacementAreaSource().IsEmpty() );
3551 BOOST_CHECK( zone->GetPlacementAreaSourceType() == PLACEMENT_SOURCE_T::SHEETNAME );
3552 }
3553 else
3554 {
3555 BOOST_CHECK( zone->GetPlacementAreaEnabled() == original->GetPlacementAreaEnabled() );
3556 BOOST_CHECK( zone->GetPlacementAreaSource() == original->GetPlacementAreaSource() );
3557 BOOST_CHECK( zone->GetPlacementAreaSourceType() == original->GetPlacementAreaSourceType() );
3558 }
3559 }
3560
3561 for( const PCB_GROUP* group : exported->Groups() )
3562 {
3563 const auto original = std::find_if( source->Groups().begin(), source->Groups().end(),
3564 [&]( const PCB_GROUP* aGroup )
3565 {
3566 return aGroup->m_Uuid == group->m_Uuid;
3567 } );
3568 BOOST_REQUIRE( original != source->Groups().end() );
3569 BOOST_CHECK( group->GetName() == ( *original )->GetName() );
3570 BOOST_CHECK( memberIds( *group ) == memberIds( **original ) );
3571 }
3572
3573 std::map<KIID, const PAD*> sourcePads;
3574
3575 for( const PAD* pad : source->GetPads() )
3576 BOOST_REQUIRE( sourcePads.emplace( pad->m_Uuid, pad ).second );
3577
3578 BOOST_REQUIRE_EQUAL( sourcePads.size(), 8u );
3579 BOOST_REQUIRE_EQUAL( exported->GetPads().size(), sourcePads.size() );
3580
3581 for( const PAD* pad : exported->GetPads() )
3582 {
3583 const auto found = sourcePads.find( pad->m_Uuid );
3584 BOOST_REQUIRE( found != sourcePads.end() );
3585 const PAD* original = found->second;
3586 BOOST_CHECK( pad->GetPosition() == original->GetPosition() );
3587 BOOST_CHECK( pad->GetOrientation() == original->GetOrientation() );
3588 BOOST_CHECK( pad->GetLayerSet() == original->GetLayerSet() );
3589 BOOST_CHECK( pad->GetNetname() == original->GetNetname() );
3590 BOOST_CHECK( pad->GetShape( F_Cu ) == original->GetShape( F_Cu ) );
3591 BOOST_CHECK( pad->GetSize( F_Cu ) == original->GetSize( F_Cu ) );
3592 BOOST_CHECK_EQUAL( pad->GetRoundRectRadiusRatio( F_Cu ),
3593 original->GetRoundRectRadiusRatio( F_Cu ) );
3594 }
3595
3596 BOOST_CHECK_EQUAL( serializeNative( source.get() ), sourceModel );
3597 BOOST_CHECK_EQUAL( KI_TEST::ReadGoldenText( src ), sourceBytes );
3598 }
3599 }
3600 }
3601}
3602
3603
3604BOOST_AUTO_TEST_CASE( GroupPlacementLinkageUsesTheSameFootprintPolicy )
3605{
3606 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
3607 {
3608 for( bool drop : { false, true } )
3609 {
3610 BOARD board;
3611 auto footprint = new FOOTPRINT( &board );
3612 auto zone = new ZONE( footprint );
3613 zone->SetIsRuleArea( true );
3614 zone->SetPlacementAreaEnabled( true );
3615 zone->SetPlacementAreaSourceType( PLACEMENT_SOURCE_T::GROUP_PLACEMENT );
3616 zone->SetPlacementAreaSource( wxT( "Repeated group" ) );
3617 zone->SetDoNotAllowTracks( true );
3618 footprint->Add( zone );
3619 board.Add( footprint );
3620 const auto report = ClassifyBoardForDowngrade( &board, target, drop );
3621 BOOST_CHECK_EQUAL( report.Count( DOWNGRADE_BUCKET::DROP ), target.m_id == kicad9.m_id ? 1 : 0 );
3622 BOOST_CHECK( zone->GetPlacementAreaEnabled() );
3623 DowngradeFootprintInPlace( footprint, target, drop );
3624 BOOST_CHECK( zone->GetIsRuleArea() );
3625 BOOST_CHECK( zone->GetDoNotAllowTracks() );
3626 BOOST_CHECK( zone->GetPlacementAreaEnabled() == ( target.m_id == kicad10.m_id ) );
3627 BOOST_CHECK( zone->GetPlacementAreaSource()
3628 == ( target.m_id == kicad10.m_id ? wxT( "Repeated group" ) : wxEmptyString ) );
3629 }
3630 }
3631}
3632
3633
3634BOOST_AUTO_TEST_CASE( GroupPlacementGuardIgnoresOrdinaryGroupsAndQuotedText )
3635{
3636 BOOST_CHECK( FindUnsupportedBoardToken( wxT( "(zone (placement (group \"SourceA\")))" ), kicad9 )
3637 == wxT( "placement/group" ) );
3638 BOOST_CHECK( FindUnsupportedBoardToken( wxT( "(zone (placement (group \"SourceA\")))" ), kicad10 ).IsEmpty() );
3639 BOOST_CHECK( FindUnsupportedBoardToken( wxT( "(group \"Ordinary\" (members \"uuid\"))" ), kicad9 ).IsEmpty() );
3640 BOOST_CHECK( FindUnsupportedBoardToken( wxT( "(gr_text \"(zone (placement (group foo)))\")" ), kicad9 ).IsEmpty() );
3641}
3642
3643
3644BOOST_AUTO_TEST_CASE( InertGroupPlacementMetadataDoesNotRequireConsent )
3645{
3646 for( bool drop : { false, true } )
3647 {
3648 BOARD board;
3649 auto emptyRuleArea = new ZONE( &board );
3650 emptyRuleArea->SetIsRuleArea( true );
3651 emptyRuleArea->SetPlacementAreaSourceType( PLACEMENT_SOURCE_T::GROUP_PLACEMENT );
3652 board.Add( emptyRuleArea );
3653 auto ordinaryZone = new ZONE( &board );
3654 ordinaryZone->SetPlacementAreaSourceType( PLACEMENT_SOURCE_T::GROUP_PLACEMENT );
3655 ordinaryZone->SetPlacementAreaEnabled( true );
3656 ordinaryZone->SetPlacementAreaSource( wxT( "Unused by ordinary copper zones" ) );
3657 board.Add( ordinaryZone );
3658 BOOST_CHECK( !ClassifyBoardForDowngrade( &board, kicad9, drop ).IsLossy() );
3659 DowngradeBoardInPlace( &board, kicad9, drop );
3660 BOOST_CHECK( emptyRuleArea->GetIsRuleArea() );
3661 BOOST_CHECK( !ordinaryZone->GetIsRuleArea() );
3662 BOOST_CHECK( emptyRuleArea->GetPlacementAreaSourceType() == PLACEMENT_SOURCE_T::SHEETNAME );
3663 BOOST_CHECK( ordinaryZone->GetPlacementAreaSourceType() == PLACEMENT_SOURCE_T::SHEETNAME );
3664 }
3665}
3666
3667
3668BOOST_AUTO_TEST_CASE( UnreviewedGeneratorKindBlocksBothPolicies )
3669{
3670 class FUTURE_GENERATOR : public PCB_VIA_STITCH
3671 {
3672 public:
3673 explicit FUTURE_GENERATOR( BOARD* aBoard ) :
3674 PCB_VIA_STITCH( aBoard )
3675 {
3676 }
3677 wxString GetGeneratorType() const override { return wxT( "future_generator" ); }
3678 };
3679
3680 BOARD board;
3681 board.Add( new FUTURE_GENERATOR( &board ) );
3682
3683 for( bool drop : { false, true } )
3684 BOOST_CHECK( ClassifyBoardForDowngrade( &board, kicad10, drop ).IsBlocked() );
3685}
3686
3687
3688BOOST_AUTO_TEST_CASE( LineEndingsLowerPrintedInkOrDropOnlyEmbellishments )
3689{
3690 for( bool drop : { false, true } )
3691 {
3692 BOARD board;
3693 FOOTPRINT* fp = new FOOTPRINT( &board );
3694 board.Add( fp );
3695 std::vector<PCB_SHAPE*> shapes;
3696 double expectedArea = 0;
3697
3698 for( BOARD_ITEM_CONTAINER* parent :
3699 { static_cast<BOARD_ITEM_CONTAINER*>( &board ), static_cast<BOARD_ITEM_CONTAINER*>( fp ) } )
3700 {
3701 PCB_SHAPE* shape = new PCB_SHAPE( parent, SHAPE_T::SEGMENT );
3702 shape->SetStart( VECTOR2I( 0, 0 ) );
3703 shape->SetEnd( VECTOR2I( 10000000, 0 ) );
3704 shape->SetWidth( 200000 );
3705 shape->SetLayer( F_SilkS );
3706 shape->SetEndEnding( LINE_ENDING( LINE_ENDING_STYLE::ARROW, 2000000, 1000000 ) );
3707 parent->Add( shape );
3708 shapes.push_back( shape );
3709 SHAPE_POLY_SET ink;
3710 shape->TransformWithLineEndingsToPolygon( ink, 0, 5000, ERROR_INSIDE );
3711 ink.Simplify();
3712 expectedArea += ink.Area();
3713 }
3714
3715 const auto report = ClassifyBoardForDowngrade( &board, kicad10, drop );
3716 checkFeature( report, wxT( "Graphic line endings" ), drop ? DOWNGRADE_BUCKET::DROP : DOWNGRADE_BUCKET::LOWER,
3717 2 );
3718 DowngradeBoardInPlace( &board, kicad10, drop );
3719
3720 if( drop )
3721 {
3722 for( PCB_SHAPE* shape : shapes )
3723 {
3724 BOOST_CHECK( shape->GetShape() == SHAPE_T::SEGMENT );
3725 BOOST_CHECK( shape->GetEnd() == VECTOR2I( 10000000, 0 ) );
3726 BOOST_CHECK( shape->GetEndEndingStyle() == LINE_ENDING_STYLE::NONE );
3727 }
3728 }
3729 else
3730 {
3731 double area = 0;
3732
3733 for( BOARD_ITEM* item : board.Drawings() )
3734 area += static_cast<PCB_SHAPE*>( item )->GetPolyShape().Area();
3735
3736 for( BOARD_ITEM* item : fp->GraphicalItems() )
3737 area += static_cast<PCB_SHAPE*>( item )->GetPolyShape().Area();
3738
3739 BOOST_CHECK_CLOSE( area, expectedArea, 0.05 );
3740 }
3741 }
3742}
3743
3744
3745BOOST_AUTO_TEST_CASE( DashedEndedLinesBlockApproximationButAllowDroppingEndings )
3746{
3747 BOARD board;
3748 PCB_SHAPE* line = new PCB_SHAPE( &board, SHAPE_T::SEGMENT );
3749 line->SetStart( VECTOR2I( 0, 0 ) );
3750 line->SetEnd( VECTOR2I( 10000000, 0 ) );
3751 line->SetWidth( 200000 );
3754 board.Add( line );
3755 BOOST_CHECK( ClassifyBoardForDowngrade( &board, kicad10 ).IsBlocked() );
3756 BOOST_CHECK( !ClassifyBoardForDowngrade( &board, kicad10, true ).IsBlocked() );
3757 DowngradeBoardInPlace( &board, kicad10, true );
3760}
3761
3762
3763BOOST_AUTO_TEST_CASE( CustomPropertiesBakeOnlyAffectedVariablesBeforeRemoval )
3764{
3765 for( bool footprintOnly : { false, true } )
3766 {
3767 BOARD board;
3768 FOOTPRINT* fp = new FOOTPRINT( &board );
3769 fp->SetReference( wxT( "42" ) );
3770 fp->SetCustomProperty( wxT( "Serial" ), wxT( "42" ) );
3771 fp->Value().SetText( wxT( "${PROPERTY.Reference}/${REFERENCE}" ) );
3772 fp->Reference().SetCustomProperty( wxT( "Owner" ), wxT( "Engineering" ) );
3773 PCB_TEXT* escaped = new PCB_TEXT( fp );
3774 escaped->SetText( wxT( "\\${PROPERTY.Serial}" ) );
3775 fp->Add( escaped );
3776 PCB_TEXT* expression = new PCB_TEXT( fp );
3777 expression->SetText( wxT( "@{${PROPERTY.Reference}+1}/${REFERENCE}" ) );
3778 fp->Add( expression );
3779 board.Add( fp );
3780 const auto report = ClassifyBoardForDowngrade( &board, kicad10 );
3781 checkFeature( report, wxT( "Custom properties" ), DOWNGRADE_BUCKET::DROP, 2 );
3782 BOOST_CHECK( !report.IsBlocked() );
3783
3784 if( footprintOnly )
3786 else
3787 DowngradeBoardInPlace( &board, kicad10 );
3788
3791 BOOST_CHECK_EQUAL( fp->Value().GetText(), wxString( "42/${REFERENCE}" ) );
3792 BOOST_CHECK_EQUAL( expression->GetText(), wxString( "43/${REFERENCE}" ) );
3793 BOOST_CHECK_EQUAL( escaped->GetText(), wxString( "\\${PROPERTY.Serial}" ) );
3794 }
3795}
3796
3797
3798BOOST_AUTO_TEST_CASE( UnresolvedPropertyVariablesBlockBothPolicies )
3799{
3800 BOARD board;
3801 FOOTPRINT* fp = new FOOTPRINT( &board );
3802 fp->Value().SetText( wxT( "${PROPERTY.Missing}" ) );
3803 board.Add( fp );
3804
3805 for( bool drop : { false, true } )
3806 BOOST_CHECK( ClassifyBoardForDowngrade( &board, kicad10, drop ).IsBlocked() );
3807}
3808
3809
3810BOOST_AUTO_TEST_CASE( UsedProjectVariablesBakeInFootprintContextAndUnusedDefinitionsDoNotBlock )
3811{
3812 KI_TEST::SCOPED_TEMP_DIR tmp( "kicad_qa_property_dependencies" );
3813 const wxString projectPath = ( tmp.Path() / "source.kicad_pro" ).string();
3814
3815 {
3816 std::ofstream project( projectPath.ToStdString() );
3817 project << R"({"meta":{"version":4},"text_variables":{"SERIAL":"${PROPERTY.Reference}","ALIAS":"${SERIAL}","UNUSED":"${PROPERTY.Missing}"}})";
3818 }
3819
3820 SETTINGS_MANAGER settings;
3821 BOOST_REQUIRE( settings.LoadProject( projectPath, false ) );
3822 BOARD board;
3823 board.SetProject( settings.GetProject( projectPath ), true );
3824
3825 std::vector<FOOTPRINT*> footprints;
3826
3827 for( const wxString& serial : { wxT( "42" ), wxT( "73" ) } )
3828 {
3829 FOOTPRINT* fp = new FOOTPRINT( &board );
3830 fp->SetReference( serial );
3831 fp->SetCustomProperty( wxT( "Serial" ), serial );
3832 fp->Value().SetText( wxT( "${ALIAS}/${REFERENCE}" ) );
3833 board.Add( fp );
3834 footprints.push_back( fp );
3835 }
3836
3837 BOOST_CHECK( !ClassifyBoardForDowngrade( &board, kicad10 ).IsBlocked() );
3838 DowngradeBoardInPlace( &board, kicad10 );
3839 BOOST_CHECK_EQUAL( footprints[0]->Value().GetText(), wxString( "42/${REFERENCE}" ) );
3840 BOOST_CHECK_EQUAL( footprints[1]->Value().GetText(), wxString( "73/${REFERENCE}" ) );
3841
3842 footprints[0]->Value().SetText( wxT( "${UNUSED}" ) );
3843 BOOST_CHECK( ClassifyBoardForDowngrade( &board, kicad10 ).IsBlocked() );
3844 BOOST_CHECK( ClassifyBoardForDowngrade( &board, kicad10, true ).IsBlocked() );
3845}
3846
3847
3848BOOST_AUTO_TEST_CASE( CachedBoardVariableDependenciesBakeInFootprintContext )
3849{
3850 BOARD board;
3851 board.SetProperties( { { wxT( "SERIAL" ), wxT( "${PROPERTY.Reference}" ) },
3852 { wxT( "UNUSED" ), wxT( "${PROPERTY.Missing}" ) } } );
3853 FOOTPRINT* fp = new FOOTPRINT( &board );
3854 fp->SetReference( wxT( "42" ) );
3855 fp->Value().SetText( wxT( "${SERIAL}/${REFERENCE}" ) );
3856 board.Add( fp );
3857 BOOST_CHECK( !ClassifyBoardForDowngrade( &board, kicad10 ).IsBlocked() );
3858 DowngradeBoardInPlace( &board, kicad10 );
3859 BOOST_CHECK_EQUAL( fp->Value().GetText(), wxString( "42/${REFERENCE}" ) );
3860}
3861
3862
3863BOOST_AUTO_TEST_CASE( MixedPropertyTextKeepsEscapedLiteralsAndSupportedVariables )
3864{
3865 BOARD board;
3866 FOOTPRINT* fp = new FOOTPRINT( &board );
3867 fp->SetReference( wxT( "42" ) );
3868 fp->SetCustomProperty( wxT( "Serial" ), wxT( "42" ) );
3869 fp->SetKeywords( wxT( "\\${REFERENCE}/\\@{1+2}" ) );
3870 fp->Value().SetText( wxT( "${PROPERTY.Reference}/\\${U1:PROPERTY.Missing}/${REFERENCE}" ) );
3871 PCB_TEXT* text = new PCB_TEXT( fp );
3872 text->SetText( wxT( "${PROPERTY.Keywords}/${REFERENCE}" ) );
3873 fp->Add( text );
3874 board.Add( fp );
3875 const wxString shown = text->GetShownText( FOR_CANVAS );
3876 BOOST_CHECK( !ClassifyBoardForDowngrade( &board, kicad10 ).IsBlocked() );
3877 DowngradeBoardInPlace( &board, kicad10 );
3878 BOOST_CHECK_EQUAL( fp->Value().GetText(), wxString( "42/\\${U1:PROPERTY.Missing}/${REFERENCE}" ) );
3879 BOOST_CHECK( !text->GetText().Contains( wxT( "PROPERTY." ) ) );
3880 BOOST_CHECK_EQUAL( text->GetShownText( FOR_CANVAS ), shown );
3881}
3882
3883
3884BOOST_AUTO_TEST_CASE( NewExpressionUnitAliasIsBakedWithoutFreezingOtherVariables )
3885{
3886 BOARD board;
3887 FOOTPRINT* fp = new FOOTPRINT( &board );
3888 fp->SetReference( wxT( "U1" ) );
3889 fp->Value().SetText( wxT( "@{1000mils}/${REFERENCE}" ) );
3890 board.Add( fp );
3891 BOOST_CHECK( !ClassifyBoardForDowngrade( &board, kicad10 ).IsBlocked() );
3892 DowngradeBoardInPlace( &board, kicad10 );
3893 BOOST_CHECK_EQUAL( fp->Value().GetText(), wxString( "25.4/${REFERENCE}" ) );
3894}
3895
3896
3897BOOST_AUTO_TEST_CASE( UnresolvedAffectedExpressionsBlockBothPolicies )
3898{
3899 for( const wxString& raw :
3900 { wxT( "@{${PROPERTY.Reference}+${MISSING}}" ), wxT( "@{unknown_function(${PROPERTY.Reference})}" ) } )
3901 {
3902 BOARD board;
3903 FOOTPRINT* fp = new FOOTPRINT( &board );
3904 fp->SetReference( wxT( "42" ) );
3905 fp->SetCustomProperty( wxT( "Serial" ), wxT( "42" ) );
3906 fp->Value().SetText( raw );
3907 board.Add( fp );
3908
3909 for( bool drop : { false, true } )
3910 BOOST_CHECK( ClassifyBoardForDowngrade( &board, kicad10, drop ).IsBlocked() );
3911 }
3912}
3913
3914
3915BOOST_AUTO_TEST_CASE( EscapedTextControlsBlockKicad9AndRemainLiteralInKicad10 )
3916{
3917 for( bool inProperty : { false, true } )
3918 {
3919 BOARD board;
3920 FOOTPRINT* fp = new FOOTPRINT( &board );
3921 fp->SetReference( wxT( "U1" ) );
3922 const wxString literal = wxT( "\\${REFERENCE}/\\@{1+2}" );
3923 fp->SetKeywords( literal );
3924 fp->Value().SetText( inProperty ? wxString( wxT( "${PROPERTY.Keywords}" ) ) : literal );
3925 board.Add( fp );
3926 const wxString shown = fp->Value().GetShownText( FOR_CANVAS );
3927
3928 for( bool drop : { false, true } )
3929 {
3930 BOOST_CHECK( ClassifyBoardForDowngrade( &board, kicad9, drop ).IsBlocked() );
3931 BOOST_CHECK( !ClassifyBoardForDowngrade( &board, kicad10, drop ).IsBlocked() );
3932 }
3933
3934 DowngradeBoardInPlace( &board, kicad10 );
3936 }
3937}
3938
3939
3940BOOST_AUTO_TEST_CASE( TitleBlockExpressionsBakeBeforeDependentFootprintText )
3941{
3942 for( bool drop : { false, true } )
3943 {
3944 BOARD board;
3945 board.GetTitleBlock().SetTitle( wxT( "@{1000mils}" ) );
3946 FOOTPRINT* fp = new FOOTPRINT( &board );
3947 fp->SetReference( wxT( "U1" ) );
3948 fp->Value().SetText( wxT( "${TITLE}/${REFERENCE}" ) );
3949 board.Add( fp );
3950 const auto report = ClassifyBoardForDowngrade( &board, kicad10, drop );
3951 BOOST_CHECK( !report.IsBlocked() );
3952 checkFeature( report, wxT( "Post-target text variables" ), DOWNGRADE_BUCKET::DROP, 2 );
3953 DowngradeBoardInPlace( &board, kicad10, drop );
3954 BOOST_CHECK_EQUAL( board.GetTitleBlock().GetTitle(), wxString( "25.4" ) );
3955 BOOST_CHECK_EQUAL( fp->Value().GetText(), wxString( "25.4/${REFERENCE}" ) );
3956 }
3957}
3958
3959
3960BOOST_AUTO_TEST_CASE( TitleBlockUnresolvablePropertyContextBlocksBothPolicies )
3961{
3962 BOARD board;
3963 board.GetTitleBlock().SetComment( 0, wxT( "${PROPERTY.Reference}" ) );
3964
3965 for( bool drop : { false, true } )
3966 BOOST_CHECK( ClassifyBoardForDowngrade( &board, kicad10, drop ).IsBlocked() );
3967}
3968
3969
3970BOOST_AUTO_TEST_CASE( TitleBlockSelfReferenceDoesNotHideUsedProjectDependencies )
3971{
3972 KI_TEST::SCOPED_TEMP_DIR temporary( "kicad_qa_title_dependencies" );
3973 const wxString projectPath = ( temporary.Path() / "source.kicad_pro" ).string();
3974
3975 {
3976 std::ofstream project( projectPath.ToStdString() );
3977 project << R"({"meta":{"version":4},"text_variables":{"TITLE":"${PROPERTY.Reference}"}})";
3978 }
3979
3980 SETTINGS_MANAGER settings;
3981 BOOST_REQUIRE( settings.LoadProject( projectPath, false ) );
3982 BOARD board;
3983 board.SetProject( settings.GetProject( projectPath ), true );
3984 board.GetTitleBlock().SetTitle( wxT( "${TITLE}" ) );
3985 FOOTPRINT* fp = new FOOTPRINT( &board );
3986 fp->SetReference( wxT( "42" ) );
3987 fp->Value().SetText( wxT( "${TITLE}" ) );
3988 board.Add( fp );
3989 BOOST_CHECK_EQUAL( fp->Value().GetShownText( FOR_CANVAS ), wxString( "42" ) );
3990
3991 for( bool drop : { false, true } )
3992 BOOST_CHECK( ClassifyBoardForDowngrade( &board, kicad10, drop ).IsBlocked() );
3993
3994 board.GetTitleBlock().SetTitle( wxT( "Ordinary title" ) );
3995 const auto report = ClassifyBoardForDowngrade( &board, kicad10 );
3996 BOOST_CHECK( !report.IsBlocked() );
3997 BOOST_CHECK( !report.IsLossy() );
3998}
3999
4000
4001BOOST_AUTO_TEST_CASE( AffectedTableExpressionsUseNativeCellCoordinates )
4002{
4003 BOARD board;
4004 FOOTPRINT* fp = new FOOTPRINT( &board );
4005 fp->SetReference( wxT( "42" ) );
4006 board.Add( fp );
4007 PCB_TABLE* table = new PCB_TABLE( fp );
4008 table->SetColCount( 1 );
4009 table->SetColWidth( 0, 10000000 );
4010 table->SetRowHeight( 0, 6000000 );
4011 PCB_TABLECELL* cell = new PCB_TABLECELL( table );
4012 cell->SetText( wxT( "@{${PROPERTY.Reference}+${ROW}}/${COL}/${REFERENCE}" ) );
4013 table->AddCell( cell );
4014 fp->Add( table );
4015 BOOST_CHECK( !ClassifyBoardForDowngrade( &board, kicad10 ).IsBlocked() );
4016 DowngradeBoardInPlace( &board, kicad10 );
4017 BOOST_CHECK_EQUAL( cell->GetText(), wxString( "43/${COL}/${REFERENCE}" ) );
4018}
4019
4020
4021BOOST_AUTO_TEST_CASE( FootprintFileUsesTheSamePostMasterPoliciesAsBoardFootprints )
4022{
4023 SETTINGS_MANAGER settingsManager;
4024 KI_TEST::SCOPED_TEMP_DIR temporary( "kicad_qa_fp_post_master" );
4025 const wxString source = ( temporary.Path() / "source.kicad_mod" ).string();
4026 FOOTPRINT footprint( nullptr );
4027 footprint.SetReference( wxT( "42" ) );
4028 footprint.Value().SetText( wxT( "${PROPERTY.Reference}/${REFERENCE}" ) );
4029 footprint.SetCustomProperty( wxT( "Owner" ), wxT( "QA" ) );
4030 footprint.SetExcludedFromSim( true );
4031 PCB_SHAPE* line = new PCB_SHAPE( &footprint, SHAPE_T::SEGMENT );
4032 line->SetStart( VECTOR2I( 0, 0 ) );
4033 line->SetEnd( VECTOR2I( 10000000, 0 ) );
4034 line->SetWidth( 200000 );
4035 line->SetLayer( F_SilkS );
4037 footprint.Add( line );
4038 PCB_CONSTRAINT* constraint = new PCB_CONSTRAINT( &footprint, PCB_CONSTRAINT_TYPE::HORIZONTAL );
4039 constraint->AddMember( line->m_Uuid );
4040 footprint.Add( constraint );
4042 io.FootprintSave( source, &footprint );
4043
4044 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
4045 {
4046 for( bool drop : { false, true } )
4047 {
4048 const wxString destination = ( temporary.Path() / "out.kicad_mod" ).string();
4049 BOOST_REQUIRE( DowngradeFootprintFileToTemp( source, destination, target, drop )
4051 wxString name;
4052 auto converted = io.ImportFootprint( destination, name );
4053 BOOST_REQUIRE( converted );
4054 BOOST_CHECK( converted->Constraints().empty() );
4055 BOOST_CHECK( !converted->HasCustomProperties() );
4056 BOOST_CHECK( !converted->IsExcludedFromSim() );
4057 BOOST_CHECK_EQUAL( converted->Value().GetText(), wxString( "42/${REFERENCE}" ) );
4058 BOOST_REQUIRE( !converted->GraphicalItems().empty() );
4059
4060 for( BOARD_ITEM* item : converted->GraphicalItems() )
4061 {
4062 BOOST_REQUIRE( item->Type() == PCB_SHAPE_T );
4063 const PCB_SHAPE* shape = static_cast<PCB_SHAPE*>( item );
4065 BOOST_CHECK( shape->GetShape() == ( drop ? SHAPE_T::SEGMENT : SHAPE_T::POLY ) );
4066 }
4067 }
4068 }
4069}
4070
4071
4072BOOST_AUTO_TEST_CASE( SimulationExclusionsFollowSelectedVariantAndReportTheirLoss )
4073{
4074 for( bool drop : { false, true } )
4075 {
4076 BOARD board;
4077 FOOTPRINT* fp = new FOOTPRINT( &board );
4078 fp->SetExcludedFromSim( false );
4079 fp->AddVariant( wxT( "selected" ) )->SetExcludedFromSim( true );
4080 board.Add( fp );
4081 FlattenBoardVariant( &board, wxT( "selected" ) );
4083 const auto report = ClassifyBoardForDowngrade( &board, kicad10, drop );
4084 checkFeature( report, wxT( "Footprint simulation exclusions" ), DOWNGRADE_BUCKET::DROP, 1 );
4085 DowngradeBoardInPlace( &board, kicad10, drop );
4087 for( const auto& [name, variant] : fp->GetVariants() )
4088 BOOST_CHECK( !variant.GetExcludedFromSim() );
4089 }
4090}
4091
4092
4093BOOST_AUTO_TEST_CASE( LegacyBoldWidthsPreserveNestedTextAndDoNotMutateSource )
4094{
4095 SETTINGS_MANAGER settingsManager;
4096 BOARD board;
4097 FOOTPRINT* fp = new FOOTPRINT( &board );
4098 board.Add( fp );
4099 std::vector<EDA_TEXT*> bold;
4100 PCB_TEXT* text = new PCB_TEXT( &board );
4101 text->SetText( wxT( "board bold" ) );
4102 board.Add( text );
4103 bold.push_back( text );
4104 fp->Value().SetText( wxT( "footprint bold" ) );
4105 bold.push_back( &fp->Value() );
4106 PCB_TEXT* fpText = new PCB_TEXT( fp );
4107 fpText->SetText( wxT( "graphic bold" ) );
4108 fp->Add( fpText );
4109 bold.push_back( fpText );
4110 PCB_TABLE* table = new PCB_TABLE( fp, 100000 );
4111 table->SetColCount( 1 );
4112 table->SetColWidth( 0, 10000000 );
4113 table->SetRowHeight( 0, 5000000 );
4114 PCB_TABLECELL* cell = new PCB_TABLECELL( table );
4115 cell->SetText( wxT( "cell bold" ) );
4116 table->AddCell( cell );
4117 fp->Add( table );
4118 bold.push_back( cell );
4119 PCB_DIM_ALIGNED* dimension = new PCB_DIM_ALIGNED( &board );
4120 dimension->SetStart( VECTOR2I( 0, 0 ) );
4121 dimension->SetEnd( VECTOR2I( 10000000, 0 ) );
4122 dimension->SetHeight( 2000000 );
4123 dimension->ChangeOverrideText( wxT( "dimension bold" ) );
4124 board.Add( dimension );
4125 bold.push_back( dimension );
4126
4127 for( EDA_TEXT* item : bold )
4128 {
4129 item->SetBold( true );
4130 item->SetTextThickness( 100000 );
4131 }
4132
4133 PCB_TEXT* automatic = new PCB_TEXT( &board );
4134 automatic->SetText( wxT( "auto bold" ) );
4135 automatic->SetBold( true );
4136 automatic->SetTextThickness( 0 );
4137 board.Add( automatic );
4138 PCB_TEXT* outline = new PCB_TEXT( &board );
4139 outline->SetText( wxT( "outline bold" ) );
4140 outline->SetFont( KIFONT::FONT::GetFont( wxT( "DejaVu Sans" ), true ) );
4141 outline->SetBold( true );
4142 outline->SetTextThickness( 100000 );
4143 board.Add( outline );
4144 const bool outlineAvailable = outline->GetFont() && outline->GetFont()->IsOutline();
4145 KI_TEST::SCOPED_TEMP_DIR temporary( "kicad_qa_legacy_bold_widths" );
4146
4147 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
4148 {
4149 const std::string first = ( temporary.Path() / ( target.m_id.ToStdString() + "-first.kicad_pcb" ) ).string();
4150 const std::string second = ( temporary.Path() / ( target.m_id.ToStdString() + "-second.kicad_pcb" ) ).string();
4151 SaveBoardForTarget( &board, first, target );
4152 SaveBoardForTarget( &board, second, target );
4153 const std::string content = KI_TEST::ReadGoldenText( first );
4154 BOOST_CHECK( content.find( "(thickness 0.16)" ) != std::string::npos );
4155 BOOST_CHECK( content == KI_TEST::ReadGoldenText( second ) );
4156
4157 for( EDA_TEXT* item : bold )
4158 BOOST_CHECK_EQUAL( item->GetTextThickness(), 100000 );
4159
4160 BOOST_CHECK_EQUAL( automatic->GetTextThickness(), 0 );
4161 BOOST_CHECK_EQUAL( outline->GetTextThickness(), 100000 );
4163 auto reloaded = io.LoadBoard( first );
4164 BOOST_REQUIRE( reloaded );
4165 size_t checked = 0;
4166 reloaded->RunOnChildren(
4167 [&]( BOARD_ITEM* item )
4168 {
4169 EDA_TEXT* reloadedText = dynamic_cast<EDA_TEXT*>( item );
4170
4171 if( !reloadedText || !reloadedText->IsBold() )
4172 return;
4173
4174 if( reloadedText->GetText() == wxT( "auto bold" ) )
4175 BOOST_CHECK_EQUAL( reloadedText->GetTextThickness(), 0 );
4176 else if( reloadedText->GetText() != wxT( "outline bold" ) || outlineAvailable )
4177 BOOST_CHECK_EQUAL( reloadedText->GetTextThickness(), 100000 );
4178
4179 ++checked;
4180 },
4182 BOOST_CHECK_EQUAL( checked, bold.size() + 2 );
4183 }
4184}
4185
4186
4187BOOST_AUTO_TEST_CASE( ComplexPadstackFrontAndInnerCustomOptionsRoundTripToKicad10 )
4188{
4189 SETTINGS_MANAGER settingsManager;
4190 BOARD board;
4191 board.SetCopperLayerCount( 4 );
4192 FOOTPRINT* fp = new FOOTPRINT( &board );
4193 PAD* pad = new PAD( fp );
4194 pad->SetAttribute( PAD_ATTRIB::PTH );
4195 pad->SetLayerSet( LSET::AllCuMask() );
4196 pad->Padstack().SetMode( PADSTACK::MODE::CUSTOM );
4197 pad->SetShape( F_Cu, PAD_SHAPE::OVAL );
4198 pad->SetSize( F_Cu, VECTOR2I( 2000000, 1000000 ) );
4199 pad->SetShape( In1_Cu, PAD_SHAPE::CUSTOM );
4200 pad->SetSize( In1_Cu, VECTOR2I( 500000, 500000 ) );
4201 pad->SetCustomShapeInZoneOpt( CUSTOM_SHAPE_ZONE_MODE::CONVEXHULL );
4202 PCB_SHAPE* primitive = new PCB_SHAPE( pad, SHAPE_T::SEGMENT );
4203 primitive->SetStart( VECTOR2I( -1000000, 0 ) );
4204 primitive->SetEnd( VECTOR2I( 1000000, 0 ) );
4205 primitive->SetWidth( 200000 );
4206 pad->AddPrimitive( In1_Cu, primitive );
4207 pad->SetDrillSize( VECTOR2I( 300000, 300000 ) );
4208 fp->Add( pad );
4209 board.Add( fp );
4210 KI_TEST::SCOPED_TEMP_DIR temporary( "kicad_qa_complex_pad_front" );
4211 const std::string path = ( temporary.Path() / "out.kicad_pcb" ).string();
4212 SaveBoardForTarget( &board, path, kicad10 );
4214 auto reloaded = io.LoadBoard( path );
4215 BOOST_REQUIRE( reloaded && reloaded->Footprints().size() == 1 );
4216 BOOST_REQUIRE_EQUAL( reloaded->Footprints()[0]->Pads().size(), 1 );
4217 const PAD* result = reloaded->Footprints()[0]->Pads()[0];
4218 BOOST_CHECK( result->GetShape( F_Cu ) == PAD_SHAPE::OVAL );
4219 BOOST_CHECK( result->GetSize( F_Cu ) == VECTOR2I( 2000000, 1000000 ) );
4220 BOOST_CHECK( result->GetShape( In1_Cu ) == PAD_SHAPE::CUSTOM );
4221 BOOST_REQUIRE_EQUAL( result->GetPrimitives( In1_Cu ).size(), 1 );
4222 BOOST_CHECK( result->GetCustomShapeInZoneOpt() == CUSTOM_SHAPE_ZONE_MODE::CONVEXHULL );
4223}
4224
4225
4226BOOST_AUTO_TEST_CASE( CurrentProjectSchemasAndSupportedDrcExclusionsMigrateForBothTargets )
4227{
4228 const nlohmann::json exclusion = { { "marker",
4229 { { "error_type", "DRCET_CLEARANCE" },
4230 { "position", { { "x_nm", "123" }, { "y_nm", "-456" } } },
4231 { "items",
4232 { { { "value", "11111111-1111-1111-1111-111111111111" } },
4233 { { "value", "22222222-2222-2222-2222-222222222222" } } } } } },
4234 { "comment", "accepted" } };
4235
4236 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
4237 {
4238 nlohmann::json doc = {
4239 { "meta", { { "version", 4 } } },
4240 { "board",
4241 { { "design_settings", { { "meta", { { "version", 3 } } }, { "drc_exclusions", { exclusion } } } } } }
4242 };
4243 COMPATIBILITY_REPORT report;
4244 DowngradeProjectFileJson( doc, target, report );
4245 BOOST_CHECK_EQUAL( doc["meta"]["version"].get<int>(), 3 );
4246 const auto& settings = doc["board"]["design_settings"];
4247 BOOST_CHECK_EQUAL( settings["meta"]["version"].get<int>(), 2 );
4248 BOOST_REQUIRE_EQUAL( settings["drc_exclusions"].size(), 1 );
4250 settings["drc_exclusions"][0][0].get<std::string>(),
4251 "clearance|123|-456|11111111-1111-1111-1111-111111111111|22222222-2222-2222-2222-222222222222" );
4252 BOOST_CHECK_EQUAL( settings["drc_exclusions"][0][1].get<std::string>(), "accepted" );
4253 BOOST_CHECK( !report.IsLossy() );
4254 const auto migrated = doc;
4255 COMPATIBILITY_REPORT second;
4256 DowngradeProjectFileJson( doc, target, second );
4257 BOOST_CHECK( doc == migrated );
4258 BOOST_CHECK( !second.IsLossy() );
4259 }
4260}
4261
4262
4263BOOST_AUTO_TEST_CASE( UnrepresentableDrcExclusionsDropWithReports )
4264{
4265 nlohmann::json multiple = { { "marker",
4266 { { "error_type", "DRCET_CLEARANCE" },
4267 { "position", { { "x_nm", "123" }, { "y_nm", "-456" } } },
4268 { "items",
4269 { { { "value", "11111111-1111-1111-1111-111111111111" } },
4270 { { "value", "22222222-2222-2222-2222-222222222222" } },
4271 { { "value", "33333333-3333-3333-3333-333333333333" } } } } } },
4272 { "comment", "three items" } };
4273 nlohmann::json unknown = multiple;
4274 unknown["marker"]["error_type"] = "DRCET_MICROVIA_STACK_DEPTH";
4275 unknown["marker"]["items"].erase( 2 );
4276
4277 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
4278 {
4279 nlohmann::json doc = {
4280 { "board",
4281 { { "design_settings",
4282 { { "meta", { { "version", 3 } } }, { "drc_exclusions", { multiple, unknown, "malformed" } } } } } }
4283 };
4284 COMPATIBILITY_REPORT report;
4285 DowngradeProjectFileJson( doc, target, report );
4286 BOOST_CHECK( doc["board"]["design_settings"]["drc_exclusions"].empty() );
4288 }
4289}
4290
4291
4292BOOST_AUTO_TEST_CASE( PostMasterProjectSettingsReportOnlyMeaningfulLosses )
4293{
4294 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
4295 {
4296 nlohmann::json doc;
4297 doc["net_settings"]["net_chain_netclasses"] = { { { "name", "fast" } } };
4298 doc["board"]["design_settings"]["via_stack_presets"] = { { { "name", "fanout" } } };
4299 doc["board"]["design_settings"]["teardrop_parameters"] = {
4300 { { "td_target_name", "td_track_end" }, { "td_enabled", true }, { "td_max_len", 1.0 } },
4301 { { "td_enabled", false } }
4302 };
4303 COMPATIBILITY_REPORT report;
4304 DowngradeProjectFileJson( doc, target, report );
4305 BOOST_CHECK( !doc["net_settings"].contains( "net_chain_netclasses" ) );
4306 const auto& settings = doc["board"]["design_settings"];
4307 BOOST_CHECK( !settings.contains( "via_stack_presets" ) );
4308 BOOST_CHECK( !settings["teardrop_parameters"][0].contains( "td_enabled" ) );
4309 BOOST_CHECK( !settings["teardrop_parameters"][1].contains( "td_enabled" ) );
4310 BOOST_CHECK_EQUAL( settings["teardrop_parameters"][0]["td_max_len"].get<double>(), 1.0 );
4312 COMPATIBILITY_REPORT second;
4313 DowngradeProjectFileJson( doc, target, second );
4314 BOOST_CHECK( !second.IsLossy() );
4315
4316 nlohmann::json defaults;
4317 defaults["net_settings"]["net_chain_netclasses"] = nlohmann::json::array();
4318 defaults["board"]["design_settings"]["via_stack_presets"] = nlohmann::json::array();
4319 defaults["board"]["design_settings"]["teardrop_parameters"] = { { { "td_enabled", false } } };
4320 COMPATIBILITY_REPORT silent;
4321 DowngradeProjectFileJson( defaults, target, silent );
4322 BOOST_CHECK( !silent.IsLossy() );
4323 }
4324}
4325
4326
4327BOOST_AUTO_TEST_CASE( NewTuningBridgeDelayIsRemovedAndReportedForKicad10 )
4328{
4329 nlohmann::json doc = {
4330 { "tuning_profiles",
4331 { { "meta", { { "version", 2 } } },
4332 { "tuning_profiles_impedance_geometric",
4333 { { { "net_chain_bridge_prop_delay", 25 } }, { { "net_chain_bridge_prop_delay", 0 } } } } } }
4334 };
4335 COMPATIBILITY_REPORT report;
4336 DowngradeProjectFileJson( doc, kicad10, report );
4337 BOOST_CHECK_EQUAL( doc["tuning_profiles"]["meta"]["version"].get<int>(), 0 );
4339
4340 for( const auto& profile : doc["tuning_profiles"]["tuning_profiles_impedance_geometric"] )
4341 BOOST_CHECK( !profile.contains( "net_chain_bridge_prop_delay" ) );
4342}
4343
4344
4345BOOST_AUTO_TEST_CASE( NativeNetChainClassMapReportsOnlyNonemptyMappings )
4346{
4347 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
4348 {
4349 nlohmann::json doc;
4350 doc["net_settings"]["net_chain_classes"] = { { "clock", "Fast" } };
4351 COMPATIBILITY_REPORT report;
4352 DowngradeProjectFileJson( doc, target, report );
4353 BOOST_CHECK( !doc["net_settings"].contains( "net_chain_classes" ) );
4355 COMPATIBILITY_REPORT second;
4356 DowngradeProjectFileJson( doc, target, second );
4357 BOOST_CHECK( !second.IsLossy() );
4358 doc["net_settings"]["net_chain_classes"] = nlohmann::json::object();
4360 DowngradeProjectFileJson( doc, target, empty );
4361 BOOST_CHECK( !empty.IsLossy() );
4362 }
4363}
4364
4365
4366// A word the generated vocabulary lists must never be the reason a rule is dropped. That is the
4367// direction a generator mistake would take, silently losing a rule the target can read.
4368BOOST_AUTO_TEST_CASE( ReleaseVocabularyKeepsEveryRuleTheReleaseCouldRead )
4369{
4370 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
4371 {
4372 const DRC_RULE_VOCABULARY* vocabulary = FindDrcRuleVocabulary( target );
4373 BOOST_REQUIRE( vocabulary );
4374
4375 for( size_t i = 0; i < vocabulary->m_functionCount; i++ )
4376 {
4377 const wxString name = wxString::FromUTF8( vocabulary->m_functions[i] );
4378
4379 if( std::any_of( std::begin( DRC_RULE_SEMANTIC_BREAKS ), std::end( DRC_RULE_SEMANTIC_BREAKS ),
4380 [&]( const char* aBreak )
4381 {
4382 return name == aBreak;
4383 } ) )
4384 continue;
4385
4386 const wxString rule = wxT( "(version 1)(rule \"f\" (condition \"A." ) + name
4387 + wxT( "('x')\") (constraint clearance (min 0.2mm)))" );
4388
4389 BOOST_CHECK_MESSAGE( FilterDrcRulesForTarget( rule, target ).m_dropped.empty(),
4390 target.m_id + " dropped a rule calling " + name );
4391 }
4392
4393 for( size_t i = 0; i < vocabulary->m_propertyCount; i++ )
4394 {
4395 const wxString name = wxString::FromUTF8( vocabulary->m_properties[i] );
4396 const wxString rule = wxT( "(version 1)(rule \"p\" (condition \"A." ) + name
4397 + wxT( " == 1\") (constraint clearance (min 0.2mm)))" );
4398
4399 BOOST_CHECK_MESSAGE( FilterDrcRulesForTarget( rule, target ).m_dropped.empty(),
4400 target.m_id + " dropped a rule reading " + name );
4401 }
4402 }
4403}
4404
4405
4406// Adding a downgrade target without rerunning tools/generate_drc_rules_vocabulary.py must fail here.
4407BOOST_AUTO_TEST_CASE( EveryDowngradeTargetHasARuleVocabulary )
4408{
4409 for( const DOWNGRADE_TARGET& target : GetDowngradeTargets() )
4410 {
4411 BOOST_CHECK_MESSAGE( FindDrcRuleVocabulary( target ),
4412 target.m_name + " has no entry in include/drc_rules_release_vocabulary.h" );
4413 }
4414}
4415
4416
4417// A registered property is only usable if the release's evaluator can read its type. Priority
4418// is unsigned and Hatch Orientation an angle, neither readable before 11, while an enum always is.
4419BOOST_AUTO_TEST_CASE( PropertiesTheReleaseCannotEvaluateAreDropped )
4420{
4421 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
4422 {
4423 for( const wxString& property : { wxT( "A.Priority > 1" ), wxT( "A.Hatch_Orientation == 0" ),
4424 wxT( "A.Line_Color == 'red'" ) } )
4425 {
4426 const auto result = FilterDrcRulesForTarget( wxT( "(version 1)(rule \"typed\" (condition \"" ) + property
4427 + wxT( "\") (constraint clearance (min 0.2mm)))" ),
4428 target );
4429 BOOST_CHECK_MESSAGE( result.m_dropped.size() == 1, target.m_id + " kept " + property );
4430 }
4431
4432 const auto kept = FilterDrcRulesForTarget( wxT( "(version 1)(rule \"enum\" (condition \"A.Pad_Type == 'PTH'\") "
4433 "(constraint clearance (min 0.2mm)))" ),
4434 target );
4435 BOOST_CHECK( kept.m_dropped.empty() );
4436 }
4437}
4438
4439
4440// A unit may stand apart from its number. That is a value, not a keyword the target lacks.
4441BOOST_AUTO_TEST_CASE( SpacedUnitsAreNotMistakenForKeywords )
4442{
4443 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
4444 {
4445 const auto kept = FilterDrcRulesForTarget(
4446 wxT( "(version 1)(rule \"spaced\" (constraint clearance (min 0.2 mm) (opt 0.3 mil) (max 1 in)))" ),
4447 target );
4448 BOOST_CHECK_MESSAGE( kept.m_dropped.empty(), target.m_id + " dropped a rule with a spaced unit" );
4449
4450 // A unit the target cannot read is still caught when it stands apart.
4451 const auto dropped = FilterDrcRulesForTarget(
4452 wxT( "(version 1)(rule \"delay\" (constraint skew (max 5 ps)))" ), target );
4453 BOOST_CHECK_EQUAL( dropped.m_dropped.size(), target.m_id == wxT( "9.0" ) ? 1 : 0 );
4454 }
4455}
4456
4457
4458// 9.0 reads neither degrees nor picoseconds, and knows nothing of text variables. 10.0 has all three.
4459BOOST_AUTO_TEST_CASE( UnitsAndTextVariablesFollowWhatTheReleaseParses )
4460{
4461 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
4462 {
4463 const bool old = target.m_id == wxT( "9.0" );
4464
4465 auto angle = FilterDrcRulesForTarget(
4466 wxT( "(version 1)(rule \"a\" (constraint track_angle (max 45deg)))" ), target );
4467 BOOST_CHECK_EQUAL( angle.m_dropped.size(), old ? 1 : 0 );
4468
4469 auto delay = FilterDrcRulesForTarget( wxT( "(version 1)(rule \"d\" (constraint skew (max 5ps)))" ), target );
4470 BOOST_CHECK_EQUAL( delay.m_dropped.size(), old ? 1 : 0 );
4471
4472 for( const wxString& rule : { wxT( "(rule \"v\" (constraint clearance (min ${GAP})))" ),
4473 wxT( "(rule \"c\" (condition \"A.NetName == '${NET}'\") "
4474 "(constraint clearance (min 0.2mm)))" ) } )
4475 {
4476 auto variable = FilterDrcRulesForTarget( wxT( "(version 1)" ) + rule, target );
4477 BOOST_CHECK_EQUAL( variable.m_dropped.size(), old ? 1 : 0 );
4478 }
4479 }
4480}
4481
4482
4483BOOST_AUTO_TEST_CASE( WordsAddedInAPointReleaseAreNotInTheSeriesVocabulary )
4484{
4485 for( const wxString& expression : { wxT( "A.hasExactNetclass('x')" ), wxT( "A.Column_Width == 1" ),
4486 wxT( "A.Visible == 1" ) } )
4487 {
4488 const wxString rule = wxT( "(version 1)(rule \"r\" (condition \"" ) + expression
4489 + wxT( "\") (constraint clearance (min 0.2mm)))" );
4490 BOOST_CHECK_MESSAGE( FilterDrcRulesForTarget( rule, kicad9 ).m_dropped.size() == 1,
4491 "9.0 kept " + expression + " which not every 9.0.x release reads" );
4492 }
4493
4494 for( const wxString& expression : { wxT( "A.intersectsKeepout('x')" ), wxT( "A.Origin_X == 1" ) } )
4495 {
4496 const wxString rule = wxT( "(version 1)(rule \"r\" (condition \"" ) + expression
4497 + wxT( "\") (constraint clearance (min 0.2mm)))" );
4498 BOOST_CHECK_MESSAGE( FilterDrcRulesForTarget( rule, kicad10 ).m_dropped.size() == 1,
4499 "10.0 kept " + expression + " which not every 10.0.x release reads" );
4500 }
4501}
4502
4503
4504// A layer may be given unquoted, and a user layer name is not a grammar keyword.
4505BOOST_AUTO_TEST_CASE( UnquotedLayerNamesAreNotMistakenForKeywords )
4506{
4507 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
4508 {
4509 for( const wxString& layer : { wxT( "MyCustomLayer" ), wxT( "F.Cu" ), wxT( "\"In1.Cu\"" ), wxT( "outer" ) } )
4510 {
4511 const auto result = FilterDrcRulesForTarget( wxT( "(version 1)(rule \"on layer\" (layer " ) + layer
4512 + wxT( ") (constraint clearance (min 0.2mm)))" ),
4513 target );
4514 BOOST_CHECK_MESSAGE( result.m_dropped.empty(), target.m_id + " dropped a rule on layer " + layer );
4515 }
4516 }
4517}
4518
4519
4520BOOST_AUTO_TEST_CASE( MissingRuleVocabularyFailsClosed )
4521{
4523 unknown.m_id = wxT( "0.0" );
4524 unknown.m_name = wxT( "KiCad 0.0" );
4525
4526 const auto result =
4527 FilterDrcRulesForTarget( wxT( "(version 1)(rule \"r\" (constraint clearance (min 0.2mm)))" ), unknown );
4528
4529 BOOST_CHECK( !result.m_error.IsEmpty() );
4530 BOOST_CHECK( result.m_text.IsEmpty() );
4531}
4532
4533
4534BOOST_AUTO_TEST_CASE( NewDrcConstraintsAndFunctionsAreFilteredWithoutLiteralFalsePositives )
4535{
4536 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
4537 {
4538 for( const wxString& keyword : { wxT( "hole_aspect_ratio" ), wxT( "microvia_stack_depth" ) } )
4539 {
4540 const wxString rule = "(version 1)(rule \"new\" (constraint " + keyword + " (max 2)))";
4541 const auto result = FilterDrcRulesForTarget( rule, target );
4542 BOOST_CHECK( result.m_error.IsEmpty() );
4543 BOOST_CHECK_EQUAL( result.m_dropped.size(), 1 );
4544 }
4545
4546 for( const wxString& call :
4547 { wxT( "A.intersectsKeepout('x')" ), wxT( "A.intersectsArea('x')" ), wxT( "A.isStackedVia()" ),
4548 wxT( "A.ISSTACKEDVIA()" ), wxT( "A.customProperty('Owner') == 'Team'" ),
4549 wxT( "A.hasCustomProperty('Owner')" ) } )
4550 {
4551 for( const wxString& expression : { wxT( "condition" ), wxT( "assertion" ) } )
4552 {
4553 const wxString rule =
4554 expression == wxT( "condition" )
4555 ? "(rule \"new\" (condition \"" + call + "\") (constraint clearance (min 0.2mm)))"
4556 : "(rule \"new\" (constraint assertion \"" + call + "\"))";
4557 const auto result = FilterDrcRulesForTarget( wxT( "(version 1)" ) + rule, target );
4558 BOOST_CHECK( result.m_error.IsEmpty() );
4559 BOOST_CHECK_EQUAL( result.m_dropped.size(), 1 );
4560 }
4561 }
4562
4563 const wxString kept = wxT( "(rule \"customProperty\" (condition \"A.NetName == 'isStackedVia()'\") "
4564 "(constraint clearance (min 0.2mm)))\n"
4565 "(rule \"literal\" (condition \"A.NetName == 'intersectsArea()'\") "
4566 "(constraint clearance (min 0.2mm)))\n"
4567 "(rule \"uppercase literal\" (condition \"A.NetName == 'ISSTACKEDVIA()'\") "
4568 "(constraint clearance (min 0.2mm)))\n"
4569 "(rule \"legacy\" (condition \"A.insideArea('x')\") "
4570 "(constraint clearance (min 0.2mm)))\n"
4571 "(rule \"position\" (condition \"A.Start_X > 1mm && A.Start_Y > 1mm\") "
4572 "(constraint clearance (min 0.2mm)))" );
4573 const auto result = FilterDrcRulesForTarget( wxT( "(version 1)\n" ) + kept, target );
4574 BOOST_CHECK( result.m_error.IsEmpty() );
4575 BOOST_CHECK( result.m_dropped.empty() );
4576 BOOST_CHECK( result.m_text.Contains( kept ) );
4577 }
4578}
4579
4580
4581BOOST_AUTO_TEST_CASE( DrcPostTargetPropertiesAreGuardedInConditionsAndAssertions )
4582{
4583 for( const DOWNGRADE_TARGET& target : { kicad9, kicad10 } )
4584 {
4585 for( const wxString& kind : { wxT( "condition" ), wxT( "assertion" ) } )
4586 {
4587 const wxString expression = wxT( "A.Parent == A.Parent && A.NetName != 'A.parent.Reference'" );
4588 const wxString body =
4589 kind == wxT( "condition" )
4590 ? wxT( "(condition \"" ) + expression + wxT( "\") (constraint clearance (min 0.2mm))" )
4591 : wxT( "(constraint assertion \"" ) + expression + wxT( "\")" );
4592 const auto parent =
4593 FilterDrcRulesForTarget( wxT( "(version 1)(rule \"parent property\" " ) + body + ")", target );
4594 BOOST_CHECK( parent.m_error.IsEmpty() );
4595 BOOST_CHECK( parent.m_dropped.empty() );
4596 }
4597
4598 for( const wxString& property :
4599 { wxT( "A.Start_Shape" ), wxT( "A.start_shape" ), wxT( "B.End_Width" ), wxT( "AB.Scale_X" ),
4600 wxT( "A.parent.Reference" ), wxT( "A . PARENT . Reference" ), wxT( "A.Major_Radius" ),
4601 wxT( "A.Exclude_From_Simulation" ), wxT( "A.Automatically_Update_Net" ), wxT( "A.Grid_Type" ) } )
4602 {
4603 for( const wxString& kind : { wxT( "condition" ), wxT( "assertion" ) } )
4604 {
4605 const wxString expression =
4606 property + ( property.Contains( wxT( "Reference" ) ) ? wxT( " == 'U1'" ) : wxT( " == 1" ) );
4607 const wxString body =
4608 kind == wxT( "condition" )
4609 ? wxT( "(condition \"" ) + expression + wxT( "\") (constraint clearance (min 0.2mm))" )
4610 : wxT( "(constraint assertion \"" ) + expression + wxT( "\")" );
4611 const auto result =
4612 FilterDrcRulesForTarget( wxT( "(version 1)(rule \"props\" " ) + body + ")", target );
4613 BOOST_CHECK( result.m_error.IsEmpty() );
4614 BOOST_CHECK_EQUAL( result.m_dropped.size(), 1 );
4615 }
4616 }
4617
4618 for( const wxString& property :
4619 { wxT( "A.Corner_Radius" ), wxT( "A.Auto_Thickness" ), wxT( "A.Target_Delay" ) } )
4620 {
4621 for( const wxString& kind : { wxT( "condition" ), wxT( "assertion" ) } )
4622 {
4623 const wxString expression = property + wxT( " == 1" );
4624 const wxString body =
4625 kind == wxT( "condition" )
4626 ? wxT( "(condition \"" ) + expression + wxT( "\") (constraint clearance (min 0.2mm))" )
4627 : wxT( "(constraint assertion \"" ) + expression + wxT( "\")" );
4628 const auto result =
4629 FilterDrcRulesForTarget( wxT( "(version 1)(rule \"older\" " ) + body + ")", target );
4630 BOOST_CHECK( result.m_error.IsEmpty() );
4631 BOOST_CHECK_EQUAL( result.m_dropped.size(), target.m_boardVersion == kicad9.m_boardVersion ? 1 : 0 );
4632 }
4633 }
4634
4635 const wxString kept = wxT( "(rule \"literal\" (condition \"A.NetName == 'A.Start_Shape' && A.Start_X > 1mm "
4636 "&& A.End_Y > 1mm && A.Radius > 1mm\") (constraint clearance (min 0.2mm)))" );
4637 const auto result = FilterDrcRulesForTarget( wxT( "(version 1)" ) + kept, target );
4638 BOOST_CHECK( result.m_error.IsEmpty() );
4639 BOOST_CHECK( result.m_dropped.empty() );
4640 BOOST_CHECK( result.m_text.Contains( kept ) );
4641 }
4642}
4643
4644
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:85
virtual bool IsKnockout() const
Definition board_item.h:423
virtual PCB_LAYER_ID GetLayer() const
Return the primary layer this item is on.
virtual void SetIsKnockout(bool aKnockout)
Definition board_item.h:424
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:411
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Removes an item from the container.
Definition board.cpp:1520
const GENERATORS & Generators() const
Definition board.h:485
void SetProperties(const std::map< wxString, wxString > &aProps)
Definition board.h:527
NETINFO_ITEM * FindNet(int aNetcode) const
Search for a net with the given netcode.
Definition board.cpp:2797
TITLE_BLOCK & GetTitleBlock()
Definition board.h:1026
const TRACKS & Tracks() const
Definition board.h:463
const CONSTRAINTS & Constraints() const
Geometric constraints (#2329) owned by this board.
Definition board.h:522
void SetCopperLayerCount(int aCount)
Definition board.cpp:1133
void SetProject(PROJECT *aProject, bool aReferenceOnly=false)
Link a board to a given project.
Definition board.cpp:379
wxString GroupsSanityCheck(bool repair=false)
Consistency check of internal m_groups structure.
Definition board.cpp:3918
void SetEnabledLayers(const LSET &aLayerMask)
A proxy function that calls the correspondent function in m_BoardSettings.
Definition board.cpp:1194
BOARD_DESIGN_SETTINGS & GetDesignSettings()
Definition board.cpp:1290
const DRAWINGS & Drawings() const
Definition board.h:467
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:189
FILL_T GetFillMode() const
Definition eda_shape.h:150
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:195
SHAPE_T GetShape() const
Definition eda_shape.h:177
LINE_STYLE GetLineStyle() const
LINE_ENDING_STYLE GetEndEndingStyle() const
Definition eda_shape.h:194
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:178
void SetExcludedFromSim(bool aExclude)
Definition footprint.h:199
void SetDNP(bool aDNP)
Definition footprint.h:193
void SetFieldValue(const wxString &aFieldName, const wxString &aValue)
Set a field value override for this variant.
Definition footprint.h:224
bool GetDuplicatePadNumbersAreJumpers() const
Definition footprint.h:1133
const CASE_INSENSITIVE_MAP< FOOTPRINT_VARIANT > & GetVariants() const
Get all variants.
Definition footprint.h:1011
bool IsDNP() const
Definition footprint.h:947
EDA_ANGLE GetOrientation() const
Definition footprint.h:382
PCB_3D_BODY & EnsureBody()
void SetDNP(bool aDNP=true)
Definition footprint.h:948
void SetStackupMode(FOOTPRINT_STACKUP aMode)
Set the stackup mode for this footprint.
void SetExcludedFromSim(bool aExclude=true)
Definition footprint.h:939
void SetKeywords(const wxString &aKeywords)
Definition footprint.h:453
PCB_FIELD & Value()
read/write accessors:
Definition footprint.h:831
void SetDuplicatePadNumbersAreJumpers(bool aEnabled)
Definition footprint.h:1134
CONSTRAINTS & Constraints()
Definition footprint.h:367
PCB_FIELD * GetField(FIELD_T aFieldType)
Return a mandatory field in this footprint.
std::deque< PAD * > & Pads()
Definition footprint.h:355
void SetReference(const wxString &aReference)
Definition footprint.h:799
bool IsExcludedFromSim() const
Definition footprint.h:938
void SetTransformScale(double aScaleX, double aScaleY)
PCB_FIELD & Reference()
Definition footprint.h:832
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.
bool HasBody() const
Convenience accessors for now while we restrict to only one body per footprint.
Definition footprint.h:426
const wxString & GetReference() const
Definition footprint.h:793
VECTOR2I GetPosition() const override
Definition footprint.h:379
DRAWINGS & GraphicalItems()
Definition footprint.h:358
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:144
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
const std::filesystem::path & Path() const
Get the path to the temporary directory as a std::filesystem::path.
Definition file_utils.h:61
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:158
MASK_LAYER_PROPS & FrontOuterLayers()
Definition padstack.h:407
void SetUnconnectedLayerMode(UNCONNECTED_LAYER_MODE aMode)
Definition padstack.h:402
UNCONNECTED_LAYER_MODE UnconnectedLayerMode() const
Definition padstack.h:401
DRILL_PROPS & Drill()
Definition padstack.h:360
@ CUSTOM
Shapes can be defined on arbitrary layers.
Definition padstack.h:174
MASK_LAYER_PROPS & BackOuterLayers()
Definition padstack.h:410
Definition pad.h:61
LSET GetLayerSet() const override
Return a std::bitset of all layers on which the item physically resides.
Definition pad.h:575
VECTOR2I GetPosition() const override
Definition pad.cpp:255
double GetRoundRectRadiusRatio(PCB_LAYER_ID aLayer) const
Definition pad.h:833
PAD_SHAPE GetShape(PCB_LAYER_ID aLayer) const
Definition pad.h:205
VECTOR2I GetSize(PCB_LAYER_ID aLayer) const
Definition pad.cpp:297
EDA_ANGLE GetOrientation() const
Return the rotation angle of the pad.
Definition pad.cpp:1658
const VECTOR2I & GetMid() const
Definition pcb_track.h:300
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:70
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:101
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.
EDA_ITEM * Clone() const override
Create a duplicate of this item with linked list members set to NULL.
Definition pcb_track.cpp:73
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:431
void SetViaType(VIATYPE aViaType)
Definition pcb_track.h:423
VIATYPE GetViaType() const
Definition pcb_track.h:422
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:850
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:1496
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:852
PLACEMENT_SOURCE_T GetPlacementAreaSourceType() const
Definition zone.h:839
bool GetDoNotAllowTracks() const
Definition zone.h:851
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:675
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:1746
bool IsCopperThieving() const
Definition zone.h:349
const wxString & GetZoneName() const
Definition zone.h:160
SHAPE_POLY_SET GetBoardOutline() const
Definition zone.cpp:951
void SetFillMode(ZONE_FILL_MODE aFillMode)
Definition zone.cpp:681
void SetLayerSet(const LSET &aLayerSet) override
Definition zone.cpp:700
bool GetPlacementAreaEnabled() const
Definition zone.h:834
bool GetDoNotAllowFootprints() const
Definition zone.h:853
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:849
@ FOR_CANVAS
Definition common.h:92
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.
const std::vector< DOWNGRADE_TARGET > & GetDowngradeTargets()
Supported downgrade targets, newest first.
@ TRACK_WIDTH_CONSTRAINT
@ CLEARANCE_CONSTRAINT
constexpr const char * DRC_RULE_SEMANTIC_BREAKS[]
A function a release parses but answers differently, which the vocabulary cannot say.
const DRC_RULE_VOCABULARY * FindDrcRuleVocabulary(const DOWNGRADE_TARGET &aTarget)
The vocabulary a target release could parse, generated from its release tags by tools/generate_drc_ru...
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:472
static constexpr EDA_ANGLE ANGLE_90
Definition eda_angle.h:474
@ DEGREES_T
Definition eda_angle.h:30
static constexpr EDA_ANGLE ANGLE_360
Definition eda_angle.h:478
@ 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:116
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:461
@ PTH
Plated through hole pad.
Definition padstack.h:99
@ PRESSFIT
a PTH with a hole diameter with tight tolerances for press fit pin
Definition padstack.h:124
@ NONE
no special fabrication property
Definition padstack.h:116
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.
Everything a rules file could name in one release.
std::optional< bool > is_capped
True if the drill hole should be capped.
Definition padstack.h:281
std::optional< bool > is_filled
True if the drill hole should be filled completely.
Definition padstack.h:280
std::optional< bool > has_covering
True if the pad on this side should have covering.
Definition padstack.h:263
std::optional< bool > has_plugging
True if the drill hole should be plugged on this side.
Definition padstack.h:264
std::optional< VECTOR2I > hatching_offset
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_CASE(EmptyBoardIsClean)
static size_t ParsedDrcRuleCount(const wxString &aText, const wxString &aSource)
static void checkFeature(const COMPATIBILITY_REPORT &aReport, const wxString &aName, DOWNGRADE_BUCKET aBucket, int aCount)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_CHECK(res.column_datatype(0)==SQL_NUMERIC||res.column_datatype(0)==SQL_DECIMAL)
const std::string table
BOOST_REQUIRE_EQUAL(rows.size(), 1)
BOOST_CHECK_EQUAL(std::any_cast< std::string >(rows[0].at("val")), "12345678901234567.89")
BOOST_REQUIRE(res.columns()==1)
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.
#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.