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, you may find one here:
19 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
20 * or you may search the http://www.gnu.org website for the version 2 license,
21 * or you may write to the Free Software Foundation, Inc.,
22 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
23 */
24
25#include <bitset>
26#include <filesystem>
27#include <string>
28
29#include <board.h>
30#include <footprint.h>
31#include <pcb_generator.h>
32#include <pcb_group.h>
33#include <pcb_text.h>
34#include <pcb_track.h>
36#include <common.h>
40
41BOOST_AUTO_TEST_SUITE( GroupSaveLoad )
42
43// The tests below set up a test case with a spec for the set of groups to create.
44// A group can contain members from this list of candidates.
46{
56 REMOVED_TEXT, // Text not added to board
62 NAME_GROUP3_DUP, // Group with name identical to NAME_GROUP3
63 REMOVED_GROUP, // Group not added to board
65};
66
67// The objects associated with item REMOVED_TEXT and REMOVED_GROUP are not added to the board,
68// so they are not cleaned up when the board is deleted. These pointers stores the objects
69// so they can be deleted once they are done being used.
70static PCB_TEXT* s_removedText = nullptr;
71static PCB_GROUP* s_removedGroup = nullptr;
72
73
74/*
75 * Takes a vector of group specifications for groups to create.
76 * Each group is a vector of which ItemTypes to put in the group.
77 * The first group corresponds to GROUP0, the second to GROUP1, and os on.
78 */
79std::unique_ptr<BOARD> createBoard( const std::vector<std::vector<ItemType>>& spec, bool aAllowInvalidGroups = false )
80{
81 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
82 std::vector<BOARD_ITEM*> items;
83
84 // Create text items and add to board.
85 for( int idx = 0; idx <= REMOVED_TEXT; idx++ )
86 {
87 PCB_TEXT* textItem = new PCB_TEXT( board.get() );
88 textItem->SetText( wxString::Format( _( "some text-%d" ), idx ) );
89
90 // Don't add REMOVED_TEXT to the board
91 if( idx < REMOVED_TEXT )
92 board->Add( textItem );
93
94 items.push_back( textItem );
95 }
96
97 // Create groups
98 for( int idx = 0; idx < ( NUM_ITEMS - GROUP0 ); idx++ )
99 {
100 PCB_GROUP* gr = new PCB_GROUP( board.get() );
101
102 if( idx >= ( NAME_GROUP3 - GROUP0 ) )
103 {
104 wxString name = wxString::Format( _( "group-%d" ),
105 ( idx == ( NAME_GROUP3_DUP - GROUP0 ) ) ? 3 : idx );
106 gr->SetName( name );
108 }
109
110 items.push_back( gr );
111 }
112
113 std::bitset<NUM_ITEMS> used;
114
115 // Populate groups based on spec
116 for( int offset = 0; offset < ( NUM_ITEMS - GROUP0 ); offset++ )
117 {
118 int groupIdx = GROUP0 + offset;
119
120 PCB_GROUP* group = static_cast<PCB_GROUP*>( items[groupIdx] );
121
122 if( offset < spec.size() )
123 {
124 const std::vector<ItemType>& groupSpec = spec[offset];
125
126 for( ItemType item : groupSpec )
127 {
128 used.set( static_cast<size_t>( item ) );
129
130 if( aAllowInvalidGroups )
131 {
132 // The invalid-group tests intentionally build graphs that AddItem()
133 // rejects so GroupsSanityCheck() can verify diagnostics.
134 group->GetItems().insert( items[item] );
135 items[item]->SetParentGroup( group );
136 }
137 else
138 {
139 group->AddItem( items[item] );
140 }
141 }
142
143 BOOST_CHECK_EQUAL( group->GetItems().size(), groupSpec.size() );
144 board->Add( group );
145 }
146 else if( groupIdx != REMOVED_GROUP && used.test( groupIdx ) )
147 {
148 // This group is used in another group, so it must be on the board
149 board->Add( group );
150 }
151 else if( groupIdx != REMOVED_GROUP )
152 {
153 // If the group isn't used, delete it
154 delete group;
155 }
156 }
157
158 // Delete the removed text item if it isn't used
159 if( used.test( REMOVED_TEXT ) )
160 s_removedText = static_cast<PCB_TEXT*>( items[REMOVED_TEXT] );
161 else
162 delete items[REMOVED_TEXT];
163
164 // Delete the removed group item if it isn't used
165 if( used.test( REMOVED_GROUP ) )
166 s_removedGroup = static_cast<PCB_GROUP*>( items[REMOVED_GROUP] );
167 else
168 delete items[REMOVED_GROUP];
169
170 BOOST_TEST_CHECKPOINT( "Returning fresh board" );
171 return board;
172}
173
174
175// Check if two groups are identical by comparing the fields (by Uuid).
176void testGroupEqual( const PCB_GROUP& group1, const PCB_GROUP& group2 )
177{
178 BOOST_CHECK_EQUAL( group1.m_Uuid.AsString(), group2.m_Uuid.AsString() );
179 BOOST_CHECK_EQUAL( group1.GetName(), group2.GetName() );
180
181 const std::unordered_set<EDA_ITEM*>& items1 = group1.GetItems();
182 const std::unordered_set<EDA_ITEM*>& items2 = group2.GetItems();
183
184 BOOST_CHECK_EQUAL( items1.size(), items2.size() );
185
186 // Test that the sets items1 and items2 are identical, by checking m_Uuid
187 for( EDA_ITEM* item1 : items1 )
188 {
189 auto cmp = [&]( EDA_ITEM* elem )
190 {
191 return elem->m_Uuid.AsString() == item1->m_Uuid.AsString();
192 };
193
194 auto item2 = std::find_if( items2.begin(), items2.end(), cmp );
195
196 BOOST_CHECK( item2 != items2.end() );
197 }
198}
199
200
201// Check if two GROUPS are identical by comparing the groups in each of them.
202void testGroupsEqual( const GROUPS& groups1, const GROUPS& groups2 )
203{
204 BOOST_CHECK_EQUAL( groups1.size(), groups2.size() );
205
206 for( PCB_GROUP* group1 : groups1 )
207 {
208 auto cmp = [&]( BOARD_ITEM* elem )
209 {
210 return elem->m_Uuid.AsString() == group1->m_Uuid.AsString();
211 };
212
213 auto group2 = std::find_if( groups2.begin(), groups2.end(), cmp );
214
215 BOOST_CHECK( group2 != groups2.end() );
216
217 testGroupEqual( *group1, **group2 );
218 }
219}
220
221
222/*
223 * Create board based on spec, save it to a file, load it, and make sure the
224 * groups in the resulting board are the same as the groups we started with.
225 */
226void testSaveLoad( const std::vector<std::vector<ItemType>>& spec )
227{
228 std::unique_ptr<BOARD> board1 = createBoard( spec );
229 auto path = std::filesystem::temp_directory_path() / "group_saveload_tst.kicad_pcb";
230 ::KI_TEST::DumpBoardToFile( *board1, path.string() );
231
232 std::unique_ptr<BOARD> board2 = ::KI_TEST::ReadBoardFromFileOrStream( path.string() );
233 testGroupsEqual( board1->Groups(), board2->Groups() );
234}
235
236
237// Test saving & loading of a few configurations.
238// Groups with fewer than 2 members are not saved, so all round-trip tests
239// use groups with at least 2 members.
240BOOST_AUTO_TEST_CASE( HealthyGroups )
241{
242 // Test board with no groups
243 testSaveLoad( {} );
244
245 // Single group with 2 members
246 testSaveLoad( { { TEXT0, TEXT1 } } );
247
248 // Two groups
249 testSaveLoad( { { TEXT0, TEXT1 }, { TEXT2, TEXT3 } } );
250 testSaveLoad( { { TEXT0, TEXT1 }, { TEXT2, GROUP0 } } );
251
252 // Subgroups with 2+ members each
253 testSaveLoad( { { TEXT0, TEXT1, GROUP1 }, { TEXT2, TEXT3 }, { TEXT4, GROUP0 } } );
254 testSaveLoad( { { TEXT0, TEXT1 }, { TEXT2, TEXT3 }, { GROUP1, GROUP0 } } );
255 testSaveLoad( { { TEXT0, TEXT1 }, { TEXT2, TEXT3 }, { TEXT4, NAME_GROUP3 }, { TEXT5, TEXT6 } } );
256}
257
258
259BOOST_AUTO_TEST_CASE( SingleMemberGroupsSaved )
260{
261 std::unique_ptr<BOARD> board1 = createBoard( { { TEXT0 } } );
262 auto path = std::filesystem::temp_directory_path() / "group_saveload_tst.kicad_pcb";
263 ::KI_TEST::DumpBoardToFile( *board1, path.string() );
264
265 std::unique_ptr<BOARD> board2 = ::KI_TEST::ReadBoardFromFileOrStream( path.string() );
266 BOOST_CHECK_EQUAL( board2->Groups().size(), 1u );
267}
268
269
270// TODO: this is *probably* not needed any more as long as nothing is bypassing AddItem's
271// check for cyclic group membership, but it doesn't hurt to have it as a sanity check for the groups graph.
272BOOST_AUTO_TEST_CASE( InvalidGroups )
273{
274 // A cycle
275 std::unique_ptr<BOARD> board1 = createBoard( { { TEXT0, GROUP1 }, { TEXT2, GROUP0 } }, true );
276 BOOST_CHECK_EQUAL( board1->GroupsSanityCheck(), "Cycle detected in group membership" );
277
278 // More complex cycle
279 board1 = createBoard(
280 { { TEXT0, GROUP1 }, { TEXT1 }, { TEXT2, NAME_GROUP4 }, { TEXT3, GROUP2 }, { TEXT4, NAME_GROUP3 } }, true );
281 BOOST_CHECK_EQUAL( board1->GroupsSanityCheck(), "Cycle detected in group membership" );
282
283 // Delete the removed group since the test is over
284 board1.reset( nullptr );
285 delete s_removedGroup;
286 s_removedGroup = nullptr;
287
288 // Delete the removed text since the test is over
289 board1.reset( nullptr );
290 delete s_removedText;
291 s_removedText = nullptr;
292}
293
294
300BOOST_AUTO_TEST_CASE( DeepCloneGroupMembership )
301{
302 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
303
304 PCB_TEXT* text0 = new PCB_TEXT( board.get() );
305 text0->SetText( wxT( "child-0" ) );
306 board->Add( text0 );
307
308 PCB_TEXT* text1 = new PCB_TEXT( board.get() );
309 text1->SetText( wxT( "child-1" ) );
310 board->Add( text1 );
311
312 PCB_GROUP* group = new PCB_GROUP( board.get() );
313 group->SetName( wxT( "TestGroup" ) );
314 group->AddItem( text0 );
315 group->AddItem( text1 );
316 board->Add( group );
317
318 BOOST_CHECK_EQUAL( group->GetItems().size(), 2 );
319
320 PCB_GROUP* deepCopy = group->DeepClone();
321
322 // DeepClone preserves the UUID
323 BOOST_CHECK_EQUAL( deepCopy->m_Uuid.AsString(), group->m_Uuid.AsString() );
324 BOOST_CHECK_EQUAL( deepCopy->GetName(), group->GetName() );
325 BOOST_CHECK_EQUAL( deepCopy->GetItems().size(), 2 );
326
327 // The cloned group's children must be different objects from the originals
328 for( EDA_ITEM* clonedChild : deepCopy->GetItems() )
329 {
330 BOOST_CHECK( clonedChild != text0 );
331 BOOST_CHECK( clonedChild != text1 );
332 }
333
334 // Children must NOT be the same pointers as the original group's children
335 for( EDA_ITEM* clonedChild : deepCopy->GetItems() )
336 {
337 bool foundInOriginal = group->GetItems().count( clonedChild ) > 0;
338 BOOST_CHECK_MESSAGE( !foundInOriginal,
339 "DeepClone child should not be in original group's m_items" );
340 }
341
342 // Round-trip: add the deep clone and its children to a temp board, save, reload
343 std::unique_ptr<BOARD> tempBoard = std::make_unique<BOARD>();
344 tempBoard->Add( deepCopy );
345
346 deepCopy->RunOnChildren(
347 [&]( BOARD_ITEM* child )
348 {
349 tempBoard->Add( child, ADD_MODE::APPEND, false );
350 },
352
353 auto path = std::filesystem::temp_directory_path() / "group_deepclone_tst.kicad_pcb";
354 ::KI_TEST::DumpBoardToFile( *tempBoard, path.string() );
355
356 std::unique_ptr<BOARD> reloaded = ::KI_TEST::ReadBoardFromFileOrStream( path.string() );
357
358 BOOST_CHECK_EQUAL( reloaded->Groups().size(), 1 );
359
360 if( !reloaded->Groups().empty() )
361 {
362 PCB_GROUP* loadedGroup = static_cast<PCB_GROUP*>( reloaded->Groups().front() );
363 BOOST_CHECK_EQUAL( loadedGroup->GetItems().size(), 2 );
364 BOOST_CHECK_EQUAL( loadedGroup->GetName(), wxT( "TestGroup" ) );
365 }
366}
367
368
374BOOST_AUTO_TEST_CASE( DeepCloneNestedGeneratorMembership )
375{
376 std::unique_ptr<BOARD> board = std::make_unique<BOARD>();
377
378 PCB_TEXT* text = new PCB_TEXT( board.get() );
379 text->SetText( wxT( "generated-child" ) );
380 board->Add( text );
381
382 PCB_TUNING_PATTERN* nested = new PCB_TUNING_PATTERN( board.get() );
383 nested->AddItem( text );
384 board->Add( nested );
385
386 PCB_TUNING_PATTERN* root = new PCB_TUNING_PATTERN( board.get() );
387 root->AddItem( nested );
388 board->Add( root );
389
390 PCB_GENERATOR* deepCopy = root->DeepClone();
391
392 BOOST_CHECK_EQUAL( deepCopy->GetItems().size(), 1 );
393
394 EDA_ITEM* clonedNestedRaw = *deepCopy->GetItems().begin();
395 BOOST_CHECK( clonedNestedRaw != nested );
396 BOOST_CHECK_EQUAL( clonedNestedRaw->Type(), PCB_GENERATOR_T );
397
398 PCB_GENERATOR* clonedNested = static_cast<PCB_GENERATOR*>( clonedNestedRaw );
399 BOOST_CHECK_EQUAL( clonedNested->GetItems().size(), 1 );
400
401 for( EDA_ITEM* member : clonedNested->GetItems() )
402 {
403 BOOST_CHECK( member != text );
404 }
405
406 // Clean up the deep copy tree (not owned by any board)
407 EDA_ITEM* clonedText = *clonedNested->GetItems().begin();
408 delete clonedText;
409 delete clonedNested;
410 delete deepCopy;
411}
412
413
const char * name
Construction utilities for PCB tests.
General utilities for PCB file IO for QA programs.
A base class for any item which can be embedded within the BOARD container class, and therefore insta...
Definition board_item.h:84
std::unordered_set< EDA_ITEM * > & GetItems()
Definition eda_group.h:54
wxString GetName() const
Definition eda_group.h:51
void AddItem(EDA_ITEM *aItem)
Add item to group.
Definition eda_group.cpp:62
void SetName(const wxString &aName)
Definition eda_group.h:52
A base class for most all the KiCad significant classes used in schematics and boards.
Definition eda_item.h:100
const KIID m_Uuid
Definition eda_item.h:535
KICAD_T Type() const
Returns the type of object.
Definition eda_item.h:112
virtual void SetText(const wxString &aText)
Definition eda_text.cpp:269
wxString AsString() const
Definition kiid.cpp:244
PCB_GENERATOR * DeepClone() const
A set of BOARD_ITEMs (i.e., without duplicates).
Definition pcb_group.h:53
void RunOnChildren(const std::function< void(BOARD_ITEM *)> &aFunction, RECURSE_MODE aMode) const override
Invoke a function on all children.
The common library.
#define _(s)
@ RECURSE
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)
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)
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_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:88