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// Writes a loadable project plus a lock file owned by aOwner, and returns the project path.
192static wxString seedLockedProject( const fs::path& aDir, const std::string& aName,
193 const nlohmann::json& aOwner )
194{
195 fs::path pro = aDir / ( aName + ".kicad_pro" );
196
197 {
198 std::ofstream out( pro.string() );
199 out << R"({"meta": {"filename": ")" << aName << R"(.kicad_pro", "version": 3}})";
200 }
201
202 std::ofstream lck( LOCKFILE::LockPathFor( wxString( pro.string() ) ).ToStdString() );
203 lck << aOwner.dump();
204
205 return wxString( pro.string() );
206}
207
208
209static nlohmann::json selfOwnerRecord()
210{
211 nlohmann::json owner;
212 owner["username"] = std::string( wxGetUserId().mb_str() );
213 owner["hostname"] = std::string( wxGetHostName().mb_str() );
214 owner["token"] = "0123456789abcdef0123456789abcdef";
215 return owner;
216}
217
218
219// Issue #11458 - a crash-orphaned self lock must be reclaimed, not leave the project read-only
220BOOST_AUTO_TEST_CASE( StaleOwnProjectLockIsReclaimedOnLoad )
221{
222 // No OS lock is held, the state a crash leaves behind
223 wxString projectPath = seedLockedProject( m_tempDir, "stale", selfOwnerRecord() );
224
226 BOOST_REQUIRE( mgr.LoadProject( projectPath ) );
227
228 PROJECT* project = mgr.GetProject( projectPath );
230
231 BOOST_CHECK( !project->IsReadOnly() );
232 BOOST_CHECK( project->GetProjectLock() != nullptr );
233 BOOST_CHECK( wxFileName::FileExists( LOCKFILE::LockPathFor( projectPath ) ) );
234}
235
236
237// Foreign lock is the negative control - the project must still open read-only, untouched
238BOOST_AUTO_TEST_CASE( ForeignProjectLockOpensReadOnlyAndIsNotStolen )
239{
240 nlohmann::json owner;
241 owner["username"] = "someone-else";
242 owner["hostname"] = "another-host";
243
244 wxString projectPath = seedLockedProject( m_tempDir, "locked", owner );
245
247 BOOST_REQUIRE( mgr.LoadProject( projectPath ) );
248
249 PROJECT* project = mgr.GetProject( projectPath );
251
252 // A lock we cannot take degrades to read-only, never to refusing the project
253 BOOST_CHECK( project->IsReadOnly() );
254
255 BOOST_REQUIRE( wxFileName::FileExists( LOCKFILE::LockPathFor( projectPath ) ) );
256
257 LOCKFILE reread( projectPath );
258 BOOST_CHECK_EQUAL( reread.GetUsername(), wxString( "someone-else" ) );
259 BOOST_CHECK_EQUAL( reread.GetHostname(), wxString( "another-host" ) );
260}
261
262
263// A live same-user lock held by another KiCad process must never be taken
264BOOST_AUTO_TEST_CASE( LiveProjectLockNotStolenFromAnotherExecutable )
265{
266 wxString projectPath = seedLockedProject( m_tempDir, "live", selfOwnerRecord() );
267
269 bool created = false;
270
271 BOOST_REQUIRE( owner.Acquire( LOCKFILE::LockPathFor( projectPath ), created )
273
275 BOOST_REQUIRE( mgr.LoadProject( projectPath ) );
276
277 PROJECT* project = mgr.GetProject( projectPath );
279
280 BOOST_CHECK( project->IsReadOnly() );
281
282 BOOST_REQUIRE( wxFileName::FileExists( LOCKFILE::LockPathFor( projectPath ) ) );
283
284 std::ifstream in( LOCKFILE::LockPathFor( projectPath ).ToStdString() );
285 BOOST_CHECK_EQUAL( nlohmann::json::parse( in ).value( "token", std::string() ),
286 std::string( "0123456789abcdef0123456789abcdef" ) );
287}
288
289
290class TEST_KIWAY : public KIWAY
291{
292public:
295 m_manager( aManager )
296 {
297 }
298
299 void ProjectChanged() override
300 {
301 m_notified = true;
302 m_lockHeldWhenNotified = m_manager.Prj().GetProjectLock() != nullptr;
303 }
304
305 bool Notified() const { return m_notified; }
307
308private:
310 bool m_notified = false;
312};
313
314
315BOOST_AUTO_TEST_CASE( ProjectOwnsItsLockBeforeTheChangeIsAnnounced )
316{
317 fs::path pro = m_tempDir / "unversioned.kicad_pro";
318
319 {
320 std::ofstream out( pro.string() );
321 out << "{}";
322 }
323
324 wxString projectPath = wxString( pro.string() );
325
327 TEST_KIWAY kiway( mgr );
328
329 mgr.SetKiway( &kiway );
330 mgr.LoadProject( projectPath );
331
332 PROJECT* project = mgr.GetProject( projectPath );
334 BOOST_REQUIRE( kiway.Notified() );
335
336 BOOST_CHECK_MESSAGE( kiway.LockHeldWhenNotified(), "The project must own its lock before the change is announced" );
337
338 BOOST_CHECK( project->GetProjectLock() != nullptr );
339 BOOST_CHECK( wxFileName::FileExists( LOCKFILE::LockPathFor( projectPath ) ) );
340}
341
342
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")