KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_settings_manager.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 modify it
7 * under the terms of the GNU General Public License as published by the
8 * Free Software Foundation, either version 3 of the License, or (at your
9 * option) any later version.
10 *
11 * This program is distributed in the hope that it will be useful, but
12 * WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 * 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
26
30#include <settings/parameters.h>
32
33#include <json_common.h>
34#include <kiplatform/io.h>
35#include <kiway.h>
36#include <lockfile.h>
37#include <project.h>
38
39#include <wx/filename.h>
40
41#include <filesystem>
42#include <fstream>
43#include <sstream>
44#include <system_error>
45
46namespace fs = std::filesystem;
47
48
49// Backed by a caller-controlled absolute path (SETTINGS_LOC::NONE) so the tests persist and
50// reload from a scratch directory without touching the shared config corpus.
52{
53public:
54 FLUSH_TEST_SETTINGS( const wxString& aFullPath ) :
55 JSON_SETTINGS( aFullPath, SETTINGS_LOC::NONE, 1 ),
56 m_value( 0 )
57 {
58 m_params.emplace_back( new PARAM<int>( "test.value", &m_value, 0 ) );
59 }
60
62};
63
64
66{
68 m_originalDir( fs::current_path() ),
69 m_tempDir( fs::temp_directory_path() / "kicad_settings_manager_test" )
70 {
71 std::error_code ec;
72 fs::remove_all( m_tempDir, ec );
73
74 // Throwing overload so an unusable scratch directory fails setup loudly
75 fs::create_directories( m_tempDir );
76 }
77
79 {
80 fs::current_path( m_originalDir );
81 std::error_code ec;
82 fs::remove_all( m_tempDir, ec );
83 }
84
85 wxString Path( const std::string& aName ) const
86 {
87 return wxString( ( m_tempDir / aName ).string() );
88 }
89
90 fs::path m_originalDir;
91 fs::path m_tempDir;
92};
93
94
95BOOST_FIXTURE_TEST_SUITE( SettingsManager, SETTINGS_MANAGER_FIXTURE )
96
97
98// Load() may run after a settings object was edited in memory but not yet written; the pending
99// edit must be flushed before reloading or the stale on-disk copy silently discards it.
100BOOST_AUTO_TEST_CASE( LoadFlushesDirtySettings )
101{
103
104 // Drop the auto-registered common settings so Load() only touches the scratch object
105 mgr.FlushAndRelease( mgr.GetCommonSettings(), false );
106
108 mgr.RegisterSettings( new FLUSH_TEST_SETTINGS( Path( "dirty" ) ), false );
109
110 cfg->SaveToFile();
111 cfg->m_value = 42;
112
113 mgr.Load();
114
115 BOOST_CHECK_EQUAL( cfg->m_value, 42 );
116
117 // The dirty value must have reached disk, not merely survived in memory
118 FLUSH_TEST_SETTINGS fresh( Path( "dirty" ) );
119 fresh.LoadFromFile();
120 BOOST_CHECK_EQUAL( fresh.m_value, 42 );
121}
122
123
124// A registered object that has never been synchronized with its file must not be flushed by
125// Load(); flushing would overwrite the file with construction state before it is ever read.
126BOOST_AUTO_TEST_CASE( LoadDoesNotFlushNeverSyncedSettings )
127{
128 {
129 FLUSH_TEST_SETTINGS seed( Path( "cold" ) );
130 seed.m_value = 7;
131 seed.SaveToFile();
132 }
133
135 mgr.FlushAndRelease( mgr.GetCommonSettings(), false );
136
138 mgr.RegisterSettings( new FLUSH_TEST_SETTINGS( Path( "cold" ) ), false );
139
140 mgr.Load();
141
142 BOOST_CHECK_EQUAL( cfg->m_value, 7 );
143
144 FLUSH_TEST_SETTINGS fresh( Path( "cold" ) );
145 fresh.LoadFromFile();
146 BOOST_CHECK_EQUAL( fresh.m_value, 7 );
147}
148
149
150// An incomplete color theme (missing keys added by a newer build) must not be rewritten merely
151// to inject default colors when the user made no change, mirroring the .kicad_pro guarantee.
152//
153// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/24402
154BOOST_AUTO_TEST_CASE( ColorThemeNotRewrittenWhenUnchanged )
155{
156 // Canonical theme written with KiCad's own writer so the reload round-trip is clean.
157 {
158 COLOR_SETTINGS seed( Path( "theme" ), true );
159 seed.SaveToFile( wxEmptyString, true );
160 }
161
162 fs::path themePath = m_tempDir / "theme.json";
163
164 auto readFile = []( const fs::path& aPath )
165 {
166 std::ifstream in( aPath );
167 std::stringstream buffer;
168 buffer << in.rdbuf();
169 return buffer.str();
170 };
171
172 // Drop a whole colored section so the file mimics a theme saved before those colors existed.
173 // Their in-memory values load as defaults, so a no-op load must not resurrect them.
174 {
175 nlohmann::json js = nlohmann::json::parse( readFile( themePath ) );
176 BOOST_REQUIRE( js.contains( "gerbview" ) );
177 js.erase( "gerbview" );
178
179 std::ofstream out( themePath );
180 out << std::setw( 2 ) << js << std::endl;
181 out.close();
182 }
183
184 std::string before = readFile( themePath );
185
186 COLOR_SETTINGS cfg( Path( "theme" ), true );
187 cfg.LoadFromFile();
188
189 BOOST_CHECK( !cfg.SaveToFile( wxEmptyString ) );
190 BOOST_CHECK_EQUAL( before, readFile( themePath ) );
191}
192
193
194// History compare joins a native temp dir with "/" and then looked the project up by that spelling
195BOOST_AUTO_TEST_CASE( GetProjectMatchesAnySpellingOfTheLoadedPath )
196{
197 wxString projectPath = Path( "spelling.kicad_pro" );
198
199 {
200 std::ofstream out( projectPath.ToStdString() );
201 out << R"({"meta": {"filename": "spelling.kicad_pro", "version": 3}})";
202 }
203
205 BOOST_REQUIRE( mgr.LoadProject( projectPath, false ) );
206
207 // A doubled separator is a second spelling on every platform, not only on Windows
208 PROJECT* project = mgr.GetProject( wxString( m_tempDir.string() ) + wxS( "//spelling.kicad_pro" ) );
209
211 BOOST_CHECK_EQUAL( project->GetProjectFullName(), projectPath );
212}
213
214
215// Writes a loadable project plus a lock file owned by aOwner, and returns the project path.
216static wxString seedLockedProject( const fs::path& aDir, const std::string& aName,
217 const nlohmann::json& aOwner )
218{
219 fs::path pro = aDir / ( aName + ".kicad_pro" );
220
221 {
222 std::ofstream out( pro.string() );
223 out << R"({"meta": {"filename": ")" << aName << R"(.kicad_pro", "version": 3}})";
224 }
225
226 std::ofstream lck( LOCKFILE::LockPathFor( wxString( pro.string() ) ).ToStdString() );
227 lck << aOwner.dump();
228
229 return wxString( pro.string() );
230}
231
232
233static nlohmann::json selfOwnerRecord()
234{
235 nlohmann::json owner;
236 owner["username"] = std::string( wxGetUserId().mb_str() );
237 owner["hostname"] = std::string( wxGetHostName().mb_str() );
238 owner["token"] = "0123456789abcdef0123456789abcdef";
239 return owner;
240}
241
242
243// Issue #11458 - a crash-orphaned self lock must be reclaimed, not leave the project read-only
244BOOST_AUTO_TEST_CASE( StaleOwnProjectLockIsReclaimedOnLoad )
245{
246 // No OS lock is held, the state a crash leaves behind
247 wxString projectPath = seedLockedProject( m_tempDir, "stale", selfOwnerRecord() );
248
250 BOOST_REQUIRE( mgr.LoadProject( projectPath ) );
251
252 PROJECT* project = mgr.GetProject( projectPath );
254
255 BOOST_CHECK( !project->IsReadOnly() );
256 BOOST_CHECK( project->GetProjectLock() != nullptr );
257 BOOST_CHECK( wxFileName::FileExists( LOCKFILE::LockPathFor( projectPath ) ) );
258}
259
260
261// Foreign lock is the negative control - the project must still open read-only, untouched
262BOOST_AUTO_TEST_CASE( ForeignProjectLockOpensReadOnlyAndIsNotStolen )
263{
264 nlohmann::json owner;
265 owner["username"] = "someone-else";
266 owner["hostname"] = "another-host";
267
268 wxString projectPath = seedLockedProject( m_tempDir, "locked", owner );
269
271 BOOST_REQUIRE( mgr.LoadProject( projectPath ) );
272
273 PROJECT* project = mgr.GetProject( projectPath );
275
276 // A lock we cannot take degrades to read-only, never to refusing the project
277 BOOST_CHECK( project->IsReadOnly() );
278
279 BOOST_REQUIRE( wxFileName::FileExists( LOCKFILE::LockPathFor( projectPath ) ) );
280
281 LOCKFILE reread( projectPath );
282 BOOST_CHECK_EQUAL( reread.GetUsername(), wxString( "someone-else" ) );
283 BOOST_CHECK_EQUAL( reread.GetHostname(), wxString( "another-host" ) );
284}
285
286
287// A live same-user lock held by another KiCad process must never be taken
288BOOST_AUTO_TEST_CASE( LiveProjectLockNotStolenFromAnotherExecutable )
289{
290 wxString projectPath = seedLockedProject( m_tempDir, "live", selfOwnerRecord() );
291
293 bool created = false;
294
295 BOOST_REQUIRE( owner.Acquire( LOCKFILE::LockPathFor( projectPath ), created )
297
299 BOOST_REQUIRE( mgr.LoadProject( projectPath ) );
300
301 PROJECT* project = mgr.GetProject( projectPath );
303
304 BOOST_CHECK( project->IsReadOnly() );
305
306 BOOST_REQUIRE( wxFileName::FileExists( LOCKFILE::LockPathFor( projectPath ) ) );
307
308 std::ifstream in( LOCKFILE::LockPathFor( projectPath ).ToStdString() );
309 BOOST_CHECK_EQUAL( nlohmann::json::parse( in ).value( "token", std::string() ),
310 std::string( "0123456789abcdef0123456789abcdef" ) );
311}
312
313
314class TEST_KIWAY : public KIWAY
315{
316public:
319 m_manager( aManager )
320 {
321 }
322
323 void ProjectChanged() override
324 {
325 m_notified = true;
326 m_lockHeldWhenNotified = m_manager.Prj().GetProjectLock() != nullptr;
327 }
328
329 bool Notified() const { return m_notified; }
331
332private:
334 bool m_notified = false;
336};
337
338
339BOOST_AUTO_TEST_CASE( ProjectOwnsItsLockBeforeTheChangeIsAnnounced )
340{
341 fs::path pro = m_tempDir / "unversioned.kicad_pro";
342
343 {
344 std::ofstream out( pro.string() );
345 out << "{}";
346 }
347
348 wxString projectPath = wxString( pro.string() );
349
351 TEST_KIWAY kiway( mgr );
352
353 mgr.SetKiway( &kiway );
354 mgr.LoadProject( projectPath );
355
356 PROJECT* project = mgr.GetProject( projectPath );
358 BOOST_REQUIRE( kiway.Notified() );
359
360 BOOST_CHECK_MESSAGE( kiway.LockHeldWhenNotified(), "The project must own its lock before the change is announced" );
361
362 BOOST_CHECK( project->GetProjectLock() != nullptr );
363 BOOST_CHECK( wxFileName::FileExists( LOCKFILE::LockPathFor( projectPath ) ) );
364}
365
366
Color settings are a bit different than most of the settings objects in that there can be more than o...
FLUSH_TEST_SETTINGS(const wxString &aFullPath)
virtual bool LoadFromFile(const wxString &aDirectory="")
Loads the backing file from disk and then calls Load()
std::vector< PARAM_BASE * > m_params
The list of parameters (owned by this object)
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.
An exclusive advisory lock on a file, held for the lifetime of this object.
Definition io.h:91
@ HELD
We hold the lock.
Definition io.h:96
STATE Acquire(const wxString &aPath, bool &aCreated)
Open aPath, creating it if it does not exist, and try to take the lock without ever blocking on it.
KIWAY(int aCtlBits, wxFrame *aTop=nullptr)
Definition kiway.cpp:51
Advisory lock over a file, taken by writing a sibling lock file and holding an exclusive lock on it f...
Definition lockfile.h:54
static wxString LockPathFor(const wxString &aFilename)
Definition lockfile.h:134
wxString GetUsername()
Definition lockfile.h:214
wxString GetHostname()
Definition lockfile.h:220
Container for project specific data.
Definition project.h:63
T * RegisterSettings(T *aSettings, bool aLoadNow=true)
Take ownership of the pointer passed in.
COMMON_SETTINGS * GetCommonSettings() const
Retrieve the common settings shared by all applications.
bool LoadProject(const wxString &aFullPath, bool aSetActive=true)
Load a project or sets up a new project with a specified path.
void SetKiway(KIWAY *aKiway)
Associate this setting manager with the given Kiway.
PROJECT * GetProject(const wxString &aFullPath) const
Retrieve a loaded project by name.
void FlushAndRelease(JSON_SETTINGS *aSettings, bool aSave=true)
If the given settings object is registered, save it to disk and unregister it.
void ProjectChanged() override
Calls ProjectChanged() on all KIWAY_PLAYERs.
bool LockHeldWhenNotified() const
SETTINGS_MANAGER & m_manager
TEST_KIWAY(SETTINGS_MANAGER &aManager)
static std::string ToStdString(const wxString &aStr)
@ NONE
Definition eda_fill.h:42
SETTINGS_LOC
#define KFCTL_STANDALONE
Running as a standalone Top.
Definition kiway.h:174
static bool readFile(const wxString &aFileName, wxString &aOut, size_t aLimit=0)
Read a file into aOut.
wxString Path(const std::string &aName) const
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
BOOST_AUTO_TEST_CASE(LoadFlushesDirtySettings)
static wxString seedLockedProject(const fs::path &aDir, const std::string &aName, const nlohmann::json &aOwner)
static nlohmann::json selfOwnerRecord()
BOOST_CHECK_EQUAL(result, "25.4")