KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_nested_settings.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
31#include <settings/parameters.h>
34
35#include <wx/filename.h>
36#include <wx/stdpaths.h>
37#include <wx/utils.h>
38
39#include <fstream>
40
41
46{
47public:
49 JSON_SETTINGS( "test_parent", SETTINGS_LOC::NONE, 0, false, false, false )
50 {
51 }
52
53 bool IsModified() const { return m_modified; }
54 void ClearModified() { m_modified = false; }
55};
56
57
64{
65public:
66 TEST_NESTED_SETTINGS( JSON_SETTINGS* aParent, const std::string& aPath,
67 bool aResetParamsIfMissing = true ) :
68 NESTED_SETTINGS( "test_nested", 0, aParent, aPath, false ),
69 m_testValue( 0 ),
70 m_extraValue( 42 )
71 {
72 m_resetParamsIfMissing = aResetParamsIfMissing;
73
74 m_params.emplace_back( new PARAM<int>( "test_value", &m_testValue, 0 ) );
75
76 // Simulates a newly-added setting not present in old project files
77 m_params.emplace_back( new PARAM<int>( "extra_value", &m_extraValue, 42 ) );
78
79 // Load after params are registered (the real KiCad classes register params in
80 // the JSON_SETTINGS base ctor, which runs before NESTED_SETTINGS calls LoadFromFile)
82 }
83
85 {
86 // Prevent the base destructor from calling ReleaseNestedSettings so we can test
87 // that path explicitly
88 m_parent = nullptr;
89 }
90
93};
94
95
97{
98public:
100 {
101 // ReleaseNestedSettings requires m_manager to be non-null
102 m_parent.SetManager( reinterpret_cast<SETTINGS_MANAGER*>( 1 ) );
103 }
104
106};
107
108
109BOOST_FIXTURE_TEST_SUITE( NestedSettings, NESTED_SETTINGS_TEST_FIXTURE )
110
111
112
122BOOST_AUTO_TEST_CASE( ReleaseDoesNotMarkParentModified )
123{
124 // Seed the parent with partial nested data that only contains "test_value".
125 // This simulates an older project file that doesn't have the "extra_value" key.
126 nlohmann::json nestedJson = {
127 { "meta", { { "version", 0 } } },
128 { "test_value", 5 }
129 };
130
131 ( *m_parent.Internals() )["/test_nested"_json_pointer] = nestedJson;
132
133 BOOST_CHECK( !m_parent.IsModified() );
134
135 // Create nested settings that load from parent JSON
136 auto* nested = new TEST_NESTED_SETTINGS( &m_parent, "test_nested" );
137
138 BOOST_CHECK_EQUAL( nested->m_testValue, 5 );
139 BOOST_CHECK_EQUAL( nested->m_extraValue, 42 );
140
141 // Ensure the parent starts from a clean state
142 m_parent.ClearModified();
143
144 // Release the nested settings without any user-initiated modifications
145 m_parent.ReleaseNestedSettings( nested );
146
147 // The parent must NOT be marked modified. Before the fix for #22972, the extra_value
148 // param (absent from the original JSON) created a diff that falsely marked the parent.
149 BOOST_CHECK_MESSAGE( !m_parent.IsModified(),
150 "Parent should not be marked modified after releasing nested settings "
151 "when no intentional changes were made" );
152
153 delete nested;
154}
155
156
161BOOST_AUTO_TEST_CASE( ExplicitSaveDetectsModifications )
162{
163 nlohmann::json nestedJson = {
164 { "meta", { { "version", 0 } } },
165 { "test_value", 5 },
166 { "extra_value", 42 }
167 };
168
169 ( *m_parent.Internals() )["/test_nested"_json_pointer] = nestedJson;
170
171 auto* nested = new TEST_NESTED_SETTINGS( &m_parent, "test_nested" );
172
173 BOOST_CHECK_EQUAL( nested->m_testValue, 5 );
174
175 // Simulate a user-initiated change to the nested settings
176 nested->m_testValue = 99;
177
178 // SaveToFile on the nested settings should report the modification
179 bool modified = nested->SaveToFile();
180 BOOST_CHECK( modified );
181
182 // The parent JSON should now contain the updated value
183 auto parentJson = m_parent.GetJson( "test_nested.test_value" );
184
185 BOOST_CHECK( parentJson.has_value() );
186 BOOST_CHECK_EQUAL( parentJson->get<int>(), 99 );
187
188 m_parent.ReleaseNestedSettings( nested );
189 delete nested;
190}
191
192
197BOOST_AUTO_TEST_CASE( ReleaseFlushesDataToParent )
198{
199 nlohmann::json nestedJson = {
200 { "meta", { { "version", 0 } } },
201 { "test_value", 5 }
202 };
203
204 ( *m_parent.Internals() )["/test_nested"_json_pointer] = nestedJson;
205
206 auto* nested = new TEST_NESTED_SETTINGS( &m_parent, "test_nested" );
207
208 // Change a value in the nested settings
209 nested->m_testValue = 77;
210
211 m_parent.ClearModified();
212 m_parent.ReleaseNestedSettings( nested );
213
214 // Verify the changed value was flushed to the parent's in-memory JSON
215 auto parentJson = m_parent.GetJson( "test_nested.test_value" );
216
217 BOOST_CHECK( parentJson.has_value() );
218 BOOST_CHECK_EQUAL( parentJson->get<int>(), 77 );
219
220 delete nested;
221}
222
223
229BOOST_AUTO_TEST_CASE( AbsentDefaultIsModifiedWhenNotResetIfMissing )
230{
231 nlohmann::json nestedJson = {
232 { "meta", { { "version", 0 } } },
233 { "test_value", 5 }
234 };
235
236 ( *m_parent.Internals() )["/test_nested"_json_pointer] = nestedJson;
237
238 // extra_value is absent from the file; with reset-if-missing disabled it keeps its default.
239 auto* nested = new TEST_NESTED_SETTINGS( &m_parent, "test_nested", /* aResetParamsIfMissing */ false );
240
241 BOOST_CHECK_EQUAL( nested->m_extraValue, 42 );
242
243 // The absent default must be reported as a change so it is persisted.
244 BOOST_CHECK( nested->SaveToFile() );
245
246 auto parentJson = m_parent.GetJson( "test_nested.extra_value" );
247
248 BOOST_CHECK( parentJson.has_value() );
249 BOOST_CHECK_EQUAL( parentJson->get<int>(), 42 );
250
251 m_parent.ReleaseNestedSettings( nested );
252 delete nested;
253}
254
255
262{
263public:
264 MIGRATING_NESTED_SETTINGS( JSON_SETTINGS* aParent, const std::string& aPath ) :
265 NESTED_SETTINGS( "test_nested", 1, aParent, aPath, false )
266 {
267 registerMigration( 0, 1,
268 [this]()
269 {
270 Set<int>( "migrated_field", 7 );
271 return true;
272 } );
273
274 LoadFromFile();
275 }
276
278 {
279 m_parent = nullptr;
280 }
281};
282
283
289BOOST_AUTO_TEST_CASE( MigrationFlushesToParent )
290{
291 nlohmann::json nestedJson = {
292 { "meta", { { "version", 0 } } }
293 };
294
295 ( *m_parent.Internals() )["/test_nested"_json_pointer] = nestedJson;
296
297 auto* nested = new MIGRATING_NESTED_SETTINGS( &m_parent, "test_nested" );
298
299 // The migrator bumped the version and wrote migrated_field directly into the JSON.
300 BOOST_CHECK( nested->SaveToFile() );
301
302 auto version = m_parent.GetJson( "test_nested.meta.version" );
303 auto migrated = m_parent.GetJson( "test_nested.migrated_field" );
304
305 BOOST_CHECK( version.has_value() );
306 BOOST_CHECK_EQUAL( version->get<int>(), 1 );
307 BOOST_CHECK( migrated.has_value() );
308 BOOST_CHECK_EQUAL( migrated->get<int>(), 7 );
309
310 m_parent.ReleaseNestedSettings( nested );
311 delete nested;
312}
313
314
323{
324public:
325 DISK_SETTINGS( const wxString& aFilename ) :
326 JSON_SETTINGS( aFilename, SETTINGS_LOC::NONE, 0, true, true, true ),
327 m_value( 0 )
328 {
329 m_params.emplace_back( new PARAM<int>( "value", &m_value, 0 ) );
330 }
331
332 // Forces the next SaveToFile past the "modified" early-exit gate so that the new
333 // on-disk content comparison is exercised even when no params actually changed.
334 void ForceModified() { m_modified = true; }
335
337};
338
339
340BOOST_AUTO_TEST_CASE( SaveToFileSkipsWriteWhenContentsMatch )
341{
342 wxFileName tempDir( wxStandardPaths::Get().GetTempDir(), wxEmptyString );
343 wxString tempName = wxString::Format( wxT( "kicad_qa_24402_%ld" ), wxGetProcessId() );
344 tempDir.AppendDir( tempName );
345
346 BOOST_REQUIRE( wxFileName::Mkdir( tempDir.GetPath(), wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
347
348 wxFileName tempFile( tempDir.GetPath(), wxT( "test.json" ) );
349
350 DISK_SETTINGS settings( tempFile.GetFullPath() );
351 settings.SetManager( reinterpret_cast<SETTINGS_MANAGER*>( 1 ) );
352 settings.m_value = 7;
353
354 // First save creates the file
355 BOOST_REQUIRE( settings.SaveToFile( wxEmptyString, true ) );
356 BOOST_REQUIRE( tempFile.FileExists() );
357
358 // Capture the original mtime
359 wxDateTime originalModTime = tempFile.GetModificationTime();
360
361 // Make sure any subsequent write would produce a different mtime
362 wxMilliSleep( 1500 );
363
364 // Force the modified flag so the early "not modified, skipping save" exit is bypassed.
365 // The serialized payload will still match what is already on disk, so the new on-disk
366 // content comparison must be what prevents the write. This mimics the false-positive
367 // modified flag pattern observed in #24402.
368 settings.ForceModified();
369
370 bool result = settings.SaveToFile();
371
372 // SaveToFile returns false when it skips the write
373 BOOST_CHECK( !result );
374
375 wxFileName refreshed( tempFile.GetFullPath() );
376 wxDateTime newModTime = refreshed.GetModificationTime();
377
378 BOOST_CHECK_MESSAGE( originalModTime == newModTime,
379 "File mtime should not change when serialized contents match on-disk "
380 "contents" );
381
382 // Also verify that an actual content change still writes the file.
383 settings.m_value = 99;
384 settings.ForceModified();
385
386 BOOST_CHECK( settings.SaveToFile() );
387
388 wxFileName refreshedAfterChange( tempFile.GetFullPath() );
389 BOOST_CHECK( refreshedAfterChange.GetModificationTime() != originalModTime );
390
391 // Cleanup
392 wxRemoveFile( tempFile.GetFullPath() );
393 wxFileName::Rmdir( tempDir.GetPath() );
394}
395
396
402{
403public:
404 DISK_PARENT_SETTINGS( const wxString& aFilename ) :
405 JSON_SETTINGS( aFilename, SETTINGS_LOC::NONE, 0, true, true, true ),
406 m_ownValue( 0 )
407 {
408 m_params.emplace_back( new PARAM<int>( "own_value", &m_ownValue, 0 ) );
409 }
410
412};
413
414
422BOOST_AUTO_TEST_CASE( ReleasedNestedChangePersistsToDisk )
423{
424 wxFileName tempDir( wxStandardPaths::Get().GetTempDir(), wxEmptyString );
425 wxString tempName = wxString::Format( wxT( "kicad_qa_24402_nested_%ld" ), wxGetProcessId() );
426 tempDir.AppendDir( tempName );
427
428 BOOST_REQUIRE( wxFileName::Mkdir( tempDir.GetPath(), wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
429
430 wxFileName tempFile( tempDir.GetPath(), wxT( "parent.json" ) );
431
432 DISK_PARENT_SETTINGS parent( tempFile.GetFullPath() );
433 parent.SetManager( reinterpret_cast<SETTINGS_MANAGER*>( 1 ) );
434
435 auto* nested = new TEST_NESTED_SETTINGS( &parent, "test_nested" );
436 nested->m_testValue = 5;
437
438 // Persist the baseline so the on-disk content comparison has something to compare against.
439 BOOST_REQUIRE( parent.SaveToFile( wxEmptyString, true ) );
440 BOOST_REQUIRE( tempFile.FileExists() );
441
442 // Make a genuine change to the nested setting, then release it (editor close path) before the
443 // parent is saved. The parent's modified flag is intentionally cleared to model the case where
444 // only the nested data changed.
445 nested->m_testValue = 123;
446 parent.ReleaseNestedSettings( nested );
447 delete nested;
448
449 BOOST_REQUIRE( parent.SaveToFile() );
450
451 // Re-read the file from disk and confirm the changed nested value was written.
452 std::ifstream in( tempFile.GetFullPath().fn_str(), std::ios::in | std::ios::binary );
453 std::string contents( ( std::istreambuf_iterator<char>( in ) ),
454 std::istreambuf_iterator<char>() );
455 nlohmann::json onDisk = nlohmann::json::parse( contents );
456
457 BOOST_CHECK_EQUAL( onDisk["test_nested"]["test_value"].get<int>(), 123 );
458
459 wxRemoveFile( tempFile.GetFullPath() );
460 wxFileName::Rmdir( tempDir.GetPath() );
461}
462
463
469BOOST_AUTO_TEST_CASE( DefaultFillDoesNotChurnExistingFile )
470{
471 wxFileName tempDir( wxStandardPaths::Get().GetTempDir(), wxEmptyString );
472 wxString tempName = wxString::Format( wxT( "kicad_qa_24402_churn_%ld" ), wxGetProcessId() );
473 tempDir.AppendDir( tempName );
474
475 BOOST_REQUIRE( wxFileName::Mkdir( tempDir.GetPath(), wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
476
477 wxFileName tempFile( tempDir.GetPath(), wxT( "parent.json" ) );
478
479 DISK_PARENT_SETTINGS parent( tempFile.GetFullPath() );
480 parent.SetManager( reinterpret_cast<SETTINGS_MANAGER*>( 1 ) );
481
482 // Older file: the nested block has test_value but not the newer extra_value param.
483 nlohmann::json nestedJson = { { "meta", { { "version", 0 } } }, { "test_value", 5 } };
484 ( *parent.Internals() )["/test_nested"_json_pointer] = nestedJson;
485 BOOST_REQUIRE( parent.SaveToFile( wxEmptyString, true ) );
486
487 std::ifstream in0( tempFile.GetFullPath().fn_str(), std::ios::binary );
488 std::string before( ( std::istreambuf_iterator<char>( in0 ) ),
489 std::istreambuf_iterator<char>() );
490
491 BOOST_REQUIRE( before.find( "extra_value" ) == std::string::npos );
492
493 // Open and close the nested settings with no user change (the extra_value default loads).
494 auto* nested = new TEST_NESTED_SETTINGS( &parent, "test_nested" );
495 parent.ReleaseNestedSettings( nested );
496 delete nested;
497
498 // No genuine change, so the parent save must be a no-op.
499 BOOST_CHECK( !parent.SaveToFile() );
500
501 std::ifstream in1( tempFile.GetFullPath().fn_str(), std::ios::binary );
502 std::string after( ( std::istreambuf_iterator<char>( in1 ) ),
503 std::istreambuf_iterator<char>() );
504
505 BOOST_CHECK_MESSAGE( before == after,
506 "Releasing an unchanged nested setting must not add default keys or "
507 "rewrite the existing file" );
508
509 wxRemoveFile( tempFile.GetFullPath() );
510 wxFileName::Rmdir( tempDir.GetPath() );
511}
512
513
A disk-backed parent that carries a nested setting, mirroring the project file / nested board or ERC ...
DISK_PARENT_SETTINGS(const wxString &aFilename)
Verify that SaveToFile does not rewrite the on-disk file when its serialized contents match what alre...
DISK_SETTINGS(const wxString &aFilename)
void Set(const std::string &aPath, ValueType aVal)
Stores a value into the JSON document Will throw an exception if ValueType isn't something that the l...
bool m_modified
True if the JSON data store has been written to since the last file write.
void SetManager(SETTINGS_MANAGER *aManager)
std::vector< PARAM_BASE * > m_params
The list of parameters (owned by this object)
void registerMigration(int aOldSchemaVersion, int aNewSchemaVersion, std::function< bool(void)> aMigrator)
Registers a migration from one schema version to another.
JSON_SETTINGS_INTERNALS * Internals()
void ReleaseNestedSettings(NESTED_SETTINGS *aSettings)
Saves and frees a nested settings object, if it exists within this one.
JSON_SETTINGS(const wxString &aFilename, SETTINGS_LOC aLocation, int aSchemaVersion)
virtual bool SaveToFile(const wxString &aDirectory="", bool aForce=false)
Calls Store() and then writes the contents of the JSON document to a file.
bool m_resetParamsIfMissing
Whether or not to set parameters to their default value if missing from JSON on Load()
A nested settings subclass that migrates a v0 block to v1 by writing a key directly into the JSON,...
MIGRATING_NESTED_SETTINGS(JSON_SETTINGS *aParent, const std::string &aPath)
JSON_SETTINGS * m_parent
A pointer to the parent object to load and store from.
NESTED_SETTINGS(const std::string &aName, int aSchemaVersion, JSON_SETTINGS *aParent, const std::string &aPath, bool aLoadFromFile=true)
bool LoadFromFile(const wxString &aDirectory="") override
Loads the JSON document from the parent and then calls Load()
A test-accessible JSON_SETTINGS subclass that exposes the protected m_modified flag.
A minimal nested settings subclass for testing.
TEST_NESTED_SETTINGS(JSON_SETTINGS *aParent, const std::string &aPath, bool aResetParamsIfMissing=true)
@ NONE
Definition eda_shape.h:72
SETTINGS_LOC
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
BOOST_AUTO_TEST_CASE(ReleaseDoesNotMarkParentModified)
Verify that releasing nested settings does not mark the parent as modified.
BOOST_CHECK_MESSAGE(totalMismatches==0, std::to_string(totalMismatches)+" board(s) with strategy disagreements")
wxString result
Test unit parsing edge cases and error handling.
BOOST_CHECK_EQUAL(result, "25.4")