KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_project_file.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, see <https://www.gnu.org/licenses/>.
18 */
19
24
29#include <wx/filename.h>
30
31#include <json_common.h>
32
33#include <filesystem>
34#include <fstream>
35#include <sstream>
36
37namespace fs = std::filesystem;
38
39
41{
42public:
44 {
45 m_tempDir = fs::temp_directory_path() / "kicad_project_file_test";
46 fs::remove_all( m_tempDir );
47 fs::create_directories( m_tempDir );
48 }
49
51 {
52 fs::remove_all( m_tempDir );
53 }
54
55 fs::path m_tempDir;
56};
57
58
59BOOST_FIXTURE_TEST_SUITE( ProjectFile, PROJECT_FILE_TEST_FIXTURE )
60
61
62
67BOOST_AUTO_TEST_CASE( SaveAsUpdatesTopLevelSheetNames )
68{
69 fs::path oldProjectDir = m_tempDir / "old_project";
70 fs::path newProjectDir = m_tempDir / "new_project";
71 fs::create_directories( oldProjectDir );
72 fs::create_directories( newProjectDir );
73
74 wxString oldProjectPath = wxString( oldProjectDir.string() ) + wxFileName::GetPathSeparator()
75 + wxS( "old_project." ) + FILEEXT::ProjectFileExtension;
76
77 PROJECT_FILE projectFile( oldProjectPath );
78
79 // Add a top-level sheet with name matching the project name
80 TOP_LEVEL_SHEET_INFO sheetInfo;
81 sheetInfo.uuid = KIID();
82 sheetInfo.name = wxS( "old_project" );
83 sheetInfo.filename = wxS( "old_project.kicad_sch" );
84
85 projectFile.GetTopLevelSheets().push_back( sheetInfo );
86
87 // Add another sheet with a custom name that should NOT be changed
88 TOP_LEVEL_SHEET_INFO customSheet;
89 customSheet.uuid = KIID();
90 customSheet.name = wxS( "CustomSheet" );
91 customSheet.filename = wxS( "custom_sheet.kicad_sch" );
92
93 projectFile.GetTopLevelSheets().push_back( customSheet );
94
95 // Perform SaveAs to new project
96 projectFile.SaveAs( wxString( newProjectDir.string() ), wxS( "new_project" ) );
97
98 // Verify the sheet name was updated
99 const std::vector<TOP_LEVEL_SHEET_INFO>& sheets = projectFile.GetTopLevelSheets();
100
101 BOOST_REQUIRE_EQUAL( sheets.size(), 2 );
102
103 // First sheet's name should be updated to match the new project name
104 BOOST_CHECK_EQUAL( sheets[0].name, wxS( "new_project" ) );
105
106 // First sheet's filename should also be updated
107 BOOST_CHECK_EQUAL( sheets[0].filename, wxS( "new_project.kicad_sch" ) );
108
109 // Second sheet's custom name should remain unchanged
110 BOOST_CHECK_EQUAL( sheets[1].name, wxS( "CustomSheet" ) );
111
112 // Second sheet's filename should remain unchanged
113 BOOST_CHECK_EQUAL( sheets[1].filename, wxS( "custom_sheet.kicad_sch" ) );
114}
115
116
126BOOST_AUTO_TEST_CASE( LoadFixesStaleTopLevelSheetReferences )
127{
128 fs::path projectDir = m_tempDir / "my_project";
129 fs::create_directories( projectDir );
130
131 // Write a .kicad_pro that references "default.kicad_sch" (as if copied from a template)
132 std::string proContent = R"({
133 "meta": {
134 "filename": "my_project.kicad_pro",
135 "version": 3
136 },
137 "schematic": {
138 "top_level_sheets": [
139 {
140 "uuid": "00000000-0000-0000-0000-000000000000",
141 "name": "default",
142 "filename": "default.kicad_sch"
143 }
144 ]
145 }
146 })";
147
148 fs::path proPath = projectDir / "my_project.kicad_pro";
149 std::ofstream proFile( proPath );
150 proFile << proContent;
151 proFile.close();
152
153 // Create the renamed schematic file (as if the template copy renamed it)
154 fs::path schPath = projectDir / "my_project.kicad_sch";
155 std::ofstream schFile( schPath );
156 schFile << "(kicad_sch (version 20231120) (generator \"eeschema\") (generator_version \"9.99\")";
157 schFile << " (uuid \"12345678-1234-1234-1234-123456789abc\")";
158 schFile << " (paper \"A4\"))";
159 schFile.close();
160
161 // Load the project using SETTINGS_MANAGER
162 SETTINGS_MANAGER settingsManager;
163 settingsManager.LoadProject( wxString( proPath.string() ) );
164
165 PROJECT& project = settingsManager.Prj();
166 PROJECT_FILE& projectFile = project.GetProjectFile();
167
168 const std::vector<TOP_LEVEL_SHEET_INFO>& sheets = projectFile.GetTopLevelSheets();
169
170 BOOST_REQUIRE_EQUAL( sheets.size(), 1 );
171
172 // The filename should have been corrected from "default.kicad_sch" to "my_project.kicad_sch"
173 BOOST_CHECK_EQUAL( sheets[0].filename, wxS( "my_project.kicad_sch" ) );
174
175 // The name should also be updated
176 BOOST_CHECK_EQUAL( sheets[0].name, wxS( "my_project" ) );
177}
178
179
183BOOST_AUTO_TEST_CASE( LoadPreservesValidTopLevelSheetReferences )
184{
185 fs::path projectDir = m_tempDir / "valid_project";
186 fs::create_directories( projectDir );
187
188 // Write a .kicad_pro with valid references
189 std::string proContent = R"({
190 "meta": {
191 "filename": "valid_project.kicad_pro",
192 "version": 3
193 },
194 "schematic": {
195 "top_level_sheets": [
196 {
197 "uuid": "00000000-0000-0000-0000-000000000000",
198 "name": "valid_project",
199 "filename": "valid_project.kicad_sch"
200 }
201 ]
202 }
203 })";
204
205 fs::path proPath = projectDir / "valid_project.kicad_pro";
206 std::ofstream proFile( proPath );
207 proFile << proContent;
208 proFile.close();
209
210 // Create the schematic file that matches the reference
211 fs::path schPath = projectDir / "valid_project.kicad_sch";
212 std::ofstream schFile( schPath );
213 schFile << "(kicad_sch (version 20231120) (generator \"eeschema\") (generator_version \"9.99\")";
214 schFile << " (uuid \"12345678-1234-1234-1234-123456789abc\")";
215 schFile << " (paper \"A4\"))";
216 schFile.close();
217
218 SETTINGS_MANAGER settingsManager;
219 settingsManager.LoadProject( wxString( proPath.string() ) );
220
221 PROJECT& project = settingsManager.Prj();
222 PROJECT_FILE& projectFile = project.GetProjectFile();
223
224 const std::vector<TOP_LEVEL_SHEET_INFO>& sheets = projectFile.GetTopLevelSheets();
225
226 BOOST_REQUIRE_EQUAL( sheets.size(), 1 );
227
228 // References should be unchanged
229 BOOST_CHECK_EQUAL( sheets[0].filename, wxS( "valid_project.kicad_sch" ) );
230 BOOST_CHECK_EQUAL( sheets[0].name, wxS( "valid_project" ) );
231}
232
233
247BOOST_AUTO_TEST_CASE( LoadProjectByAbsolutePathIsStable )
248{
249 fs::path projectDir = m_tempDir / "jobset_project";
250 fs::create_directories( projectDir );
251
252 std::string proContent = R"({
253 "meta": {
254 "filename": "jobset_project.kicad_pro",
255 "version": 3
256 }
257 })";
258
259 fs::path proPath = projectDir / "jobset_project.kicad_pro";
260 std::ofstream proFile( proPath );
261 proFile << proContent;
262 proFile.close();
263
264 wxFileName absFn( wxString( proPath.string() ) );
265 absFn.MakeAbsolute();
266 wxString absPath = absFn.GetFullPath();
267
268 SETTINGS_MANAGER settingsManager;
269
270 // Load as the fixed jobset runner does: by absolute path.
271 BOOST_REQUIRE( settingsManager.LoadProject( absPath ) );
272
273 PROJECT* heldProject = settingsManager.GetProject( absPath );
274 BOOST_REQUIRE( heldProject != nullptr );
275
276 // Simulate the kiface board loader resolving and (re)loading the project by its absolute path.
277 BOOST_REQUIRE( settingsManager.LoadProject( absPath, true ) );
278
279 // The second load must be a no-op for the held pointer; the project must not have been evicted.
280 PROJECT* afterReload = settingsManager.GetProject( absPath );
281 BOOST_CHECK( afterReload == heldProject );
282
283 // The held pointer must still resolve to the same project name (i.e. it was not freed).
284 BOOST_CHECK_EQUAL( heldProject->GetProjectFullName(), absPath );
285}
286
287
297BOOST_AUTO_TEST_CASE( UnloadProjectSavesToOwnDirectory )
298{
299 fs::path projADir = m_tempDir / "proj_a";
300 fs::path projBDir = m_tempDir / "proj_b";
301 fs::create_directories( projADir );
302 fs::create_directories( projBDir );
303
304 // Shared basename in different directories reproduces the cross-project clobber. The
305 // matching schematic keeps LoadFromFile from flagging the project as migrated, which
306 // would otherwise suppress the auto-save under test.
307 const std::string projectName = "shared_name";
308
309 auto writeProject = [&]( const fs::path& aDir )
310 {
311 std::string content = "{\n"
312 " \"meta\": {\n"
313 " \"filename\": \"" + projectName + ".kicad_pro\",\n"
314 " \"version\": 3\n"
315 " },\n"
316 " \"schematic\": {\n"
317 " \"top_level_sheets\": [\n"
318 " {\n"
319 " \"uuid\": \"00000000-0000-0000-0000-000000000000\",\n"
320 " \"name\": \"" + projectName + "\",\n"
321 " \"filename\": \"" + projectName + ".kicad_sch\"\n"
322 " }\n"
323 " ]\n"
324 " }\n"
325 "}\n";
326 std::ofstream out( aDir / ( projectName + ".kicad_pro" ) );
327 out << content;
328 out.close();
329
330 std::ofstream sch( aDir / ( projectName + ".kicad_sch" ) );
331 sch << "(kicad_sch (version 20231120) (generator \"eeschema\") (generator_version \"9.99\")";
332 sch << " (uuid \"12345678-1234-1234-1234-123456789abc\") (paper \"A4\"))";
333 sch.close();
334 };
335
336 fs::path proAPath = projADir / ( projectName + ".kicad_pro" );
337 fs::path proBPath = projBDir / ( projectName + ".kicad_pro" );
338 writeProject( projADir );
339 writeProject( projBDir );
340
342
343 // A becomes the active project; B is loaded but left non-active so both are resident.
344 BOOST_REQUIRE( mgr.LoadProject( wxString( proAPath.string() ), true ) );
345 BOOST_REQUIRE( mgr.LoadProject( wxString( proBPath.string() ), false ) );
346
347 PROJECT* projB = mgr.GetProject( wxString( proBPath.string() ) );
348 BOOST_REQUIRE( projB != nullptr );
349
350 // Prj() must be A so that a Prj()-based path resolution would target the wrong directory.
351 BOOST_REQUIRE_EQUAL( mgr.Prj().GetProjectFullName(), wxString( proAPath.string() ) );
352
353 // Mark B's project file so the save has something distinctive to persist.
354 projB->GetProjectFile().m_TextVars[wxS( "OWNER" )] = wxS( "proj_b" );
355
356 BOOST_REQUIRE( mgr.UnloadProject( projB, true ) );
357
358 auto readFile = []( const fs::path& aPath )
359 {
360 std::ifstream in( aPath );
361 std::stringstream buffer;
362 buffer << in.rdbuf();
363 return buffer.str();
364 };
365
366 // B's own file must have received B's marker.
367 std::string savedB = readFile( proBPath );
368 BOOST_CHECK_MESSAGE( savedB.find( "OWNER" ) != std::string::npos,
369 "unloaded project must be saved to its own directory" );
370
371 // A's identically named file must not have been clobbered with B's data.
372 std::string savedA = readFile( proAPath );
373 BOOST_CHECK_MESSAGE( savedA.find( "OWNER" ) == std::string::npos,
374 "active project's file must not receive the unloaded project's data" );
375}
376
377
388BOOST_AUTO_TEST_CASE( NoRewriteWhenUnchanged )
389{
390 fs::path projectDir = m_tempDir / "unchanged_project";
391 fs::create_directories( projectDir );
392
393 fs::path proPath = projectDir / "unchanged_project.kicad_pro";
394
395 // Produce a canonical, fully-populated current-version file with KiCad's own writer so the
396 // reload round-trip is otherwise clean.
397 {
398 std::ofstream seed( proPath );
399 seed << R"({"meta":{"version":3}})";
400 seed.close();
401
403 BOOST_REQUIRE( mgr.LoadProject( wxString( proPath.string() ), true ) );
404 BOOST_REQUIRE( mgr.SaveProject() );
405 mgr.UnloadProject( &mgr.Prj(), false );
406 }
407
408 auto readFile = []( const fs::path& aPath )
409 {
410 std::ifstream in( aPath );
411 std::stringstream buffer;
412 buffer << in.rdbuf();
413 return buffer.str();
414 };
415
416 // Drop keys the file would omit if saved before those parameters existed. The scalar
417 // board.ipc2581 block covers the plain PARAM path; schematic.bus_aliases (default null in
418 // memory but serialized as {}) covers the PARAM_LAMBDA path. Both hold their defaults, so a
419 // no-op load must not resurrect them.
420 {
421 nlohmann::json js = nlohmann::json::parse( readFile( proPath ) );
422 js["board"].erase( "ipc2581" );
423 js["schematic"].erase( "bus_aliases" );
424
425 std::ofstream out( proPath );
426 out << std::setw( 2 ) << js << std::endl;
427 out.close();
428 }
429
430 std::string before = readFile( proPath );
431 BOOST_REQUIRE( before.find( "ipc2581" ) == std::string::npos );
432 BOOST_REQUIRE( before.find( "bus_aliases" ) == std::string::npos );
433
435 BOOST_REQUIRE( mgr.LoadProject( wxString( proPath.string() ), true ) );
436
437 PROJECT_FILE& projectFile = mgr.Prj().GetProjectFile();
438
439 // The auto-save path must decline to write when nothing changed.
440 BOOST_CHECK( !projectFile.SaveToFile( wxString( projectDir.string() ) ) );
441
442 // And the on-disk file must be byte-for-byte unchanged.
443 BOOST_CHECK_EQUAL( before, readFile( proPath ) );
444}
445
446
const char * name
Definition kiid.h:46
The backing store for a PROJECT, in JSON format.
std::map< wxString, wxString > m_TextVars
bool SaveAs(const wxString &aDirectory, const wxString &aFile)
std::vector< TOP_LEVEL_SHEET_INFO > & GetTopLevelSheets()
bool SaveToFile(const wxString &aDirectory="", bool aForce=false) override
Calls Store() and then writes the contents of the JSON document to a file.
Container for project specific data.
Definition project.h:63
virtual const wxString GetProjectFullName() const
Return the full path and name of the project.
Definition project.cpp:177
virtual PROJECT_FILE & GetProjectFile() const
Definition project.h:201
bool SaveProject(const wxString &aFullPath=wxEmptyString, PROJECT *aProject=nullptr)
Save a loaded project.
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.
bool UnloadProject(PROJECT *aProject, bool aSave=true)
Save, unload and unregister the given PROJECT.
PROJECT & Prj() const
A helper while we are not MDI-capable – return the one and only project.
static const std::string ProjectFileExtension
static bool readFile(const wxString &aFileName, wxString &aOut, size_t aLimit=0)
Read a file into aOut.
Information about a top-level schematic sheet.
KIID uuid
Unique identifier for the sheet.
wxString name
Display name for the sheet.
wxString filename
Relative path to the sheet file.
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
BOOST_AUTO_TEST_CASE(SaveAsUpdatesTopLevelSheetNames)
Test that SaveAs updates top-level sheet names when they match the old project name.
BOOST_CHECK_EQUAL(result, "25.4")
Definition of file extensions used in Kicad.