KiCad PCB EDA Suite
Loading...
Searching...
No Matches
group_saveload.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 (C) 2020 Joshua Redstone redstone at gmail.com
5 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
6 *
7 * This program is free software; you can redistribute it and/or
8 * modify it under the terms of the GNU General Public License
9 * as published by the Free Software Foundation; either version 2
10 * of the License, or (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
19 */
20
21#include <bitset>
22#include <filesystem>
23#include <set>
24#include <string>
25#include <vector>
26
27#include <board.h>
28#include <footprint.h>
29#include <lset.h>
30#include <pcb_generator.h>
31#include <pcb_group.h>
32#include <pcb_text.h>
33#include <pcb_track.h>
35#include <common.h>
39
40BOOST_AUTO_TEST_SUITE( GroupSaveLoad )
41
42// The tests below set up a test case with a spec for the set of groups to create.
43// A group can contain members from this list of candidates.
45{
55 REMOVED_TEXT, // Text not added to board
61 NAME_GROUP3_DUP, // Group with name identical to NAME_GROUP3
62 REMOVED_GROUP, // Group not added to board
64};
65
66// The objects associated with item REMOVED_TEXT and REMOVED_GROUP are not added to the board,
67// so they are not cleaned up when the board is deleted. These pointers stores the objects
68// so they can be deleted once they are done being used.
69static PCB_TEXT* s_removedText = nullptr;
70static PCB_GROUP* s_removedGroup = nullptr;
71
72
73/*
74 * Takes a vector of group specifications for groups to create.
75 * Each group is a vector of which ItemTypes to put in the group.
76 * The first group corresponds to GROUP0, the second to GROUP1, and os on.
77 */
78std::unique_ptr<BOARD> createBoard( const std::vector<std::vector<ItemType>>& spec, bool aAllowInvalidGroups = false )
79{
80 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
81 std::vector<BOARD_ITEM*> items;
82
83 // Create text items and add to board.
84 for( int idx = 0; idx <= REMOVED_TEXT; idx++ )
85 {
86 PCB_TEXT* textItem = new PCB_TEXT( board.get() );
87 textItem->SetText( wxString::Format( _( "some text-%d" ), idx ) );
88
89 // Don't add REMOVED_TEXT to the board
90 if( idx < REMOVED_TEXT )
91 board->Add( textItem );
92
93 items.push_back( textItem );
94 }
95
96 // Create groups
97 for( int idx = 0; idx < ( NUM_ITEMS - GROUP0 ); idx++ )
98 {
99 PCB_GROUP* gr = new PCB_GROUP( board.get() );
100
101 if( idx >= ( NAME_GROUP3 - GROUP0 ) )
102 {
103 wxString name = wxString::Format( _( "group-%d" ),
104 ( idx == ( NAME_GROUP3_DUP - GROUP0 ) ) ? 3 : idx );
105 gr->SetName( name );
107 }
108
109 items.push_back( gr );
110 }
111
112 std::bitset<NUM_ITEMS> used;
113
114 // Populate groups based on spec
115 for( int offset = 0; offset < ( NUM_ITEMS - GROUP0 ); offset++ )
116 {
117 int groupIdx = GROUP0 + offset;
118
119 PCB_GROUP* group = static_cast<PCB_GROUP*>( items[groupIdx] );
120
121 if( offset < spec.size() )
122 {
123 const std::vector<ItemType>& groupSpec = spec[offset];
124
125 for( ItemType item : groupSpec )
126 {
127 used.set( static_cast<size_t>( item ) );
128
129 if( aAllowInvalidGroups )
130 {
131 // The invalid-group tests intentionally build graphs that AddItem()
132 // rejects so GroupsSanityCheck() can verify diagnostics.
133 group->GetItems().insert( items[item] );
134 items[item]->SetParentGroup( group );
135 }
136 else
137 {
138 group->AddItem( items[item] );
139 }
140 }
141
142 BOOST_CHECK_EQUAL( group->GetItems().size(), groupSpec.size() );
143 board->Add( group );
144 }
145 else if( groupIdx != REMOVED_GROUP && used.test( groupIdx ) )
146 {
147 // This group is used in another group, so it must be on the board
148 board->Add( group );
149 }
150 else if( groupIdx != REMOVED_GROUP )
151 {
152 // If the group isn't used, delete it
153 delete group;
154 }
155 }
156
157 // Delete the removed text item if it isn't used
158 if( used.test( REMOVED_TEXT ) )
159 s_removedText = static_cast<PCB_TEXT*>( items[REMOVED_TEXT] );
160 else
161 delete items[REMOVED_TEXT];
162
163 // Delete the removed group item if it isn't used
164 if( used.test( REMOVED_GROUP ) )
165 s_removedGroup = static_cast<PCB_GROUP*>( items[REMOVED_GROUP] );
166 else
167 delete items[REMOVED_GROUP];
168
169 BOOST_TEST_CHECKPOINT( "Returning fresh board" );
170 return board;
171}
172
173
174// Check if two groups are identical by comparing the fields (by Uuid).
175void testGroupEqual( const PCB_GROUP& group1, const PCB_GROUP& group2 )
176{
177 BOOST_CHECK_EQUAL( group1.m_Uuid.AsString(), group2.m_Uuid.AsString() );
178 BOOST_CHECK_EQUAL( group1.GetName(), group2.GetName() );
179
180 const std::unordered_set<EDA_ITEM*>& items1 = group1.GetItems();
181 const std::unordered_set<EDA_ITEM*>& items2 = group2.GetItems();
182
183 BOOST_CHECK_EQUAL( items1.size(), items2.size() );
184
185 // Test that the sets items1 and items2 are identical, by checking m_Uuid
186 for( EDA_ITEM* item1 : items1 )
187 {
188 auto cmp = [&]( EDA_ITEM* elem )
189 {
190 return elem->m_Uuid.AsString() == item1->m_Uuid.AsString();
191 };
192
193 auto item2 = std::find_if( items2.begin(), items2.end(), cmp );
194
195 BOOST_CHECK( item2 != items2.end() );
196 }
197}
198
199
200// Check if two GROUPS are identical by comparing the groups in each of them.
201void testGroupsEqual( const GROUPS& groups1, const GROUPS& groups2 )
202{
203 BOOST_CHECK_EQUAL( groups1.size(), groups2.size() );
204
205 for( PCB_GROUP* group1 : groups1 )
206 {
207 auto cmp = [&]( BOARD_ITEM* elem )
208 {
209 return elem->m_Uuid.AsString() == group1->m_Uuid.AsString();
210 };
211
212 auto group2 = std::find_if( groups2.begin(), groups2.end(), cmp );
213
214 BOOST_CHECK( group2 != groups2.end() );
215
216 testGroupEqual( *group1, **group2 );
217 }
218}
219
220
221/*
222 * Create board based on spec, save it to a file, load it, and make sure the
223 * groups in the resulting board are the same as the groups we started with.
224 */
225void testSaveLoad( const std::vector<std::vector<ItemType>>& spec )
226{
227 std::unique_ptr<BOARD> board1 = createBoard( spec );
228 auto path = std::filesystem::temp_directory_path() / "group_saveload_tst.kicad_pcb";
229 ::KI_TEST::DumpBoardToFile( *board1, path.string() );
230
231 std::unique_ptr<BOARD> board2 = ::KI_TEST::ReadBoardFromFileOrStream( path.string() );
232 testGroupsEqual( board1->Groups(), board2->Groups() );
233}
234
235
236// Test saving & loading of a few configurations.
237// Groups with fewer than 2 members are not saved, so all round-trip tests
238// use groups with at least 2 members.
239BOOST_AUTO_TEST_CASE( HealthyGroups )
240{
241 // Test board with no groups
242 testSaveLoad( {} );
243
244 // Single group with 2 members
245 testSaveLoad( { { TEXT0, TEXT1 } } );
246
247 // Two groups
248 testSaveLoad( { { TEXT0, TEXT1 }, { TEXT2, TEXT3 } } );
249 testSaveLoad( { { TEXT0, TEXT1 }, { TEXT2, GROUP0 } } );
250
251 // Subgroups with 2+ members each
252 testSaveLoad( { { TEXT0, TEXT1, GROUP1 }, { TEXT2, TEXT3 }, { TEXT4, GROUP0 } } );
253 testSaveLoad( { { TEXT0, TEXT1 }, { TEXT2, TEXT3 }, { GROUP1, GROUP0 } } );
254 testSaveLoad( { { TEXT0, TEXT1 }, { TEXT2, TEXT3 }, { TEXT4, NAME_GROUP3 }, { TEXT5, TEXT6 } } );
255}
256
257
258BOOST_AUTO_TEST_CASE( SingleMemberGroupsSaved )
259{
260 std::unique_ptr<BOARD> board1 = createBoard( { { TEXT0 } } );
261 auto path = std::filesystem::temp_directory_path() / "group_saveload_tst.kicad_pcb";
262 ::KI_TEST::DumpBoardToFile( *board1, path.string() );
263
264 std::unique_ptr<BOARD> board2 = ::KI_TEST::ReadBoardFromFileOrStream( path.string() );
265 BOOST_CHECK_EQUAL( board2->Groups().size(), 1u );
266}
267
268
269// TODO: this is *probably* not needed any more as long as nothing is bypassing AddItem's
270// check for cyclic group membership, but it doesn't hurt to have it as a sanity check for the groups graph.
271BOOST_AUTO_TEST_CASE( InvalidGroups )
272{
273 // A cycle
274 std::unique_ptr<BOARD> board1 = createBoard( { { TEXT0, GROUP1 }, { TEXT2, GROUP0 } }, true );
275 BOOST_CHECK_EQUAL( board1->GroupsSanityCheck(), "Cycle detected in group membership" );
276
277 // More complex cycle
278 board1 = createBoard(
279 { { TEXT0, GROUP1 }, { TEXT1 }, { TEXT2, NAME_GROUP4 }, { TEXT3, GROUP2 }, { TEXT4, NAME_GROUP3 } }, true );
280 BOOST_CHECK_EQUAL( board1->GroupsSanityCheck(), "Cycle detected in group membership" );
281
282 // Delete the removed group since the test is over
283 board1.reset( nullptr );
284 delete s_removedGroup;
285 s_removedGroup = nullptr;
286
287 // Delete the removed text since the test is over
288 board1.reset( nullptr );
289 delete s_removedText;
290 s_removedText = nullptr;
291}
292
293
299BOOST_AUTO_TEST_CASE( DeepCloneGroupMembership )
300{
301 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
302
303 PCB_TEXT* text0 = new PCB_TEXT( board.get() );
304 text0->SetText( wxT( "child-0" ) );
305 board->Add( text0 );
306
307 PCB_TEXT* text1 = new PCB_TEXT( board.get() );
308 text1->SetText( wxT( "child-1" ) );
309 board->Add( text1 );
310
311 PCB_GROUP* group = new PCB_GROUP( board.get() );
312 group->SetName( wxT( "TestGroup" ) );
313 group->AddItem( text0 );
314 group->AddItem( text1 );
315 board->Add( group );
316
317 BOOST_CHECK_EQUAL( group->GetItems().size(), 2 );
318
319 PCB_GROUP* deepCopy = group->DeepClone();
320
321 // DeepClone preserves the UUID
322 BOOST_CHECK_EQUAL( deepCopy->m_Uuid.AsString(), group->m_Uuid.AsString() );
323 BOOST_CHECK_EQUAL( deepCopy->GetName(), group->GetName() );
324 BOOST_CHECK_EQUAL( deepCopy->GetItems().size(), 2 );
325
326 // The cloned group's children must be different objects from the originals
327 for( EDA_ITEM* clonedChild : deepCopy->GetItems() )
328 {
329 BOOST_CHECK( clonedChild != text0 );
330 BOOST_CHECK( clonedChild != text1 );
331 }
332
333 // Children must NOT be the same pointers as the original group's children
334 for( EDA_ITEM* clonedChild : deepCopy->GetItems() )
335 {
336 bool foundInOriginal = group->GetItems().count( clonedChild ) > 0;
337 BOOST_CHECK_MESSAGE( !foundInOriginal,
338 "DeepClone child should not be in original group's m_items" );
339 }
340
341 // Round-trip: add the deep clone and its children to a temp board, save, reload
342 std::unique_ptr<BOARD> tempBoard = std::make_unique<BOARD>();
343 tempBoard->Add( deepCopy );
344
345 deepCopy->RunOnChildren(
346 [&]( BOARD_ITEM* child )
347 {
348 tempBoard->Add( child, ADD_MODE::APPEND, false );
349 },
351
352 auto path = std::filesystem::temp_directory_path() / "group_deepclone_tst.kicad_pcb";
353 ::KI_TEST::DumpBoardToFile( *tempBoard, path.string() );
354
355 std::unique_ptr<BOARD> reloaded = ::KI_TEST::ReadBoardFromFileOrStream( path.string() );
356
357 BOOST_CHECK_EQUAL( reloaded->Groups().size(), 1 );
358
359 if( !reloaded->Groups().empty() )
360 {
361 PCB_GROUP* loadedGroup = static_cast<PCB_GROUP*>( reloaded->Groups().front() );
362 BOOST_CHECK_EQUAL( loadedGroup->GetItems().size(), 2 );
363 BOOST_CHECK_EQUAL( loadedGroup->GetName(), wxT( "TestGroup" ) );
364 }
365}
366
367
373BOOST_AUTO_TEST_CASE( DeepCloneNestedGeneratorMembership )
374{
375 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
376
377 PCB_TEXT* text = new PCB_TEXT( board.get() );
378 text->SetText( wxT( "generated-child" ) );
379 board->Add( text );
380
381 PCB_TUNING_PATTERN* nested = new PCB_TUNING_PATTERN( board.get() );
382 nested->AddItem( text );
383 board->Add( nested );
384
385 PCB_TUNING_PATTERN* root = new PCB_TUNING_PATTERN( board.get() );
386 root->AddItem( nested );
387 board->Add( root );
388
389 PCB_GENERATOR* deepCopy = root->DeepClone();
390
391 BOOST_CHECK_EQUAL( deepCopy->GetItems().size(), 1 );
392
393 EDA_ITEM* clonedNestedRaw = *deepCopy->GetItems().begin();
394 BOOST_CHECK( clonedNestedRaw != nested );
395 BOOST_CHECK_EQUAL( clonedNestedRaw->Type(), PCB_GENERATOR_T );
396
397 PCB_GENERATOR* clonedNested = static_cast<PCB_GENERATOR*>( clonedNestedRaw );
398 BOOST_CHECK_EQUAL( clonedNested->GetItems().size(), 1 );
399
400 for( EDA_ITEM* member : clonedNested->GetItems() )
401 {
402 BOOST_CHECK( member != text );
403 }
404
405 // Clean up the deep copy tree (not owned by any board)
406 EDA_ITEM* clonedText = *clonedNested->GetItems().begin();
407 delete clonedText;
408 delete clonedNested;
409 delete deepCopy;
410}
411
412
413static PCB_TRACK* makeSegment( BOARD* aBoard, const VECTOR2I& aStart, const VECTOR2I& aEnd )
414{
415 PCB_TRACK* track = new PCB_TRACK( aBoard );
416 track->SetStart( aStart );
417 track->SetEnd( aEnd );
418 track->SetWidth( pcbIUScale.mmToIU( 0.2 ) );
419 track->SetLayer( F_Cu );
420 aBoard->Add( track );
421
422 return track;
423}
424
425
431BOOST_AUTO_TEST_CASE( DeepDuplicateGeneratorMembersAreDeepCopied )
432{
433 auto board = std::make_unique<BOARD>();
434 board->SetEnabledLayers( LSET::AllCuMask() | LSET::AllTechMask() );
435
436 PCB_TUNING_PATTERN* generator = new PCB_TUNING_PATTERN( board.get(), F_Cu );
437 board->Add( generator );
438
439 PCB_TRACK* seg1 = makeSegment( board.get(), VECTOR2I( 0, 0 ), VECTOR2I( pcbIUScale.mmToIU( 1 ), 0 ) );
440 PCB_TRACK* seg2 =
441 makeSegment( board.get(), VECTOR2I( pcbIUScale.mmToIU( 1 ), 0 ), VECTOR2I( pcbIUScale.mmToIU( 2 ), 0 ) );
442
443 generator->AddItem( seg1 );
444 generator->AddItem( seg2 );
445
446 // Wrap the generator in a group, as "Create from Selection > Group" does.
447 PCB_GROUP* group = new PCB_GROUP( board.get() );
448 group->AddItem( generator );
449 board->Add( group );
450
451 PCB_GROUP* dupGroup = group->DeepDuplicate( IGNORE_PARENT_GROUP );
452 BOOST_REQUIRE( dupGroup );
453 BOOST_REQUIRE_EQUAL( dupGroup->GetItems().size(), 1u );
454
455 PCB_GENERATOR* dupGenerator = nullptr;
456
457 for( EDA_ITEM* member : dupGroup->GetItems() )
458 {
459 BOOST_REQUIRE_EQUAL( member->Type(), PCB_GENERATOR_T );
460 dupGenerator = static_cast<PCB_GENERATOR*>( member );
461 }
462
463 BOOST_REQUIRE( dupGenerator );
464
465 // KIID has no ostream operator, so compare with BOOST_CHECK instead of BOOST_CHECK_NE.
466 BOOST_CHECK_NE( dupGenerator, static_cast<PCB_GENERATOR*>( generator ) );
467 BOOST_CHECK( dupGenerator->m_Uuid != generator->m_Uuid );
468
469 BOOST_REQUIRE_EQUAL( dupGenerator->GetItems().size(), generator->GetItems().size() );
470
471 std::set<EDA_ITEM*> originalMembers( generator->GetItems().begin(), generator->GetItems().end() );
472
473 for( EDA_ITEM* member : dupGenerator->GetItems() )
474 {
475 BOARD_ITEM* boardItem = dynamic_cast<BOARD_ITEM*>( member );
476 BOOST_REQUIRE( boardItem );
477
478 BOOST_CHECK_EQUAL( originalMembers.count( member ), 0u );
479 BOOST_CHECK( boardItem->m_Uuid != seg1->m_Uuid );
480 BOOST_CHECK( boardItem->m_Uuid != seg2->m_Uuid );
481 BOOST_CHECK_EQUAL( boardItem->GetParentGroup(), static_cast<EDA_GROUP*>( dupGenerator ) );
482 }
483
484 // The source generator must be left untouched.
485 BOOST_CHECK_EQUAL( generator->GetItems().size(), 2u );
486 BOOST_CHECK_EQUAL( seg1->GetParentGroup(), static_cast<EDA_GROUP*>( generator ) );
487 BOOST_CHECK_EQUAL( seg2->GetParentGroup(), static_cast<EDA_GROUP*>( generator ) );
488
489 // Groups do not own their members, and these copies were never added to the board.
490 std::vector<EDA_ITEM*> dupMembers( dupGenerator->GetItems().begin(), dupGenerator->GetItems().end() );
491
492 for( EDA_ITEM* member : dupMembers )
493 delete member;
494
495 delete dupGenerator;
496 delete dupGroup;
497}
498
499
const char * name
constexpr EDA_IU_SCALE pcbIUScale
Definition base_units.h:121
Construction utilities for PCB tests.
General utilities for PCB file IO for QA programs.
void SetLayer(PCB_LAYER_ID aLayer) override
Set the layer this item is on.
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:81
Information pertinent to a Pcbnew printed circuit board.
Definition board.h:372
void Add(BOARD_ITEM *aItem, ADD_MODE aMode=ADD_MODE::INSERT, bool aSkipConnectivity=false) override
Removes an item from the container.
Definition board.cpp:1295
A set of EDA_ITEMs (i.e., without duplicates).
Definition eda_group.h:42
std::unordered_set< EDA_ITEM * > & GetItems()
Definition eda_group.h:50
wxString GetName() const
Definition eda_group.h:47
void AddItem(EDA_ITEM *aItem)
Add item to group.
Definition eda_group.cpp:58
void SetName(const wxString &aName)
Definition eda_group.h:48
A base class for most all the KiCad significant classes used in schematics and boards.
Definition eda_item.h:96
const KIID m_Uuid
Definition eda_item.h:531
virtual EDA_GROUP * GetParentGroup() const
Definition eda_item.h:114
KICAD_T Type() const
Returns the type of object.
Definition eda_item.h:108
virtual void SetText(const wxString &aText)
Definition eda_text.cpp:265
wxString AsString() const
Definition kiid.cpp:242
static const LSET & AllTechMask()
Return a mask holding all technical layers (no CU layer) on both side.
Definition lset.cpp:672
static LSET AllCuMask(int aCuLayerCount)
Return a mask holding the requested number of Cu PCB_LAYER_IDs.
Definition lset.cpp:595
PCB_GENERATOR * DeepClone() const
A set of BOARD_ITEMs (i.e., without duplicates).
Definition pcb_group.h:49
void RunOnChildren(const std::function< void(BOARD_ITEM *)> &aFunction, RECURSE_MODE aMode) const override
Invoke a function on all children.
void SetEnd(const VECTOR2I &aEnd)
Definition pcb_track.h:89
void SetStart(const VECTOR2I &aStart)
Definition pcb_track.h:92
virtual void SetWidth(int aWidth)
Definition pcb_track.h:86
The common library.
#define _(s)
@ RECURSE
Definition eda_item.h:49
#define IGNORE_PARENT_GROUP
Definition eda_item.h:53
BOOST_AUTO_TEST_CASE(HealthyGroups)
@ REMOVED_TEXT
@ TEXT5
@ TEXT8
@ TEXT0
@ NAME_GROUP3_DUP
@ TEXT3
@ TEXT4
@ GROUP1
@ REMOVED_GROUP
@ NAME_GROUP4
@ TEXT6
@ NAME_GROUP3
@ TEXT1
@ TEXT2
@ NUM_ITEMS
@ GROUP2
@ GROUP0
@ TEXT7
void testGroupEqual(const PCB_GROUP &group1, const PCB_GROUP &group2)
static PCB_TRACK * makeSegment(BOARD *aBoard, const VECTOR2I &aStart, const VECTOR2I &aEnd)
void testGroupsEqual(const GROUPS &groups1, const GROUPS &groups2)
static PCB_GROUP * s_removedGroup
void testSaveLoad(const std::vector< std::vector< ItemType > > &spec)
static PCB_TEXT * s_removedText
std::unique_ptr< BOARD > createBoard(const std::vector< std::vector< ItemType > > &spec, bool aAllowInvalidGroups=false)
@ F_Cu
Definition layer_ids.h:60
std::unique_ptr< BOARD > ReadBoardFromFileOrStream(const std::string &aFilename, std::istream &aFallback)
Read a board from a file, or another stream, as appropriate.
void DumpBoardToFile(BOARD &board, const std::string &aFilename)
Utility function to simply write a Board out to a file.
Class to handle a set of BOARD_ITEMs.
std::deque< PCB_GROUP * > GROUPS
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
std::string path
BOOST_CHECK_MESSAGE(totalMismatches==0, std::to_string(totalMismatches)+" board(s) with strategy disagreements")
BOOST_CHECK_EQUAL(result, "25.4")
@ PCB_GENERATOR_T
class PCB_GENERATOR, generator on a layer
Definition typeinfo.h:84
VECTOR2< int32_t > VECTOR2I
Definition vector2d.h:683