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_tempDir( fs::temp_directory_path() / "kicad_settings_manager_test" )
69 {
70 std::error_code ec;
71 fs::remove_all( m_tempDir, ec );
72
73 // Throwing overload so an unusable scratch directory fails setup loudly
74 fs::create_directories( m_tempDir );
75 }
76
78 {
79 std::error_code ec;
80 fs::remove_all( m_tempDir, ec );
81 }
82
83 wxString Path( const std::string& aName ) const
84 {
85 return wxString( ( m_tempDir / aName ).string() );
86 }
87
88 fs::path m_tempDir;
89};
90
91
92BOOST_FIXTURE_TEST_SUITE( SettingsManager, SETTINGS_MANAGER_FIXTURE )
93
94
95// Load() may run after a settings object was edited in memory but not yet written; the pending
96// edit must be flushed before reloading or the stale on-disk copy silently discards it.
97BOOST_AUTO_TEST_CASE( LoadFlushesDirtySettings )
98{
100
101 // Drop the auto-registered common settings so Load() only touches the scratch object
102 mgr.FlushAndRelease( mgr.GetCommonSettings(), false );
103
105 mgr.RegisterSettings( new FLUSH_TEST_SETTINGS( Path( "dirty" ) ), false );
106
107 cfg->SaveToFile();
108 cfg->m_value = 42;
109
110 mgr.Load();
111
112 BOOST_CHECK_EQUAL( cfg->m_value, 42 );
113
114 // The dirty value must have reached disk, not merely survived in memory
115 FLUSH_TEST_SETTINGS fresh( Path( "dirty" ) );
116 fresh.LoadFromFile();
117 BOOST_CHECK_EQUAL( fresh.m_value, 42 );
118}
119
120
121// A registered object that has never been synchronized with its file must not be flushed by
122// Load(); flushing would overwrite the file with construction state before it is ever read.
123BOOST_AUTO_TEST_CASE( LoadDoesNotFlushNeverSyncedSettings )
124{
125 {
126 FLUSH_TEST_SETTINGS seed( Path( "cold" ) );
127 seed.m_value = 7;
128 seed.SaveToFile();
129 }
130
132 mgr.FlushAndRelease( mgr.GetCommonSettings(), false );
133
135 mgr.RegisterSettings( new FLUSH_TEST_SETTINGS( Path( "cold" ) ), false );
136
137 mgr.Load();
138
139 BOOST_CHECK_EQUAL( cfg->m_value, 7 );
140
141 FLUSH_TEST_SETTINGS fresh( Path( "cold" ) );
142 fresh.LoadFromFile();
143 BOOST_CHECK_EQUAL( fresh.m_value, 7 );
144}
145
146
147// An incomplete color theme (missing keys added by a newer build) must not be rewritten merely
148// to inject default colors when the user made no change, mirroring the .kicad_pro guarantee.
149//
150// Regression test for https://gitlab.com/kicad/code/kicad/-/issues/24402
151BOOST_AUTO_TEST_CASE( ColorThemeNotRewrittenWhenUnchanged )
152{
153 // Canonical theme written with KiCad's own writer so the reload round-trip is clean.
154 {
155 COLOR_SETTINGS seed( Path( "theme" ), true );
156 seed.SaveToFile( wxEmptyString, true );
157 }
158
159 fs::path themePath = m_tempDir / "theme.json";
160
161 auto readFile = []( const fs::path& aPath )
162 {
163 std::ifstream in( aPath );
164 std::stringstream buffer;
165 buffer << in.rdbuf();
166 return buffer.str();
167 };
168
169 // Drop a whole colored section so the file mimics a theme saved before those colors existed.
170 // Their in-memory values load as defaults, so a no-op load must not resurrect them.
171 {
172 nlohmann::json js = nlohmann::json::parse( readFile( themePath ) );
173 BOOST_REQUIRE( js.contains( "gerbview" ) );
174 js.erase( "gerbview" );
175
176 std::ofstream out( themePath );
177 out << std::setw( 2 ) << js << std::endl;
178 out.close();
179 }
180
181 std::string before = readFile( themePath );
182
183 COLOR_SETTINGS cfg( Path( "theme" ), true );
184 cfg.LoadFromFile();
185
186 BOOST_CHECK( !cfg.SaveToFile( wxEmptyString ) );
187 BOOST_CHECK_EQUAL( before, readFile( themePath ) );
188}
189
190
191// History compare joins a native temp dir with "/" and then looked the project up by that spelling
192BOOST_AUTO_TEST_CASE( GetProjectMatchesAnySpellingOfTheLoadedPath )
193{
194 wxString projectPath = Path( "spelling.kicad_pro" );
195
196 {
197 std::ofstream out( projectPath.ToStdString() );
198 out << R"({"meta": {"filename": "spelling.kicad_pro", "version": 3}})";
199 }
200
202 BOOST_REQUIRE( mgr.LoadProject( projectPath, false ) );
203
204 // A doubled separator is a second spelling on every platform, not only on Windows
205 PROJECT* project = mgr.GetProject( wxString( m_tempDir.string() ) + wxS( "//spelling.kicad_pro" ) );
206
208 BOOST_CHECK_EQUAL( project->GetProjectFullName(), projectPath );
209}
210
211
212// Writes a loadable project plus a lock file owned by aOwner, and returns the project path.
213static wxString seedLockedProject( const fs::path& aDir, const std::string& aName,
214 const nlohmann::json& aOwner )
215{
216 fs::path pro = aDir / ( aName + ".kicad_pro" );
217
218 {
219 std::ofstream out( pro.string() );
220 out << R"({"meta": {"filename": ")" << aName << R"(.kicad_pro", "version": 3}})";
221 }
222
223 std::ofstream lck( LOCKFILE::LockPathFor( wxString( pro.string() ) ).ToStdString() );
224 lck << aOwner.dump();
225
226 return wxString( pro.string() );
227}
228
229
230static nlohmann::json selfOwnerRecord()
231{
232 nlohmann::json owner;
233 owner["username"] = std::string( wxGetUserId().mb_str() );
234 owner["hostname"] = std::string( wxGetHostName().mb_str() );
235 owner["token"] = "0123456789abcdef0123456789abcdef";
236 return owner;
237}
238
239
240// Issue #11458 - a crash-orphaned self lock must be reclaimed, not leave the project read-only
241BOOST_AUTO_TEST_CASE( StaleOwnProjectLockIsReclaimedOnLoad )
242{
243 // No OS lock is held, the state a crash leaves behind
244 wxString projectPath = seedLockedProject( m_tempDir, "stale", selfOwnerRecord() );
245
247 BOOST_REQUIRE( mgr.LoadProject( projectPath ) );
248
249 PROJECT* project = mgr.GetProject( projectPath );
251
252 BOOST_CHECK( !project->IsReadOnly() );
253 BOOST_CHECK( project->GetProjectLock() != nullptr );
254 BOOST_CHECK( wxFileName::FileExists( LOCKFILE::LockPathFor( projectPath ) ) );
255}
256
257
258// Foreign lock is the negative control - the project must still open read-only, untouched
259BOOST_AUTO_TEST_CASE( ForeignProjectLockOpensReadOnlyAndIsNotStolen )
260{
261 nlohmann::json owner;
262 owner["username"] = "someone-else";
263 owner["hostname"] = "another-host";
264
265 wxString projectPath = seedLockedProject( m_tempDir, "locked", owner );
266
268 BOOST_REQUIRE( mgr.LoadProject( projectPath ) );
269
270 PROJECT* project = mgr.GetProject( projectPath );
272
273 // A lock we cannot take degrades to read-only, never to refusing the project
274 BOOST_CHECK( project->IsReadOnly() );
275
276 BOOST_REQUIRE( wxFileName::FileExists( LOCKFILE::LockPathFor( projectPath ) ) );
277
278 LOCKFILE reread( projectPath );
279 BOOST_CHECK_EQUAL( reread.GetUsername(), wxString( "someone-else" ) );
280 BOOST_CHECK_EQUAL( reread.GetHostname(), wxString( "another-host" ) );
281}
282
283
284// A live same-user lock held by another KiCad process must never be taken
285BOOST_AUTO_TEST_CASE( LiveProjectLockNotStolenFromAnotherExecutable )
286{
287 wxString projectPath = seedLockedProject( m_tempDir, "live", selfOwnerRecord() );
288
290 bool created = false;
291
292 BOOST_REQUIRE( owner.Acquire( LOCKFILE::LockPathFor( projectPath ), created )
294
296 BOOST_REQUIRE( mgr.LoadProject( projectPath ) );
297
298 PROJECT* project = mgr.GetProject( projectPath );
300
301 BOOST_CHECK( project->IsReadOnly() );
302
303 BOOST_REQUIRE( wxFileName::FileExists( LOCKFILE::LockPathFor( projectPath ) ) );
304
305 std::ifstream in( LOCKFILE::LockPathFor( projectPath ).ToStdString() );
306 BOOST_CHECK_EQUAL( nlohmann::json::parse( in ).value( "token", std::string() ),
307 std::string( "0123456789abcdef0123456789abcdef" ) );
308}
309
310
311class TEST_KIWAY : public KIWAY
312{
313public:
316 m_manager( aManager )
317 {
318 }
319
320 void ProjectChanged() override
321 {
322 m_notified = true;
323 m_lockHeldWhenNotified = m_manager.Prj().GetProjectLock() != nullptr;
324 }
325
326 bool Notified() const { return m_notified; }
328
329private:
331 bool m_notified = false;
333};
334
335
336BOOST_AUTO_TEST_CASE( ProjectOwnsItsLockBeforeTheChangeIsAnnounced )
337{
338 fs::path pro = m_tempDir / "unversioned.kicad_pro";
339
340 {
341 std::ofstream out( pro.string() );
342 out << "{}";
343 }
344
345 wxString projectPath = wxString( pro.string() );
346
348 TEST_KIWAY kiway( mgr );
349
350 mgr.SetKiway( &kiway );
351 mgr.LoadProject( projectPath );
352
353 PROJECT* project = mgr.GetProject( projectPath );
355 BOOST_REQUIRE( kiway.Notified() );
356
357 BOOST_CHECK_MESSAGE( kiway.LockHeldWhenNotified(), "The project must own its lock before the change is announced" );
358
359 BOOST_CHECK( project->GetProjectLock() != nullptr );
360 BOOST_CHECK( wxFileName::FileExists( LOCKFILE::LockPathFor( projectPath ) ) );
361}
362
363
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:50
Advisory lock over a file, taken by writing a sibling lock file and holding an exclusive lock on it f...
Definition lockfile.h:60
static wxString LockPathFor(const wxString &aFilename)
Definition lockfile.h:140
wxString GetUsername()
Definition lockfile.h:220
wxString GetHostname()
Definition lockfile.h:226
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
File locking utilities.
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")