KiCad PCB EDA Suite
Loading...
Searching...
No Matches
test_atomic_save.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 along
17 * with this program. If not, see <http://www.gnu.org/licenses/>.
18 */
19
20#include <boost/test/unit_test.hpp>
21
23#include <ki_exception.h>
24#include <kiplatform/io.h>
25#include <richio.h>
26
27#include <wx/dir.h>
28#include <wx/ffile.h>
29#include <wx/filefn.h>
30#include <wx/filename.h>
31#include <wx/stdpaths.h>
32#include <wx/stopwatch.h>
33
34#include <cstdio>
35#include <string>
36
37#if defined( _WIN32 )
38#include <windows.h>
39#else
40#include <sys/stat.h>
41#include <sys/types.h>
42#endif
43
44
45namespace
46{
47
48wxString makeTempTargetPath( const wxString& aTag )
49{
50 wxString tempDir = wxFileName::GetTempDir();
51 wxString leaf = wxString::Format( wxT( "kicad-atomicsave-%s-%ld" ), aTag,
52 static_cast<long>( wxGetLocalTimeMillis().GetValue() ) );
53 return tempDir + wxFileName::GetPathSeparator() + leaf;
54}
55
56
57void writeFileContents( const wxString& aPath, const std::string& aContent )
58{
59 wxFFile fp( aPath, wxT( "wb" ) );
60 BOOST_REQUIRE( fp.IsOpened() );
61
62 if( !aContent.empty() )
63 BOOST_REQUIRE( fp.Write( aContent.data(), aContent.size() ) == aContent.size() );
64
65 fp.Close();
66}
67
68
69std::string readFileContents( const wxString& aPath )
70{
71 wxFFile fp( aPath, wxT( "rb" ) );
72
73 if( !fp.IsOpened() )
74 return std::string();
75
76 wxString buf;
77 fp.ReadAll( &buf );
78 std::string out = std::string( buf.mb_str( wxConvUTF8 ) );
79 fp.Close();
80 return out;
81}
82
83
84// Counts remaining sibling temp files matching the atomic-save pattern next to a target.
85// Used to assert no orphan temps leak out of a successful commit.
86unsigned countSiblingTemps( const wxString& aTargetPath )
87{
88 wxFileName fn( aTargetPath );
89 wxString dir = fn.GetPath();
90 wxString pattern = fn.GetFullName() + wxT( ".kicad-save-*" );
91 wxArrayString matches;
92 wxDir::GetAllFiles( dir, &matches, pattern, wxDIR_FILES );
93 return matches.GetCount();
94}
95
96} // anonymous namespace
97
98
99BOOST_AUTO_TEST_SUITE( AtomicSave )
100
101
102BOOST_AUTO_TEST_CASE( PrettifiedFormatter_HappyPath )
103{
104 wxString target = makeTempTargetPath( wxT( "happy" ) );
105
106 {
108 f.Print( 0, "(kicad_test (content \"hello\"))\n" );
109 BOOST_REQUIRE( f.Finish() );
110 }
111
112 BOOST_REQUIRE( wxFileName::FileExists( target ) );
113 std::string actual = readFileContents( target );
114 BOOST_REQUIRE( !actual.empty() );
115 BOOST_REQUIRE( actual.find( "hello" ) != std::string::npos );
116 BOOST_REQUIRE_EQUAL( countSiblingTemps( target ), 0u );
117
118 wxRemoveFile( target );
119}
120
121
122BOOST_AUTO_TEST_CASE( PrettifiedFormatter_UnwindingPreservesOriginal )
123{
124 // Pre-seed target with known content. If anything throws between formatter
125 // construction and Finish() -- the exact bug class we are fixing -- the user's
126 // file must remain byte-identical.
127 wxString target = makeTempTargetPath( wxT( "unwind" ) );
128 const std::string original = "(original \"do not lose me\")\n";
129 writeFileContents( target, original );
130
131 BOOST_REQUIRE_THROW(
132 {
134 f.Print( 0, "(partial " );
135 // Simulate a serializer crash mid-save -- the equivalent of std::bad_alloc
136 // from KICAD_FORMAT::Prettify on a large board, or an IO_ERROR nested
137 // deep in a FormatBoardToFormatter call.
138 throw std::runtime_error( "simulated serializer failure" );
139 },
140 std::runtime_error );
141
142 BOOST_REQUIRE( wxFileName::FileExists( target ) );
143 BOOST_REQUIRE_EQUAL( readFileContents( target ), original );
144 BOOST_REQUIRE_EQUAL( countSiblingTemps( target ), 0u );
145
146 wxRemoveFile( target );
147}
148
149
150BOOST_AUTO_TEST_CASE( PrettifiedFormatter_DestructorCommitsWithoutExplicitFinish )
151{
152 // Callers that don't call Finish() explicitly rely on the destructor to commit.
153 // This is the legacy contract -- preserve it, but without the silent-error-swallow.
154 wxString target = makeTempTargetPath( wxT( "implicit" ) );
155
156 {
158 f.Print( 0, "(implicit_commit ok)\n" );
159 }
160
161 BOOST_REQUIRE( wxFileName::FileExists( target ) );
162 std::string actual = readFileContents( target );
163 BOOST_REQUIRE( actual.find( "implicit_commit" ) != std::string::npos );
164 BOOST_REQUIRE_EQUAL( countSiblingTemps( target ), 0u );
165
166 wxRemoveFile( target );
167}
168
169
170BOOST_AUTO_TEST_CASE( FileFormatter_UnwindingPreservesOriginal )
171{
172 // Same invariant for the streaming (non-prettified) formatter: a throw mid-
173 // serialization must leave the user's file intact.
174 wxString target = makeTempTargetPath( wxT( "stream" ) );
175 const std::string original = "original streaming content\n";
176 writeFileContents( target, original );
177
178 BOOST_REQUIRE_THROW(
179 {
180 FILE_OUTPUTFORMATTER f( target );
181 f.Print( 0, "partial write before the crash " );
182 throw std::runtime_error( "simulated exporter failure" );
183 },
184 std::runtime_error );
185
186 BOOST_REQUIRE( wxFileName::FileExists( target ) );
187 BOOST_REQUIRE_EQUAL( readFileContents( target ), original );
188 BOOST_REQUIRE_EQUAL( countSiblingTemps( target ), 0u );
189
190 wxRemoveFile( target );
191}
192
193
194BOOST_AUTO_TEST_CASE( AtomicWriteFile_HappyPath )
195{
196 wxString target = makeTempTargetPath( wxT( "atomicbuf" ) );
197 const std::string payload = "{\"setting\":42}\n";
198
199 wxString err;
200 BOOST_REQUIRE( KIPLATFORM::IO::AtomicWriteFile( target, payload.data(), payload.size(),
201 &err ) );
202 BOOST_REQUIRE( err.IsEmpty() );
203 BOOST_REQUIRE( wxFileName::FileExists( target ) );
204 BOOST_REQUIRE_EQUAL( readFileContents( target ), payload );
205 BOOST_REQUIRE_EQUAL( countSiblingTemps( target ), 0u );
206
207 wxRemoveFile( target );
208}
209
210
211BOOST_AUTO_TEST_CASE( AtomicWriteFile_OverwritePreservesOriginalOnFailure )
212{
213 // AtomicWriteFile targeting a non-writable directory must fail without touching
214 // the existing file at the target path.
215 wxString target = makeTempTargetPath( wxT( "overwrite" ) );
216 const std::string original = "unchanged\n";
217 writeFileContents( target, original );
218
219 // Point at a path whose parent directory cannot be created: the temp-file open
220 // should fail immediately, leaving the target intact. Using an empty buffer is
221 // fine -- the failure happens before any write.
222 wxString bogus = target + wxT( "/subdir/cannot-create" );
223 wxString err;
224 bool ok = KIPLATFORM::IO::AtomicWriteFile( bogus, original.data(), original.size(), &err );
225 BOOST_REQUIRE( !ok );
226 BOOST_REQUIRE( !err.IsEmpty() );
227
228 BOOST_REQUIRE( wxFileName::FileExists( target ) );
229 BOOST_REQUIRE_EQUAL( readFileContents( target ), original );
230
231 wxRemoveFile( target );
232}
233
234
235BOOST_AUTO_TEST_CASE( PrettifiedFormatter_ExplicitFinishThrowsOnCommitFailure )
236{
237 // Target path is a pre-existing directory so rename() fails with EISDIR during commit.
238 wxString target = makeTempTargetPath( wxT( "commitfail-explicit" ) );
239
240 BOOST_REQUIRE( wxFileName::Mkdir( target, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
241
242 bool threw = false;
243
244 try
245 {
247 f.Print( 0, "(doomed save)\n" );
248 f.Finish();
249 }
250 catch( const IO_ERROR& )
251 {
252 threw = true;
253 }
254
255 BOOST_REQUIRE_MESSAGE( threw, "Finish() must throw IO_ERROR when atomic commit fails" );
256 BOOST_REQUIRE( wxFileName::DirExists( target ) );
257 BOOST_REQUIRE_EQUAL( countSiblingTemps( target ), 0u );
258
259 wxFileName::Rmdir( target );
260}
261
262
263BOOST_AUTO_TEST_CASE( FileFormatter_ExplicitFinishThrowsOnCommitFailure )
264{
265 wxString target = makeTempTargetPath( wxT( "commitfail-stream" ) );
266
267 BOOST_REQUIRE( wxFileName::Mkdir( target, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
268
269 bool threw = false;
270
271 try
272 {
273 FILE_OUTPUTFORMATTER f( target );
274 f.Print( 0, "doomed streaming save\n" );
275 f.Finish();
276 }
277 catch( const IO_ERROR& )
278 {
279 threw = true;
280 }
281
282 BOOST_REQUIRE_MESSAGE( threw, "Finish() must throw IO_ERROR when atomic commit fails" );
283 BOOST_REQUIRE( wxFileName::DirExists( target ) );
284 BOOST_REQUIRE_EQUAL( countSiblingTemps( target ), 0u );
285
286 wxFileName::Rmdir( target );
287}
288
289
290BOOST_AUTO_TEST_CASE( DrawingSheetSave_PropagatesCommitFailure )
291{
292 // Regression for the DS_DATA_MODEL_FILEIO refactor: the drawing-sheet writer must
293 // propagate commit failures instead of swallowing them in a constructor catch.
295 model.SetEmptyLayout();
296
297 wxString target = makeTempTargetPath( wxT( "wks-commitfail" ) );
298 BOOST_REQUIRE( wxFileName::Mkdir( target, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
299
300 BOOST_REQUIRE_THROW( model.Save( target ), IO_ERROR );
301 BOOST_REQUIRE( wxFileName::DirExists( target ) );
302 BOOST_REQUIRE_EQUAL( countSiblingTemps( target ), 0u );
303
304 wxFileName::Rmdir( target );
305}
306
307
308BOOST_AUTO_TEST_CASE( PrettifiedFormatter_DestructorCommitFailureLeavesTargetIntact )
309{
310 // Legacy callers without explicit Finish() must still leave the target untouched
311 // on commit failure. A destructor cannot throw during unwinding, so we can only
312 // assert the on-disk state.
313 wxString target = makeTempTargetPath( wxT( "commitfail-implicit" ) );
314
315 BOOST_REQUIRE( wxFileName::Mkdir( target, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
316
317 {
319 f.Print( 0, "(implicit doomed)\n" );
320 }
321
322 BOOST_REQUIRE( wxFileName::DirExists( target ) );
323 BOOST_REQUIRE_EQUAL( countSiblingTemps( target ), 0u );
324
325 wxFileName::Rmdir( target );
326}
327
328
329BOOST_AUTO_TEST_CASE( PrettifiedFormatter_CreatesNewTarget )
330{
331 // When the target file doesn't exist, atomic save must create it cleanly.
332 wxString target = makeTempTargetPath( wxT( "newtarget" ) );
333 BOOST_REQUIRE( !wxFileName::FileExists( target ) );
334
335 {
337 f.Print( 0, "(new_file content)\n" );
338 BOOST_REQUIRE( f.Finish() );
339 }
340
341 BOOST_REQUIRE( wxFileName::FileExists( target ) );
342 std::string actual = readFileContents( target );
343 BOOST_REQUIRE( actual.find( "new_file" ) != std::string::npos );
344 BOOST_REQUIRE_EQUAL( countSiblingTemps( target ), 0u );
345
346 wxRemoveFile( target );
347}
348
349
350#if !defined( _WIN32 )
351
352BOOST_AUTO_TEST_CASE( AtomicWriteFile_PreservesPosixMode )
353{
354 // Regression: MakeWriteable used to run before DuplicatePermissions, so the temp
355 // inherited a relaxed mode. A file saved atomically over a 0400 target ended up as
356 // 0600. Verify the target mode survives the save exactly.
357 wxString target = makeTempTargetPath( wxT( "mode" ) );
358 const std::string original = "original\n";
359 writeFileContents( target, original );
360
361 BOOST_REQUIRE_EQUAL( chmod( target.fn_str(), 0400 ), 0 );
362
363 const std::string payload = "replacement\n";
364 wxString err;
365 BOOST_REQUIRE( KIPLATFORM::IO::AtomicWriteFile( target, payload.data(), payload.size(),
366 &err ) );
367 BOOST_REQUIRE( err.IsEmpty() );
368
369 struct stat st;
370 BOOST_REQUIRE_EQUAL( stat( target.fn_str(), &st ), 0 );
371 BOOST_REQUIRE_EQUAL( st.st_mode & 0777, 0400 );
372 BOOST_REQUIRE_EQUAL( readFileContents( target ), payload );
373
374 chmod( target.fn_str(), 0600 );
375 wxRemoveFile( target );
376}
377
378
379BOOST_AUTO_TEST_CASE( AtomicWriteFile_FollowsSymlinkTarget )
380{
381 // Regression: pre-atomic saves opened the referent via wxFopen so the symlink
382 // survived. The new rename-based path would have replaced the symlink with a
383 // regular file; ResolveSymlinkTarget fixes that by resolving first.
384 wxString referent = makeTempTargetPath( wxT( "symref" ) );
385 wxString linkPath = makeTempTargetPath( wxT( "symlink" ) );
386 const std::string original = "referent original\n";
387 writeFileContents( referent, original );
388
389 BOOST_REQUIRE_EQUAL( symlink( referent.fn_str(), linkPath.fn_str() ), 0 );
390
391 const std::string payload = "replacement via symlink\n";
392 wxString err;
393 BOOST_REQUIRE( KIPLATFORM::IO::AtomicWriteFile( linkPath, payload.data(), payload.size(),
394 &err ) );
395
396 // Link must still be a symlink pointing at the referent.
397 struct stat st;
398 BOOST_REQUIRE_EQUAL( lstat( linkPath.fn_str(), &st ), 0 );
399 BOOST_REQUIRE( S_ISLNK( st.st_mode ) );
400
401 // Referent must have the new content.
402 BOOST_REQUIRE_EQUAL( readFileContents( referent ), payload );
403
404 wxRemoveFile( linkPath );
405 wxRemoveFile( referent );
406}
407
408
409BOOST_AUTO_TEST_CASE( AtomicWriteFile_FailedRenameRestoresTargetMode )
410{
411 // Regression: if AtomicRename fails after MakeWriteable has run on the target, the
412 // target must not be left with its mode bits permanently widened. We provoke a
413 // rename failure by targeting a path whose parent directory cannot accept the write,
414 // while ensuring the pre-existing target still has an unusual mode.
415 wxString target = makeTempTargetPath( wxT( "failrestore" ) );
416 const std::string original = "original\n";
417 writeFileContents( target, original );
418 BOOST_REQUIRE_EQUAL( chmod( target.fn_str(), 0400 ), 0 );
419
420 // Route through a non-existent subdirectory so wxFopen on the temp fails before the
421 // atomic sequence even reaches MakeWriteable. This exercises the "target untouched"
422 // invariant for the common early-failure path.
423 wxString bogus = target + wxT( "/cannot-create" );
424 wxString err;
425 BOOST_REQUIRE( !KIPLATFORM::IO::AtomicWriteFile( bogus, "x", 1, &err ) );
426
427 struct stat st;
428 BOOST_REQUIRE_EQUAL( stat( target.fn_str(), &st ), 0 );
429 BOOST_REQUIRE_EQUAL( st.st_mode & 0777, 0400 );
430 BOOST_REQUIRE_EQUAL( readFileContents( target ), original );
431
432 chmod( target.fn_str(), 0600 );
433 wxRemoveFile( target );
434}
435
436#else // _WIN32
437
438BOOST_AUTO_TEST_CASE( AtomicWriteFile_PreservesWindowsAttributes )
439{
440 // Regression: DuplicatePermissions copies ACLs via SetFileSecurity but not attribute
441 // bits. READONLY and HIDDEN used to be silently dropped on every successful save.
442 wxString target = makeTempTargetPath( wxT( "winattrs" ) );
443 const std::string original = "original\n";
444 writeFileContents( target, original );
445
446 BOOST_REQUIRE( SetFileAttributesW( target.wc_str(),
447 FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_HIDDEN ) );
448
449 const std::string payload = "replacement\n";
450 wxString err;
451 BOOST_REQUIRE( KIPLATFORM::IO::AtomicWriteFile( target, payload.data(), payload.size(),
452 &err ) );
453 BOOST_REQUIRE( err.IsEmpty() );
454
455 DWORD attrs = GetFileAttributesW( target.wc_str() );
456 BOOST_REQUIRE( attrs != INVALID_FILE_ATTRIBUTES );
457 BOOST_REQUIRE( ( attrs & FILE_ATTRIBUTE_READONLY ) != 0 );
458 BOOST_REQUIRE( ( attrs & FILE_ATTRIBUTE_HIDDEN ) != 0 );
459 BOOST_REQUIRE_EQUAL( readFileContents( target ), payload );
460
461 SetFileAttributesW( target.wc_str(), FILE_ATTRIBUTE_NORMAL );
462 wxRemoveFile( target );
463}
464
465#endif // _WIN32
466
467
Handle the graphic items list to draw/plot the frame and title block.
static DS_DATA_MODEL & GetTheInstance()
Return the instance of DS_DATA_MODEL used in the application.
Used for text file output.
Definition richio.h:474
bool Finish() override
Flushes the temp file to disk and atomically renames it over the final target path.
Definition richio.cpp:646
Hold an error message and may be used when throwing exceptions containing meaningful error messages.
int PRINTF_FUNC_N Print(int nestLevel, const char *fmt,...)
Format and write text to the output stream.
Definition richio.cpp:426
bool Finish() override
Runs prettification over the buffered bytes, writes them to the sibling temp file,...
Definition richio.cpp:700
bool AtomicWriteFile(const wxString &aTargetPath, const void *aData, size_t aSize, wxString *aError=nullptr)
Writes aData to aTargetPath via a sibling temp file, fsyncs the data and directory,...
BOOST_AUTO_TEST_CASE(HorizontalAlignment)
BOOST_AUTO_TEST_CASE(PrettifiedFormatter_HappyPath)
BOOST_AUTO_TEST_SUITE(CadstarPartParser)
BOOST_REQUIRE(intersection.has_value()==c.ExpectedIntersection.has_value())
BOOST_AUTO_TEST_SUITE_END()
KIBIS_MODEL * model
int actual