KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_save_load_schematic.cpp
Go to the documentation of this file.
1/*
2 * This program source code file is part of KiCad, a free EDA CAD application.
3 *
4 * Copyright The KiCad Developers, see AUTHORS.txt for contributors.
5 *
6 * This program is free software; you can redistribute it and/or
7 * modify it under the terms of the GNU General Public License
8 * as published by the Free Software Foundation; either version 2
9 * of the License, or (at your option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License
17 * along with this program; if not, you may find one here:
18 * http://www.gnu.org/licenses/old-licenses/gpl-2.0.html
19 * or you may search the http://www.gnu.org website for the version 2 license,
20 * or you may write to the Free Software Foundation, Inc.,
21 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA
22 */
23
28
29#include <boost/test/unit_test.hpp>
30#include <eeschema_test_utils.h>
31
33#include <sch_screen.h>
34#include <sch_sheet.h>
35#include <sch_symbol.h>
36#include <schematic.h>
37#include <kiid.h>
38#include <sch_file_versions.h>
40
41#include <wx/ffile.h>
42#include <wx/filename.h>
43#include <wx/stdpaths.h>
44
45
47{
50 {
51 // Create a temporary project file so we have a valid project name
52 wxString tempDir = wxStandardPaths::Get().GetTempDir();
53 wxString projectPath = tempDir + wxFileName::GetPathSeparator() + wxT("test_project.kicad_pro");
54 m_tempFiles.push_back( projectPath );
55
56 m_settingsManager.LoadProject( projectPath.ToStdString() );
57 m_schematic = std::make_unique<SCHEMATIC>( nullptr );
59 m_schematic->SetProject( m_project );
60 }
61
63 {
64 // Clean up temp files
65 for( const wxString& file : m_tempFiles )
66 {
67 if( wxFileExists( file ) )
68 wxRemoveFile( file );
69 }
70
71 m_schematic.reset();
72 }
73
74 wxString GetTempFileName( const wxString& aPrefix )
75 {
76 wxString tempDir = wxStandardPaths::Get().GetTempDir();
77 wxString fileName = wxFileName::CreateTempFileName( tempDir + wxFileName::GetPathSeparator() + aPrefix );
78 m_tempFiles.push_back( fileName );
79 return fileName;
80 }
81
83 std::unique_ptr<SCHEMATIC> m_schematic;
85 std::vector<wxString> m_tempFiles;
86};
87
88
89BOOST_FIXTURE_TEST_SUITE( SaveLoadSchematic, SAVE_LOAD_FIXTURE )
90
91
92
96BOOST_AUTO_TEST_CASE( TestSaveLoadSimpleSchematic )
97{
98 // Create a simple schematic
99 m_schematic->CreateDefaultScreens();
100
101 // Get the first (and only) top-level sheet
102 std::vector<SCH_SHEET*> topSheets = m_schematic->GetTopLevelSheets();
103 BOOST_REQUIRE( !topSheets.empty() );
104
105 SCH_SCREEN* screen = topSheets[0]->GetScreen();
106 BOOST_REQUIRE( screen != nullptr );
107
108 // Set some basic properties
109 screen->SetFileName( "test.kicad_sch" );
110 topSheets[0]->SetName( "Main Sheet" );
111
112 // Verify that currentSheet is set to a top-level sheet, not virtual root
113 SCH_SHEET_PATH& currentSheet = m_schematic->CurrentSheet();
114 BOOST_CHECK( !currentSheet.empty() );
115 if( !currentSheet.empty() )
116 {
117 BOOST_CHECK( currentSheet.Last() != &m_schematic->Root() );
118 BOOST_CHECK( currentSheet.Last()->m_Uuid != niluuid );
119 }
120
121 // Save the schematic
122 wxString fileName = GetTempFileName( "test_schematic" );
123 fileName += ".kicad_sch";
124 m_tempFiles.push_back( fileName );
125
127 BOOST_CHECK_NO_THROW( io.SaveSchematicFile( fileName, topSheets[0], m_schematic.get() ) );
128
129 // Verify file was created
130 BOOST_CHECK( wxFileExists( fileName ) );
131
132 // Now try to load it back
133 m_schematic->Reset();
134 SCH_SHEET* defaultSheet = m_schematic->GetTopLevelSheet( 0 );
135 SCH_SHEET* loadedSheet = nullptr;
136 BOOST_CHECK_NO_THROW( loadedSheet = io.LoadSchematicFile( fileName, m_schematic.get() ) );
137 BOOST_CHECK( loadedSheet != nullptr );
138
139 if( loadedSheet )
140 {
141 // Set it as root (which should create virtual root and make it a top-level sheet)
142 m_schematic->AddTopLevelSheet( loadedSheet );
143 m_schematic->RemoveTopLevelSheet( defaultSheet );
144 delete defaultSheet;
145
146 // Verify we can access the schematic
147 BOOST_CHECK( m_schematic->IsValid() );
148
149 // Verify current sheet is NOT the virtual root
150 SCH_SHEET_PATH& currentSheet2 = m_schematic->CurrentSheet();
151 BOOST_CHECK( !currentSheet2.empty() );
152 if( !currentSheet2.empty() )
153 {
154 BOOST_CHECK( currentSheet2.Last() != &m_schematic->Root() );
155 BOOST_CHECK( currentSheet2.Last()->m_Uuid != niluuid );
156 }
157
158 // Try to save again (this is where the bug occurs)
159 wxString fileName2 = GetTempFileName( "test_schematic_resave" );
160 fileName2 += ".kicad_sch";
161 m_tempFiles.push_back( fileName2 );
162
163 std::vector<SCH_SHEET*> topSheets2 = m_schematic->GetTopLevelSheets();
164 BOOST_REQUIRE( !topSheets2.empty() );
165
166 // This should not assert - this is the key test for the bug fix
167 // Symbol instances should be preserved, not dropped
168 BOOST_CHECK_NO_THROW( io.SaveSchematicFile( fileName2, topSheets2[0], m_schematic.get() ) );
169
170 // Verify the second file was created
171 BOOST_CHECK( wxFileExists( fileName2 ) );
172 }
173}
174
175
179BOOST_AUTO_TEST_CASE( TestSaveLoadHierarchicalSchematic )
180{
181 // Create a schematic with hierarchy
182 m_schematic->CreateDefaultScreens();
183
184 std::vector<SCH_SHEET*> topSheets = m_schematic->GetTopLevelSheets();
185 BOOST_REQUIRE( !topSheets.empty() );
186
187 SCH_SCREEN* screen = topSheets[0]->GetScreen();
188 BOOST_REQUIRE( screen != nullptr );
189
190 // Create a sub-sheet
191 SCH_SHEET* subSheet = new SCH_SHEET( m_schematic.get() );
192 subSheet->SetName( "SubSheet" );
193 SCH_SCREEN* subScreen = new SCH_SCREEN( m_schematic.get() );
194 subSheet->SetScreen( subScreen );
195 subSheet->SetFileName( "subsheet.kicad_sch" );
196 subScreen->SetFileName( "subsheet.kicad_sch" );
197 screen->Append( subSheet );
198
199 // Save the main sheet
200 wxString mainFileName = GetTempFileName( "test_main" );
201 mainFileName += ".kicad_sch";
202 m_tempFiles.push_back( mainFileName );
203
204 // Save the sub-sheet
205 wxString subFileName = GetTempFileName( "test_sub" );
206 subFileName += ".kicad_sch";
207 m_tempFiles.push_back( subFileName );
208
210 BOOST_CHECK_NO_THROW( io.SaveSchematicFile( mainFileName, topSheets[0], m_schematic.get() ) );
211 BOOST_CHECK_NO_THROW( io.SaveSchematicFile( subFileName, subSheet, m_schematic.get() ) );
212
213 // Load it back
214 m_schematic->Reset();
215 SCH_SHEET* defaultSheet = m_schematic->GetTopLevelSheet( 0 );
216 SCH_SHEET* loadedSheet = nullptr;
217 BOOST_CHECK_NO_THROW( loadedSheet = io.LoadSchematicFile( mainFileName, m_schematic.get() ) );
218 BOOST_CHECK( loadedSheet != nullptr );
219
220 if( loadedSheet )
221 {
222 m_schematic->AddTopLevelSheet( loadedSheet );
223 m_schematic->RemoveTopLevelSheet( defaultSheet );
224 delete defaultSheet;
225
226 // Build hierarchy
227 m_schematic->RefreshHierarchy();
228
229 // Verify hierarchy has 2 sheets (main + sub)
230 SCH_SHEET_LIST hierarchy = m_schematic->Hierarchy();
231 BOOST_CHECK_EQUAL( hierarchy.size(), 2 );
232
233 // Try to save again
234 wxString mainFileName2 = GetTempFileName( "test_main_resave" );
235 mainFileName2 += ".kicad_sch";
236 m_tempFiles.push_back( mainFileName2 );
237
238 std::vector<SCH_SHEET*> topSheets2 = m_schematic->GetTopLevelSheets();
239 BOOST_REQUIRE( !topSheets2.empty() );
240
241 // This should not assert - this is the key test for the bug fix
242 BOOST_CHECK_NO_THROW( io.SaveSchematicFile( mainFileName2, topSheets2[0], m_schematic.get() ) );
243
244 // Verify the file was created
245 BOOST_CHECK( wxFileExists( mainFileName2 ) );
246 }
247}
248
249
255BOOST_AUTO_TEST_CASE( TestLoadLegacyHierarchicalSchematic )
256{
257 // Load the legacy hierarchical schematic
258 wxFileName fn( KI_TEST::GetEeschemaTestDataDir() );
259 fn.AppendDir( "legacy_hierarchy" );
260 fn.SetName( "legacy_hierarchy" );
262 wxString mainFile = fn.GetFullPath();
263
264 BOOST_TEST_MESSAGE( "Loading schematic: " + mainFile.ToStdString() );
265 BOOST_REQUIRE( wxFileExists( mainFile ) );
266
268 SCH_SHEET* loadedSheet = nullptr;
269
270 // Load the schematic
271 BOOST_CHECK_NO_THROW( loadedSheet = io.LoadSchematicFile( mainFile, m_schematic.get() ) );
272 BOOST_REQUIRE( loadedSheet != nullptr );
273
274 BOOST_TEST_MESSAGE( "Loaded sheet UUID: " + loadedSheet->m_Uuid.AsString().ToStdString() );
275 BOOST_TEST_MESSAGE( "Loaded sheet name: " + loadedSheet->GetName().ToStdString() );
276
277 m_schematic->Reset();
278 SCH_SHEET* defaultSheet = m_schematic->GetTopLevelSheet( 0 );
279 m_schematic->AddTopLevelSheet( loadedSheet );
280 m_schematic->RemoveTopLevelSheet( defaultSheet );
281 delete defaultSheet;
282
283 // Build the hierarchy
284 m_schematic->RefreshHierarchy();
285 SCH_SHEET_LIST hierarchy = m_schematic->Hierarchy();
286
287 BOOST_TEST_MESSAGE( "Hierarchy size: " + std::to_string( hierarchy.size() ) );
288
289 // This schematic has 1 root + 2 sub-sheets = 3 total sheets
290 BOOST_REQUIRE_EQUAL( hierarchy.size(), 3 );
291
292 // Check each sheet path
293 for( size_t i = 0; i < hierarchy.size(); i++ )
294 {
295 const SCH_SHEET_PATH& path = hierarchy[i];
296 BOOST_TEST_MESSAGE( "\nSheet path [" + std::to_string(i) + "]: " + path.PathHumanReadable( false ).ToStdString() );
297 BOOST_TEST_MESSAGE( " Path KIID: " + path.Path().AsString().ToStdString() );
298 BOOST_TEST_MESSAGE( std::string(" Last screen: ") + (path.LastScreen() ? "YES" : "NO") );
299 BOOST_TEST_MESSAGE( " Path size: " + std::to_string( path.size() ) );
300
301 // Verify path has a screen
302 BOOST_CHECK( path.LastScreen() != nullptr );
303
304 if( path.LastScreen() )
305 {
306 // Count symbols on this sheet
307 int symbolCount = 0;
308 for( SCH_ITEM* item : path.LastScreen()->Items().OfType( SCH_SYMBOL_T ) )
309 {
310 symbolCount++;
311 }
312 BOOST_TEST_MESSAGE( " Symbol count: " + std::to_string( symbolCount ) );
313 }
314 }
315
316 // Check for missing instances (this should create them if needed)
317 BOOST_TEST_MESSAGE( "\nChecking for missing symbol instances..." );
318 hierarchy.CheckForMissingSymbolInstances( m_project->GetProjectName() );
319
320 // Now verify that symbols have proper instance data
321 std::map<wxString, int> referenceCount;
322 int totalInstances = 0;
323 int emptyPaths = 0;
324
325 for( const SCH_SHEET_PATH& path : hierarchy )
326 {
327 if( !path.LastScreen() )
328 continue;
329
330 BOOST_TEST_MESSAGE( "\nVerifying instances for path: " + path.PathHumanReadable( false ).ToStdString() );
331
332 for( SCH_ITEM* item : path.LastScreen()->Items().OfType( SCH_SYMBOL_T ) )
333 {
334 SCH_SYMBOL* symbol = static_cast<SCH_SYMBOL*>( item );
335 SCH_SYMBOL_INSTANCE symbolInstance;
336
337 // Check if symbol has instance for this path
338 bool hasInstance = symbol->GetInstance( symbolInstance, path.Path() );
339
340 if( hasInstance )
341 {
342 totalInstances++;
343
344 // Check that path is not empty
345 if( symbolInstance.m_Path.empty() )
346 {
347 emptyPaths++;
348 BOOST_TEST_MESSAGE( " ERROR: Symbol " + symbol->m_Uuid.AsString().ToStdString() +
349 " has EMPTY instance path!" );
350 }
351 else
352 {
353 referenceCount[symbolInstance.m_Reference]++;
354 BOOST_TEST_MESSAGE( " Symbol " + symbol->m_Uuid.AsString().ToStdString() +
355 " -> " + symbolInstance.m_Reference.ToStdString() +
356 " (path: " + symbolInstance.m_Path.AsString().ToStdString() + ")" );
357 }
358 }
359 else
360 {
361 BOOST_TEST_MESSAGE( " WARNING: Symbol " + symbol->m_Uuid.AsString().ToStdString() +
362 " has NO instance for path " + path.Path().AsString().ToStdString() );
363 }
364 }
365 }
366
367 BOOST_TEST_MESSAGE( "\nSummary:" );
368 BOOST_TEST_MESSAGE( " Total instances: " + std::to_string( totalInstances ) );
369 BOOST_TEST_MESSAGE( " Empty paths: " + std::to_string( emptyPaths ) );
370 BOOST_TEST_MESSAGE( " Unique references: " + std::to_string( referenceCount.size() ) );
371
372 // CRITICAL: No symbol instance should have an empty path
373 BOOST_CHECK_EQUAL( emptyPaths, 0 );
374
375 // The schematic should have instances for all symbols
376 BOOST_CHECK( totalInstances > 0 );
377
378 // Print reference counts for debugging
379 BOOST_TEST_MESSAGE( "\nReference designator counts:" );
380 for( const auto& pair : referenceCount )
381 {
382 BOOST_TEST_MESSAGE( " " + pair.first.ToStdString() + ": " + std::to_string( pair.second ) );
383 }
384
385 // Try to save the schematic - this should NOT crash or assert
386 BOOST_TEST_MESSAGE( "\nAttempting to save schematic..." );
387 wxString saveFile = GetTempFileName( "legacy_hierarchy_resave" );
388 saveFile += "." + FILEEXT::KiCadSchematicFileExtension;
389 m_tempFiles.push_back( saveFile );
390
391 std::vector<SCH_SHEET*> topSheets = m_schematic->GetTopLevelSheets();
392 BOOST_REQUIRE( !topSheets.empty() );
393
394 // This is the critical test - saving should not assert on empty paths
395 BOOST_CHECK_NO_THROW( io.SaveSchematicFile( saveFile, topSheets[0], m_schematic.get() ) );
396 BOOST_CHECK( wxFileExists( saveFile ) );
397
398 BOOST_TEST_MESSAGE( "Save successful!" );
399}
400
401
408BOOST_AUTO_TEST_CASE( TestEmbeddedFilesPersistedInHierarchy )
409{
410 m_schematic->CreateDefaultScreens();
411
412 std::vector<SCH_SHEET*> topSheets = m_schematic->GetTopLevelSheets();
413 BOOST_REQUIRE( !topSheets.empty() );
414
415 SCH_SHEET* topSheet = topSheets[0];
416 SCH_SCREEN* topScreen = topSheet->GetScreen();
417 BOOST_REQUIRE( topScreen );
418
419 SCH_SHEET* subSheet = new SCH_SHEET( m_schematic.get() );
420 SCH_SCREEN* subScreen = new SCH_SCREEN( m_schematic.get() );
421 subSheet->SetName( "SubSheet" );
422 subSheet->SetScreen( subScreen );
423 subSheet->SetFileName( "subsheet.kicad_sch" );
424 subScreen->SetFileName( "subsheet.kicad_sch" );
425 topScreen->Append( subSheet );
426
427 m_schematic->RefreshHierarchy();
428
429 // Drive the same code path Page Settings uses: write a real .kicad_wks and embed it via
430 // the file-based AddFile overload, which handles type detection, compression, and encoding.
431 wxString wksPath = GetTempFileName( "embed_wks" );
432 wksPath += ".kicad_wks";
433 m_tempFiles.push_back( wksPath );
434
435 {
436 wxFFile wksFile( wksPath, "wb" );
437 BOOST_REQUIRE( wksFile.IsOpened() );
438 const wxString wksBody = wxT( "(kicad_wks)" );
439 BOOST_REQUIRE( wksFile.Write( wksBody ) );
440 }
441
442 BOOST_REQUIRE( m_schematic->GetEmbeddedFiles()->AddFile( wxFileName( wksPath ), false )
443 != nullptr );
444 BOOST_REQUIRE( !m_schematic->GetEmbeddedFiles()->IsEmpty() );
445
446 wxString topFileName = GetTempFileName( "embed_main" );
447 topFileName += ".kicad_sch";
448 m_tempFiles.push_back( topFileName );
449
451 BOOST_CHECK_NO_THROW( io.SaveSchematicFile( topFileName, topSheet, m_schematic.get() ) );
452 BOOST_REQUIRE( wxFileExists( topFileName ) );
453
454 wxFFile readback( topFileName, "rb" );
455 wxString contents;
456 BOOST_REQUIRE( readback.IsOpened() && readback.ReadAll( &contents ) );
457
458 const wxString embeddedName = wxFileName( wksPath ).GetFullName();
459
460 BOOST_CHECK_MESSAGE( contents.Contains( "embedded_files" ),
461 "Top-level sheet file is missing (embedded_files) block" );
462 BOOST_CHECK_MESSAGE( contents.Contains( embeddedName ),
463 "Top-level sheet file is missing the embedded worksheet entry" );
464 BOOST_CHECK_MESSAGE( contents.Contains( "(type worksheet)" ),
465 "Top-level sheet file is missing (type worksheet) marker" );
466}
467
468
const KIID m_Uuid
Definition eda_item.h:528
wxString AsString() const
Definition kiid.cpp:365
wxString AsString() const
Definition kiid.cpp:244
Container for project specific data.
Definition project.h:66
A SCH_IO derivation for loading schematic files using the new s-expression file format.
void SaveSchematicFile(const wxString &aFileName, SCH_SHEET *aSheet, SCHEMATIC *aSchematic, const std::map< std::string, UTF8 > *aProperties=nullptr) override
Write aSchematic to a storage file in a format that this SCH_IO implementation knows about,...
SCH_SHEET * LoadSchematicFile(const wxString &aFileName, SCHEMATIC *aSchematic, SCH_SHEET *aAppendToMe=nullptr, const std::map< std::string, UTF8 > *aProperties=nullptr) override
Load information from some input file format that this SCH_IO implementation knows about,...
Base class for any item which can be embedded within the SCHEMATIC container class,...
Definition sch_item.h:168
void Append(SCH_ITEM *aItem, bool aUpdateLibSymbol=true)
void SetFileName(const wxString &aFileName)
Set the file name for this screen to aFileName.
A container for handling SCH_SHEET_PATH objects in a flattened hierarchy.
void CheckForMissingSymbolInstances(const wxString &aProjectName)
Handle access to a stack of flattened SCH_SHEET objects by way of a path for creating a flattened sch...
bool empty() const
Forwarded method from std::vector.
SCH_SHEET * Last() const
Return a pointer to the last SCH_SHEET of the list.
Sheet symbol placed in a schematic, and is the entry point for a sub schematic.
Definition sch_sheet.h:48
void SetFileName(const wxString &aFilename)
Definition sch_sheet.h:380
wxString GetName() const
Definition sch_sheet.h:140
void SetName(const wxString &aName)
Definition sch_sheet.h:141
SCH_SCREEN * GetScreen() const
Definition sch_sheet.h:143
void SetScreen(SCH_SCREEN *aScreen)
Set the SCH_SCREEN associated with this sheet to aScreen.
Schematic symbol object.
Definition sch_symbol.h:76
bool GetInstance(SCH_SYMBOL_INSTANCE &aInstance, const KIID_PATH &aSheetPath, bool aTestFromEnd=false) const
static const std::string KiCadSchematicFileExtension
KIID niluuid(0)
std::string GetEeschemaTestDataDir()
Get the configured location of Eeschema test data.
std::vector< wxString > m_tempFiles
wxString GetTempFileName(const wxString &aPrefix)
SETTINGS_MANAGER m_settingsManager
std::unique_ptr< SCHEMATIC > m_schematic
A simple container for schematic symbol instance information.
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
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_TEST_MESSAGE("\n=== Real-World Polygon PIP Benchmark ===\n"<< formatTable(table))
BOOST_AUTO_TEST_CASE(TestSaveLoadSimpleSchematic)
Test that we can save and reload a schematic with the virtual root pattern and that symbol instances ...
BOOST_CHECK_EQUAL(result, "25.4")
@ SCH_SYMBOL_T
Definition typeinfo.h:173