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
62BOOST_AUTO_TEST_CASE( Ipc2581ExplicitEmptySectionsRoundTrips )
63{
64 fs::path projectPath = m_tempDir / "api_kitchen_sink.kicad_pro";
65 fs::copy_file( fs::path( KI_TEST::GetTestDataRootDir() ) / "pcbnew" / "api_kitchen_sink.kicad_pro",
66 projectPath );
67
68 {
70 BOOST_REQUIRE( mgr.LoadProject( wxString( projectPath.string() ), true ) );
72 BOOST_REQUIRE( !project.m_IP2581Bom.sections );
73 project.m_IP2581Bom.sections = wxString();
74 BOOST_REQUIRE( project.SaveToFile( wxString( m_tempDir.string() ), true ) );
75 mgr.UnloadProject( &mgr.Prj(), false );
76 }
77
79 BOOST_REQUIRE( mgr.LoadProject( wxString( projectPath.string() ), true ) );
80 PROJECT_FILE& loaded = mgr.Prj().GetProjectFile();
82 BOOST_CHECK( loaded.m_IP2581Bom.sections->IsEmpty() );
83}
84
85
91BOOST_AUTO_TEST_CASE( SaveAsUpdatesTopLevelSheetNames )
92{
93 fs::path oldProjectDir = m_tempDir / "old_project";
94 fs::path newProjectDir = m_tempDir / "new_project";
95 fs::create_directories( oldProjectDir );
96 fs::create_directories( newProjectDir );
97
98 wxString oldProjectPath = wxString( oldProjectDir.string() ) + wxFileName::GetPathSeparator()
99 + wxS( "old_project." ) + FILEEXT::ProjectFileExtension;
100
101 PROJECT_FILE projectFile( oldProjectPath );
102
103 // Add a top-level sheet with name matching the project name
104 TOP_LEVEL_SHEET_INFO sheetInfo;
105 sheetInfo.uuid = KIID();
106 sheetInfo.name = wxS( "old_project" );
107 sheetInfo.filename = wxS( "old_project.kicad_sch" );
108
109 projectFile.GetTopLevelSheets().push_back( sheetInfo );
110
111 // Add another sheet with a custom name that should NOT be changed
112 TOP_LEVEL_SHEET_INFO customSheet;
113 customSheet.uuid = KIID();
114 customSheet.name = wxS( "CustomSheet" );
115 customSheet.filename = wxS( "custom_sheet.kicad_sch" );
116
117 projectFile.GetTopLevelSheets().push_back( customSheet );
118
119 // Perform SaveAs to new project
120 projectFile.SaveAs( wxString( newProjectDir.string() ), wxS( "new_project" ) );
121
122 // Verify the sheet name was updated
123 const std::vector<TOP_LEVEL_SHEET_INFO>& sheets = projectFile.GetTopLevelSheets();
124
125 BOOST_REQUIRE_EQUAL( sheets.size(), 2 );
126
127 // First sheet's name should be updated to match the new project name
128 BOOST_CHECK_EQUAL( sheets[0].name, wxS( "new_project" ) );
129
130 // First sheet's filename should also be updated
131 BOOST_CHECK_EQUAL( sheets[0].filename, wxS( "new_project.kicad_sch" ) );
132
133 // Second sheet's custom name should remain unchanged
134 BOOST_CHECK_EQUAL( sheets[1].name, wxS( "CustomSheet" ) );
135
136 // Second sheet's filename should remain unchanged
137 BOOST_CHECK_EQUAL( sheets[1].filename, wxS( "custom_sheet.kicad_sch" ) );
138}
139
140
150BOOST_AUTO_TEST_CASE( LoadFixesStaleTopLevelSheetReferences )
151{
152 fs::path projectDir = m_tempDir / "my_project";
153 fs::create_directories( projectDir );
154
155 // Write a .kicad_pro that references "default.kicad_sch" (as if copied from a template)
156 std::string proContent = R"({
157 "meta": {
158 "filename": "my_project.kicad_pro",
159 "version": 3
160 },
161 "schematic": {
162 "top_level_sheets": [
163 {
164 "uuid": "00000000-0000-0000-0000-000000000000",
165 "name": "default",
166 "filename": "default.kicad_sch"
167 }
168 ]
169 }
170 })";
171
172 fs::path proPath = projectDir / "my_project.kicad_pro";
173 std::ofstream proFile( proPath );
174 proFile << proContent;
175 proFile.close();
176
177 // Create the renamed schematic file (as if the template copy renamed it)
178 fs::path schPath = projectDir / "my_project.kicad_sch";
179 std::ofstream schFile( schPath );
180 schFile << "(kicad_sch (version 20231120) (generator \"eeschema\") (generator_version \"9.99\")";
181 schFile << " (uuid \"12345678-1234-1234-1234-123456789abc\")";
182 schFile << " (paper \"A4\"))";
183 schFile.close();
184
185 // Load the project using SETTINGS_MANAGER
186 SETTINGS_MANAGER settingsManager;
187 settingsManager.LoadProject( wxString( proPath.string() ) );
188
189 PROJECT& project = settingsManager.Prj();
190 PROJECT_FILE& projectFile = project.GetProjectFile();
191
192 const std::vector<TOP_LEVEL_SHEET_INFO>& sheets = projectFile.GetTopLevelSheets();
193
194 BOOST_REQUIRE_EQUAL( sheets.size(), 1 );
195
196 // The filename should have been corrected from "default.kicad_sch" to "my_project.kicad_sch"
197 BOOST_CHECK_EQUAL( sheets[0].filename, wxS( "my_project.kicad_sch" ) );
198
199 // The name should also be updated
200 BOOST_CHECK_EQUAL( sheets[0].name, wxS( "my_project" ) );
201}
202
203
207BOOST_AUTO_TEST_CASE( LoadPreservesValidTopLevelSheetReferences )
208{
209 fs::path projectDir = m_tempDir / "valid_project";
210 fs::create_directories( projectDir );
211
212 // Write a .kicad_pro with valid references
213 std::string proContent = R"({
214 "meta": {
215 "filename": "valid_project.kicad_pro",
216 "version": 3
217 },
218 "schematic": {
219 "top_level_sheets": [
220 {
221 "uuid": "00000000-0000-0000-0000-000000000000",
222 "name": "valid_project",
223 "filename": "valid_project.kicad_sch"
224 }
225 ]
226 }
227 })";
228
229 fs::path proPath = projectDir / "valid_project.kicad_pro";
230 std::ofstream proFile( proPath );
231 proFile << proContent;
232 proFile.close();
233
234 // Create the schematic file that matches the reference
235 fs::path schPath = projectDir / "valid_project.kicad_sch";
236 std::ofstream schFile( schPath );
237 schFile << "(kicad_sch (version 20231120) (generator \"eeschema\") (generator_version \"9.99\")";
238 schFile << " (uuid \"12345678-1234-1234-1234-123456789abc\")";
239 schFile << " (paper \"A4\"))";
240 schFile.close();
241
242 SETTINGS_MANAGER settingsManager;
243 settingsManager.LoadProject( wxString( proPath.string() ) );
244
245 PROJECT& project = settingsManager.Prj();
246 PROJECT_FILE& projectFile = project.GetProjectFile();
247
248 const std::vector<TOP_LEVEL_SHEET_INFO>& sheets = projectFile.GetTopLevelSheets();
249
250 BOOST_REQUIRE_EQUAL( sheets.size(), 1 );
251
252 // References should be unchanged
253 BOOST_CHECK_EQUAL( sheets[0].filename, wxS( "valid_project.kicad_sch" ) );
254 BOOST_CHECK_EQUAL( sheets[0].name, wxS( "valid_project" ) );
255}
256
257
271BOOST_AUTO_TEST_CASE( LoadProjectByAbsolutePathIsStable )
272{
273 fs::path projectDir = m_tempDir / "jobset_project";
274 fs::create_directories( projectDir );
275
276 std::string proContent = R"({
277 "meta": {
278 "filename": "jobset_project.kicad_pro",
279 "version": 3
280 }
281 })";
282
283 fs::path proPath = projectDir / "jobset_project.kicad_pro";
284 std::ofstream proFile( proPath );
285 proFile << proContent;
286 proFile.close();
287
288 wxFileName absFn( wxString( proPath.string() ) );
289 absFn.MakeAbsolute();
290 wxString absPath = absFn.GetFullPath();
291
292 SETTINGS_MANAGER settingsManager;
293
294 // Load as the fixed jobset runner does: by absolute path.
295 BOOST_REQUIRE( settingsManager.LoadProject( absPath ) );
296
297 PROJECT* heldProject = settingsManager.GetProject( absPath );
298 BOOST_REQUIRE( heldProject != nullptr );
299
300 // Simulate the kiface board loader resolving and (re)loading the project by its absolute path.
301 BOOST_REQUIRE( settingsManager.LoadProject( absPath, true ) );
302
303 // The second load must be a no-op for the held pointer; the project must not have been evicted.
304 PROJECT* afterReload = settingsManager.GetProject( absPath );
305 BOOST_CHECK( afterReload == heldProject );
306
307 // The held pointer must still resolve to the same project name (i.e. it was not freed).
308 BOOST_CHECK_EQUAL( heldProject->GetProjectFullName(), absPath );
309}
310
311
321BOOST_AUTO_TEST_CASE( UnloadProjectSavesToOwnDirectory )
322{
323 fs::path projADir = m_tempDir / "proj_a";
324 fs::path projBDir = m_tempDir / "proj_b";
325 fs::create_directories( projADir );
326 fs::create_directories( projBDir );
327
328 // Shared basename in different directories reproduces the cross-project clobber. The
329 // matching schematic keeps LoadFromFile from flagging the project as migrated, which
330 // would otherwise suppress the auto-save under test.
331 const std::string projectName = "shared_name";
332
333 auto writeProject = [&]( const fs::path& aDir )
334 {
335 std::string content = "{\n"
336 " \"meta\": {\n"
337 " \"filename\": \"" + projectName + ".kicad_pro\",\n"
338 " \"version\": 3\n"
339 " },\n"
340 " \"schematic\": {\n"
341 " \"top_level_sheets\": [\n"
342 " {\n"
343 " \"uuid\": \"00000000-0000-0000-0000-000000000000\",\n"
344 " \"name\": \"" + projectName + "\",\n"
345 " \"filename\": \"" + projectName + ".kicad_sch\"\n"
346 " }\n"
347 " ]\n"
348 " }\n"
349 "}\n";
350 std::ofstream out( aDir / ( projectName + ".kicad_pro" ) );
351 out << content;
352 out.close();
353
354 std::ofstream sch( aDir / ( projectName + ".kicad_sch" ) );
355 sch << "(kicad_sch (version 20231120) (generator \"eeschema\") (generator_version \"9.99\")";
356 sch << " (uuid \"12345678-1234-1234-1234-123456789abc\") (paper \"A4\"))";
357 sch.close();
358 };
359
360 fs::path proAPath = projADir / ( projectName + ".kicad_pro" );
361 fs::path proBPath = projBDir / ( projectName + ".kicad_pro" );
362 writeProject( projADir );
363 writeProject( projBDir );
364
366
367 // A becomes the active project; B is loaded but left non-active so both are resident.
368 BOOST_REQUIRE( mgr.LoadProject( wxString( proAPath.string() ), true ) );
369 BOOST_REQUIRE( mgr.LoadProject( wxString( proBPath.string() ), false ) );
370
371 PROJECT* projB = mgr.GetProject( wxString( proBPath.string() ) );
372 BOOST_REQUIRE( projB != nullptr );
373
374 // Prj() must be A so that a Prj()-based path resolution would target the wrong directory.
375 BOOST_REQUIRE_EQUAL( mgr.Prj().GetProjectFullName(), wxString( proAPath.string() ) );
376
377 // Mark B's project file so the save has something distinctive to persist.
378 projB->GetProjectFile().m_TextVars[wxS( "OWNER" )] = wxS( "proj_b" );
379
380 BOOST_REQUIRE( mgr.UnloadProject( projB, true ) );
381
382 auto readFile = []( const fs::path& aPath )
383 {
384 std::ifstream in( aPath );
385 std::stringstream buffer;
386 buffer << in.rdbuf();
387 return buffer.str();
388 };
389
390 // B's own file must have received B's marker.
391 std::string savedB = readFile( proBPath );
392 BOOST_CHECK_MESSAGE( savedB.find( "OWNER" ) != std::string::npos,
393 "unloaded project must be saved to its own directory" );
394
395 // A's identically named file must not have been clobbered with B's data.
396 std::string savedA = readFile( proAPath );
397 BOOST_CHECK_MESSAGE( savedA.find( "OWNER" ) == std::string::npos,
398 "active project's file must not receive the unloaded project's data" );
399}
400
401
402// Switching projects must replace the active project even when a passive one sorts first
403BOOST_AUTO_TEST_CASE( LoadActiveProjectReplacesActiveNotPassive )
404{
405 auto projectPath = [&]( const std::string& aName )
406 {
407 fs::path dir = m_tempDir / aName;
408 fs::create_directories( dir );
409 return wxString( ( dir / ( aName + ".kicad_pro" ) ).string() );
410 };
411
412 const wxString active = projectPath( "z_active" );
413 const wxString passive = projectPath( "a_passive" );
414 const wxString next = projectPath( "m_next" );
415
417
418 mgr.LoadProject( active, true );
419 mgr.LoadProject( passive, false );
420 mgr.LoadProject( next, true );
421
423 BOOST_CHECK( mgr.GetProject( active ) == nullptr );
424 BOOST_CHECK( mgr.GetProject( passive ) != nullptr );
425}
426
427
438BOOST_AUTO_TEST_CASE( NoRewriteWhenUnchanged )
439{
440 fs::path projectDir = m_tempDir / "unchanged_project";
441 fs::create_directories( projectDir );
442
443 fs::path proPath = projectDir / "unchanged_project.kicad_pro";
444
445 // Produce a canonical, fully-populated current-version file with KiCad's own writer so the
446 // reload round-trip is otherwise clean.
447 {
448 std::ofstream seed( proPath );
449 seed << R"({"meta":{"version":3}})";
450 seed.close();
451
453 BOOST_REQUIRE( mgr.LoadProject( wxString( proPath.string() ), true ) );
454 BOOST_REQUIRE( mgr.SaveProject() );
455 mgr.UnloadProject( &mgr.Prj(), false );
456 }
457
458 auto readFile = []( const fs::path& aPath )
459 {
460 std::ifstream in( aPath );
461 std::stringstream buffer;
462 buffer << in.rdbuf();
463 return buffer.str();
464 };
465
466 // Files saved before ipc2581 (PARAM) and bus_aliases (PARAM_LAMBDA) existed omit both keys, and a
467 // no-op load must not resurrect their defaults
468 {
469 nlohmann::json js = nlohmann::json::parse( readFile( proPath ) );
470 js["board"].erase( "ipc2581" );
471 js["schematic"].erase( "bus_aliases" );
472
473 std::ofstream out( proPath );
474 out << std::setw( 2 ) << js << std::endl;
475 out.close();
476 }
477
478 std::string before = readFile( proPath );
479 BOOST_REQUIRE( before.find( "ipc2581" ) == std::string::npos );
480 BOOST_REQUIRE( before.find( "bus_aliases" ) == std::string::npos );
481
483 BOOST_REQUIRE( mgr.LoadProject( wxString( proPath.string() ), true ) );
484
485 PROJECT_FILE& projectFile = mgr.Prj().GetProjectFile();
486
487 // The auto-save path must decline to write when nothing changed.
488 BOOST_CHECK( !projectFile.SaveToFile( wxString( projectDir.string() ) ) );
489
490 // And the on-disk file must be byte-for-byte unchanged.
491 BOOST_CHECK_EQUAL( before, readFile( proPath ) );
492
493 // An explicitly cleared alias table must survive reopening even when the old file omitted it
494 mgr.UnloadProject( &mgr.Prj(), false );
495 BOOST_REQUIRE( mgr.LoadProject( wxString( proPath.string() ), true ) );
496 PROJECT_FILE& edited = mgr.Prj().GetProjectFile();
497 BOOST_REQUIRE( !edited.GetJson( "schematic.bus_aliases" ) );
498 edited.m_BusAliasesDefined = true;
499 BOOST_REQUIRE( edited.SaveToFile( wxString( projectDir.string() ) ) );
500 nlohmann::json saved = nlohmann::json::parse( readFile( proPath ) );
501 BOOST_CHECK( saved["schematic"]["bus_aliases"].is_object() );
502 BOOST_CHECK( saved["schematic"]["bus_aliases"].empty() );
503}
504
505
506BOOST_AUTO_TEST_CASE( AliasMigrationPreservesNonemptyDefinitions )
507{
508 const fs::path projectPath = m_tempDir / "aliases.kicad_pro";
509 const nlohmann::json aliases = { { "USB", { "D+", "D-" } } };
510
511 {
512 std::ofstream output( projectPath );
513 output << nlohmann::json( { { "meta", { { "version", 3 } } },
514 { "schematic", { { "bus_aliases", aliases } } } } ).dump( 2 );
515 output.close();
516 BOOST_REQUIRE( output.good() );
517 }
518
519 SETTINGS_MANAGER manager;
520 BOOST_REQUIRE( manager.LoadProject( wxString( projectPath.string() ), true ) );
521 PROJECT_FILE& project = manager.Prj().GetProjectFile();
522 BOOST_REQUIRE( project.m_BusAliasesDefined );
523 BOOST_REQUIRE_EQUAL( project.m_BusAliases.size(), 1 );
524 BOOST_CHECK( project.m_BusAliases.at( "USB" ) == std::vector<wxString>( { "D+", "D-" } ) );
525 BOOST_REQUIRE( project.SaveToFile( wxString( m_tempDir.string() ) ) );
526 std::ifstream input( projectPath );
527 const nlohmann::json saved = nlohmann::json::parse( input );
528 BOOST_CHECK_EQUAL( saved["meta"]["version"].get<int>(), 4 );
529 BOOST_CHECK( saved["schematic"]["bus_aliases"] == aliases );
530}
531
532
533BOOST_AUTO_TEST_CASE( LegacyFieldNameTemplatesSurviveMigration )
534{
535 const fs::path legacyPath = m_tempDir / "field_names.pro";
536 const fs::path projectPath = m_tempDir / "field_names.kicad_pro";
537
538 {
539 std::ofstream output( legacyPath );
540 output << "[schematic_editor]\n"
541 "FieldNameTemplates=(templatefields (field (name \"Manufacturer\") visible url))\n";
542 }
543
544 PROJECT_FILE project( wxString( projectPath.string() ) );
545 BOOST_REQUIRE( project.LoadFromFile() );
546
547 const auto& fields = project.m_TemplateFieldNames.GetTemplateFieldNames( TEMPLATES::SCOPE::PROJECT );
548 BOOST_REQUIRE_EQUAL( fields.size(), 1 );
549 BOOST_CHECK_EQUAL( fields[0].m_Name, wxS( "Manufacturer" ) );
550 BOOST_CHECK( fields[0].m_Visible );
551 BOOST_CHECK( fields[0].m_URL );
552
553 const nlohmann::json saved = nlohmann::json::parse( project.FormatAsString() );
554 const auto& fieldNames = saved["schematic"]["drawing"]["field_names"];
555 BOOST_REQUIRE( fieldNames.is_array() );
556 BOOST_REQUIRE_EQUAL( fieldNames.size(), 1 );
557 BOOST_CHECK_EQUAL( fieldNames[0]["name"].get<std::string>(), "Manufacturer" );
558}
559
560
561BOOST_AUTO_TEST_CASE( EmptyFieldNameArrayClearsTemplatesOnReload )
562{
563 const fs::path projectPath = m_tempDir / "field_names.kicad_pro";
564 nlohmann::json projectJson = { { "meta", { { "version", 4 } } } };
565 projectJson["schematic"]["drawing"]["field_names"] =
566 nlohmann::json::array( { { { "name", "Manufacturer" }, { "visible", true }, { "url", false } } } );
567
568 auto writeProject = [&]()
569 {
570 std::ofstream output( projectPath );
571 output << projectJson.dump( 2 );
572 };
573
574 writeProject();
575
576 PROJECT_FILE project( wxString( projectPath.string() ) );
577 BOOST_REQUIRE( project.LoadFromFile() );
578 BOOST_REQUIRE_EQUAL( project.m_TemplateFieldNames.GetTemplateFieldNames( TEMPLATES::SCOPE::PROJECT ).size(), 1 );
579
580 projectJson["schematic"]["drawing"]["field_names"] = nlohmann::json::array();
581 writeProject();
582
583 BOOST_REQUIRE( project.LoadFromFile() );
584 BOOST_CHECK( project.m_TemplateFieldNames.GetTemplateFieldNames( TEMPLATES::SCOPE::PROJECT ).empty() );
585}
586
587
const char * name
std::optional< nlohmann::json > GetJson(const std::string &aPath) const
Fetches a JSON object that is a subset of this JSON_SETTINGS object, using a path of the form "key1....
Definition kiid.h:46
The backing store for a PROJECT, in JSON format.
std::map< wxString, wxString > m_TextVars
bool m_BusAliasesDefined
An explicit alias table was loaded or set, including a deliberately empty table.
struct IP2581_BOM m_IP2581Bom
Layer pair list for the board.
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 bool empty(const wxTextEntryBase *aCtrl)
static const std::string ProjectFileExtension
std::string GetTestDataRootDir()
static bool readFile(const wxString &aFileName, wxString &aOut, size_t aLimit=0)
Read a file into aOut.
CITER next(CITER it)
Definition ptree.cpp:120
std::optional< wxString > sections
Table 4 section key, unset when the mode picks the sections.
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(Ipc2581ExplicitEmptySectionsRoundTrips)
BOOST_CHECK_EQUAL(result, "25.4")
wxString dump(const wxArrayString &aArray)
Debug helper for printing wxArrayString contents.
Definition of file extensions used in Kicad.