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
17 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18 */
19
21#include <boost/test/unit_test.hpp>
22#include <qa_utils/file_utils.h>
23
25#include <ki_exception.h>
26#include <kiplatform/io.h>
27#include <richio.h>
28
29#include <wx/dir.h>
30#include <wx/ffile.h>
31#include <wx/filefn.h>
32#include <wx/filename.h>
33
34#include <string>
35
36#if defined( _WIN32 )
37#include <windows.h>
38#include <sddl.h>
39#else
40#include <sys/stat.h>
41#include <sys/types.h>
42#endif
43
44
45namespace
46{
47
48void writeFileContents( const wxString& aPath, const std::string& aContent )
49{
50 wxFFile fp( aPath, wxT( "wb" ) );
51 BOOST_REQUIRE( fp.IsOpened() );
52
53 if( !aContent.empty() )
54 BOOST_REQUIRE( fp.Write( aContent.data(), aContent.size() ) == aContent.size() );
55
56 fp.Close();
57}
58
59
60// Counts remaining sibling temp files matching the atomic-save pattern next to a target.
61// Used to assert no orphan temps leak out of a successful commit.
62unsigned countSiblingTemps( const wxString& aTargetPath )
63{
64 wxFileName fn( aTargetPath );
65 wxString dir = fn.GetPath();
66 wxString pattern = fn.GetFullName() + wxT( ".kicad-save-*" );
67 wxArrayString matches;
68 wxDir::GetAllFiles( dir, &matches, pattern, wxDIR_FILES );
69 return matches.GetCount();
70}
71
72} // anonymous namespace
73
74BOOST_AUTO_TEST_SUITE( AtomicSave )
75
76
77BOOST_AUTO_TEST_CASE( PrettifiedFormatter_HappyPath )
78{
79 KI_TEST::SCOPED_TEMP_DIR tempDir( wxT( "kicad-atomicsave-happy" ) );
80 const wxString target = tempDir.PathStr() + wxFileName::GetPathSeparator() + wxT( "target" );
81
82 {
84 f.Print( 0, "(kicad_test (content \"hello\"))\n" );
85 BOOST_REQUIRE( f.Finish() );
86 }
87
88 BOOST_REQUIRE( wxFileName::FileExists( target ) );
89 std::string actual = KI_TEST::LoadStringData( target );
90 BOOST_REQUIRE( !actual.empty() );
91 BOOST_REQUIRE( actual.find( "hello" ) != std::string::npos );
92 BOOST_REQUIRE_EQUAL( countSiblingTemps( target ), 0u );
93}
94
95
96BOOST_AUTO_TEST_CASE( PrettifiedFormatter_UnwindingPreservesOriginal )
97{
98 // Pre-seed target with known content. If anything throws between formatter
99 // construction and Finish() -- the exact bug class we are fixing -- the user's
100 // file must remain byte-identical.
101 KI_TEST::SCOPED_TEMP_DIR tempDir( wxT( "kicad-atomicsave-unwind" ) );
102 const wxString target = tempDir.PathStr() + wxFileName::GetPathSeparator() + wxT( "target" );
103 const std::string original = "(original \"do not lose me\")\n";
104 writeFileContents( target, original );
105
106 BOOST_REQUIRE_THROW(
107 {
109 f.Print( 0, "(partial " );
110 // Simulate a serializer crash mid-save -- the equivalent of std::bad_alloc
111 // from KICAD_FORMAT::Prettify on a large board, or an IO_ERROR nested
112 // deep in a FormatBoardToFormatter call.
113 throw std::runtime_error( "simulated serializer failure" );
114 },
115 std::runtime_error );
116
117 BOOST_REQUIRE( wxFileName::FileExists( target ) );
118 BOOST_REQUIRE_EQUAL( KI_TEST::LoadStringData( target ), original );
119 BOOST_REQUIRE_EQUAL( countSiblingTemps( target ), 0u );
120}
121
122
123BOOST_AUTO_TEST_CASE( PrettifiedFormatter_DestructorDiscardsWithoutExplicitFinish )
124{
125 // Finish() is the only thing that commits. A formatter that goes out of scope without it
126 // has abandoned the save, so the target keeps the bytes it already had.
127 KI_TEST::SCOPED_TEMP_DIR tempDir( wxT( "kicad-atomicsave-discard-unfinished" ) );
128 const wxString target = tempDir.PathStr() + wxFileName::GetPathSeparator() + wxT( "target" );
129 const std::string original = "(original \"keep me\")\n";
130 writeFileContents( target, original );
131
132 {
134 f.Print( 0, "(uncommitted content)\n" );
135 }
136
137 BOOST_REQUIRE_EQUAL( KI_TEST::LoadStringData( target ), original );
138 BOOST_REQUIRE_EQUAL( countSiblingTemps( target ), 0u );
139}
140
141
142BOOST_AUTO_TEST_CASE( FileFormatter_UnwindingPreservesOriginal )
143{
144 // Same invariant for the streaming (non-prettified) formatter: a throw mid-
145 // serialization must leave the user's file intact.
146 KI_TEST::SCOPED_TEMP_DIR tempDir( wxT( "kicad-atomicsave-stream" ) );
147 const wxString target = tempDir.PathStr() + wxFileName::GetPathSeparator() + wxT( "target" );
148 const std::string original = "original streaming content\n";
149 writeFileContents( target, original );
150
151 BOOST_REQUIRE_THROW(
152 {
153 FILE_OUTPUTFORMATTER f( target );
154 f.Print( 0, "partial write before the crash " );
155 throw std::runtime_error( "simulated exporter failure" );
156 },
157 std::runtime_error );
158
159 BOOST_REQUIRE( wxFileName::FileExists( target ) );
160 BOOST_REQUIRE_EQUAL( KI_TEST::LoadStringData( target ), original );
161 BOOST_REQUIRE_EQUAL( countSiblingTemps( target ), 0u );
162}
163
164
165BOOST_AUTO_TEST_CASE( FileFormatter_DestructorDiscardsWithoutExplicitFinish )
166{
167 // Same invariant for the streaming (non-prettified) formatter.
168 KI_TEST::SCOPED_TEMP_DIR tempDir( wxT( "kicad-atomicsave-stream-scope-exit" ) );
169 const wxString target = tempDir.PathStr() + wxFileName::GetPathSeparator() + wxT( "target" );
170 const std::string original = "original export content\n";
171 writeFileContents( target, original );
172
173 {
174 FILE_OUTPUTFORMATTER f( target );
175 f.Print( 0, "uncommitted streaming content\n" );
176 }
177
178 BOOST_REQUIRE_EQUAL( KI_TEST::LoadStringData( target ), original );
179 BOOST_REQUIRE_EQUAL( countSiblingTemps( target ), 0u );
180}
181
182
183BOOST_AUTO_TEST_CASE( AtomicWriteFile_HappyPath )
184{
185 KI_TEST::SCOPED_TEMP_DIR tempDir( wxT( "kicad-atomicsave-atomicbuf" ) );
186 const wxString target = tempDir.PathStr() + wxFileName::GetPathSeparator() + wxT( "target" );
187 const std::string payload = "{\"setting\":42}\n";
188
189 wxString err;
190 BOOST_REQUIRE( KIPLATFORM::IO::AtomicWriteFile( target, payload.data(), payload.size(),
191 &err ) );
192 BOOST_REQUIRE( err.IsEmpty() );
193 BOOST_REQUIRE( wxFileName::FileExists( target ) );
194 BOOST_REQUIRE_EQUAL( KI_TEST::LoadStringData( target ), payload );
195 BOOST_REQUIRE_EQUAL( countSiblingTemps( target ), 0u );
196}
197
198
199BOOST_AUTO_TEST_CASE( AtomicWriteFile_OverwritePreservesOriginalOnFailure )
200{
201 // AtomicWriteFile targeting a non-writable directory must fail without touching
202 // the existing file at the target path.
203 KI_TEST::SCOPED_TEMP_DIR tempDir( wxT( "kicad-atomicsave-overwrite" ) );
204 const wxString target = tempDir.PathStr() + wxFileName::GetPathSeparator() + wxT( "target" );
205 const std::string original = "unchanged\n";
206 writeFileContents( target, original );
207
208 // Point at a path whose parent directory cannot be created: the temp-file open
209 // should fail immediately, leaving the target intact. Using an empty buffer is
210 // fine -- the failure happens before any write.
211 wxString bogus = target + wxT( "/subdir/cannot-create" );
212 wxString err;
213 bool ok = KIPLATFORM::IO::AtomicWriteFile( bogus, original.data(), original.size(), &err );
214 BOOST_REQUIRE( !ok );
215 BOOST_REQUIRE( !err.IsEmpty() );
216
217 BOOST_REQUIRE( wxFileName::FileExists( target ) );
218 BOOST_REQUIRE_EQUAL( KI_TEST::LoadStringData( target ), original );
219}
220
221
222BOOST_AUTO_TEST_CASE( PrettifiedFormatter_ExplicitFinishThrowsOnCommitFailure )
223{
224 // Target path is a pre-existing directory so rename() fails with EISDIR during commit.
225 KI_TEST::SCOPED_TEMP_DIR tempDir( wxT( "kicad-atomicsave-commitfail-explicit" ) );
226 const wxString target = tempDir.PathStr() + wxFileName::GetPathSeparator() + wxT( "target" );
227
228 BOOST_REQUIRE( wxFileName::Mkdir( target, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
229
230 bool threw = false;
231
232 try
233 {
235 f.Print( 0, "(doomed save)\n" );
236 f.Finish();
237 }
238 catch( const IO_ERROR& )
239 {
240 threw = true;
241 }
242
243 BOOST_REQUIRE_MESSAGE( threw, "Finish() must throw IO_ERROR when atomic commit fails" );
244 BOOST_REQUIRE( wxFileName::DirExists( target ) );
245 BOOST_REQUIRE_EQUAL( countSiblingTemps( target ), 0u );
246}
247
248
249BOOST_AUTO_TEST_CASE( FileFormatter_ExplicitFinishThrowsOnCommitFailure )
250{
251 KI_TEST::SCOPED_TEMP_DIR tempDir( wxT( "kicad-atomicsave-commitfail-stream" ) );
252 const wxString target = tempDir.PathStr() + wxFileName::GetPathSeparator() + wxT( "target" );
253
254 BOOST_REQUIRE( wxFileName::Mkdir( target, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
255
256 bool threw = false;
257
258 try
259 {
260 FILE_OUTPUTFORMATTER f( target );
261 f.Print( 0, "doomed streaming save\n" );
262 f.Finish();
263 }
264 catch( const IO_ERROR& )
265 {
266 threw = true;
267 }
268
269 BOOST_REQUIRE_MESSAGE( threw, "Finish() must throw IO_ERROR when atomic commit fails" );
270 BOOST_REQUIRE( wxFileName::DirExists( target ) );
271 BOOST_REQUIRE_EQUAL( countSiblingTemps( target ), 0u );
272}
273
274
275BOOST_AUTO_TEST_CASE( Formatter_SecondFinishIsACallerError )
276{
277 // Committing is single-use: a second Finish() after a successful commit or after a
278 // failed one is a programming error and must trip the QA assertion thrower.
279 KI_TEST::SCOPED_TEMP_DIR tempDir( wxT( "kicad-atomicsave-double-finish" ) );
280 const wxString sep = wxFileName::GetPathSeparator();
281
282 // Streaming formatter: second Finish() after a successful commit.
283 {
284 const wxString target = tempDir.PathStr() + sep + wxT( "stream-ok" );
285 FILE_OUTPUTFORMATTER f( target );
286 f.Print( 0, "saved once\n" );
287
288 BOOST_REQUIRE( f.Finish() );
289 CHECK_WX_ASSERT( f.Finish() );
290 }
291
292 // Streaming formatter: second Finish() after a failed commit.
293 {
294 const wxString target = tempDir.PathStr() + sep + wxT( "stream-fail" );
295 BOOST_REQUIRE( wxFileName::Mkdir( target, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
296
297 FILE_OUTPUTFORMATTER f( target );
298 f.Print( 0, "doomed streaming save\n" );
299
300 BOOST_REQUIRE_THROW( f.Finish(), IO_ERROR );
301 CHECK_WX_ASSERT( f.Finish() );
302 }
303
304 // Prettified formatter: second Finish() after a successful commit.
305 {
306 const wxString target = tempDir.PathStr() + sep + wxT( "prettified-ok" );
308 f.Print( 0, "(saved once)\n" );
309
310 BOOST_REQUIRE( f.Finish() );
311 CHECK_WX_ASSERT( f.Finish() );
312 }
313
314 // Prettified formatter: second Finish() after a failed commit.
315 {
316 const wxString target = tempDir.PathStr() + sep + wxT( "prettified-fail" );
317 BOOST_REQUIRE( wxFileName::Mkdir( target, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
318
320 f.Print( 0, "(doomed pretty save)\n" );
321
322 BOOST_REQUIRE_THROW( f.Finish(), IO_ERROR );
323 CHECK_WX_ASSERT( f.Finish() );
324 }
325}
326
327
328BOOST_AUTO_TEST_CASE( DrawingSheetSave_PropagatesCommitFailure )
329{
330 // Regression for the DS_DATA_MODEL_FILEIO refactor: the drawing-sheet writer must
331 // propagate commit failures instead of swallowing them in a constructor catch.
333 model.SetEmptyLayout();
334
335 KI_TEST::SCOPED_TEMP_DIR tempDir( wxT( "kicad-atomicsave-wks-commitfail" ) );
336 const wxString target = tempDir.PathStr() + wxFileName::GetPathSeparator() + wxT( "target" );
337 BOOST_REQUIRE( wxFileName::Mkdir( target, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
338
339 BOOST_REQUIRE_THROW( model.Save( target ), IO_ERROR );
340 BOOST_REQUIRE( wxFileName::DirExists( target ) );
341 BOOST_REQUIRE_EQUAL( countSiblingTemps( target ), 0u );
342}
343
344
345BOOST_AUTO_TEST_CASE( PrettifiedFormatter_DestructorDiscardsWhenTargetIsDirectory )
346{
347 // A target that cannot be renamed over is still a target the formatter must not disturb.
348 // Asserting on-disk state alone would pass either way, since a destructor that attempted
349 // the commit would fail the rename and tidy up too -- but it would say so first.
350 KI_TEST::SCOPED_TEMP_DIR tempDir( wxT( "kicad-atomicsave-commitfail-implicit" ) );
351 const wxString target = tempDir.PathStr() + wxFileName::GetPathSeparator() + wxT( "target" );
352
353 BOOST_REQUIRE( wxFileName::Mkdir( target, wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL ) );
354
355 KI_TEST::SCOPED_COUNTING_WXLOG logCounter( nullptr );
356
357 {
359 f.Print( 0, "(implicit doomed)\n" );
360 }
361
362 // Nothing was attempted, so nothing had anything to report
363 BOOST_REQUIRE_EQUAL( logCounter.GetCount(), 0u );
364 BOOST_REQUIRE( wxFileName::DirExists( target ) );
365 BOOST_REQUIRE_EQUAL( countSiblingTemps( target ), 0u );
366}
367
368
369BOOST_AUTO_TEST_CASE( PrettifiedFormatter_CreatesNewTarget )
370{
371 // When the target file doesn't exist, atomic save must create it cleanly.
372 KI_TEST::SCOPED_TEMP_DIR tempDir( wxT( "kicad-atomicsave-newtarget" ) );
373 const wxString target = tempDir.PathStr() + wxFileName::GetPathSeparator() + wxT( "target" );
374 BOOST_REQUIRE( !wxFileName::FileExists( target ) );
375
376 {
378 f.Print( 0, "(new_file content)\n" );
379 BOOST_REQUIRE( f.Finish() );
380 }
381
382 BOOST_REQUIRE( wxFileName::FileExists( target ) );
383 std::string actual = KI_TEST::LoadStringData( target );
384 BOOST_REQUIRE( actual.find( "new_file" ) != std::string::npos );
385 BOOST_REQUIRE_EQUAL( countSiblingTemps( target ), 0u );
386}
387
388
389#if !defined( _WIN32 )
390
391BOOST_AUTO_TEST_CASE( AtomicWriteFile_PreservesPosixMode )
392{
393 // Regression: MakeWriteable used to run before DuplicatePermissions, so the temp
394 // inherited a relaxed mode. A file saved atomically over a 0400 target ended up as
395 // 0600. Verify the target mode survives the save exactly.
396 KI_TEST::SCOPED_TEMP_DIR tempDir( wxT( "kicad-atomicsave-mode" ) );
397 const wxString target = tempDir.PathStr() + wxFileName::GetPathSeparator() + wxT( "target" );
398 const std::string original = "original\n";
399 writeFileContents( target, original );
400
401 BOOST_REQUIRE_EQUAL( chmod( target.fn_str(), 0400 ), 0 );
402
403 const std::string payload = "replacement\n";
404 wxString err;
405 BOOST_REQUIRE( KIPLATFORM::IO::AtomicWriteFile( target, payload.data(), payload.size(),
406 &err ) );
407 BOOST_REQUIRE( err.IsEmpty() );
408
409 struct stat st;
410 BOOST_REQUIRE_EQUAL( stat( target.fn_str(), &st ), 0 );
411 BOOST_REQUIRE_EQUAL( st.st_mode & 0777, 0400 );
412 BOOST_REQUIRE_EQUAL( KI_TEST::LoadStringData( target ), payload );
413}
414
415
416BOOST_AUTO_TEST_CASE( AtomicWriteFile_NewTargetHonoursUmask )
417{
418 KI_TEST::SCOPED_TEMP_DIR tempDir( wxT( "kicad-atomicsave-umask" ) );
419 const wxString target = tempDir.PathStr() + wxFileName::GetPathSeparator() + wxT( "target" );
420 const std::string payload = "fresh\n";
421 const mode_t previousMask = umask( 022 );
422 wxString err;
423
424 const bool written = KIPLATFORM::IO::AtomicWriteFile( target, payload.data(), payload.size(), &err );
425
426 umask( previousMask );
427
428 BOOST_REQUIRE_MESSAGE( written, err );
429
430 struct stat st;
431 BOOST_REQUIRE_EQUAL( stat( target.fn_str(), &st ), 0 );
432 BOOST_CHECK_EQUAL( st.st_mode & 0777, 0644 );
433}
434
435
436BOOST_AUTO_TEST_CASE( AtomicWriteFile_FollowsSymlinkTarget )
437{
438 // Regression: pre-atomic saves opened the referent via wxFopen so the symlink
439 // survived. The new rename-based path would have replaced the symlink with a
440 // regular file; ResolveSymlinkTarget fixes that by resolving first.
441 KI_TEST::SCOPED_TEMP_DIR tempDir( wxT( "kicad-atomicsave-symlink" ) );
442 const wxString referent = tempDir.PathStr() + wxFileName::GetPathSeparator() + wxT( "referent" );
443 const wxString linkPath = tempDir.PathStr() + wxFileName::GetPathSeparator() + wxT( "link" );
444 const std::string original = "referent original\n";
445 writeFileContents( referent, original );
446
447 BOOST_REQUIRE_EQUAL( symlink( referent.fn_str(), linkPath.fn_str() ), 0 );
448
449 const std::string payload = "replacement via symlink\n";
450 wxString err;
451 BOOST_REQUIRE( KIPLATFORM::IO::AtomicWriteFile( linkPath, payload.data(), payload.size(),
452 &err ) );
453
454 // Link must still be a symlink pointing at the referent.
455 struct stat st;
456 BOOST_REQUIRE_EQUAL( lstat( linkPath.fn_str(), &st ), 0 );
457 BOOST_REQUIRE( S_ISLNK( st.st_mode ) );
458
459 // Referent must have the new content.
460 BOOST_REQUIRE_EQUAL( KI_TEST::LoadStringData( referent ), payload );
461}
462
463
464BOOST_AUTO_TEST_CASE( AtomicWriteFile_FailedRenameRestoresTargetMode )
465{
466 // Regression: if AtomicRename fails after MakeWriteable has run on the target, the
467 // target must not be left with its mode bits permanently widened. We provoke a
468 // rename failure by targeting a path whose parent directory cannot accept the write,
469 // while ensuring the pre-existing target still has an unusual mode.
470 KI_TEST::SCOPED_TEMP_DIR tempDir( wxT( "kicad-atomicsave-failrestore" ) );
471 const wxString target = tempDir.PathStr() + wxFileName::GetPathSeparator() + wxT( "target" );
472 const std::string original = "original\n";
473 writeFileContents( target, original );
474 BOOST_REQUIRE_EQUAL( chmod( target.fn_str(), 0400 ), 0 );
475
476 // Route through a non-existent subdirectory so wxFopen on the temp fails before the
477 // atomic sequence even reaches MakeWriteable. This exercises the "target untouched"
478 // invariant for the common early-failure path.
479 wxString bogus = target + wxT( "/cannot-create" );
480 wxString err;
481 BOOST_REQUIRE( !KIPLATFORM::IO::AtomicWriteFile( bogus, "x", 1, &err ) );
482
483 struct stat st;
484 BOOST_REQUIRE_EQUAL( stat( target.fn_str(), &st ), 0 );
485 BOOST_REQUIRE_EQUAL( st.st_mode & 0777, 0400 );
486 BOOST_REQUIRE_EQUAL( KI_TEST::LoadStringData( target ), original );
487}
488
489#else // _WIN32
490
491BOOST_AUTO_TEST_CASE( AtomicWriteFile_PreservesWindowsAttributes )
492{
493 // Regression: DuplicatePermissions copies ACLs via SetFileSecurity but not attribute
494 // bits. READONLY and HIDDEN used to be silently dropped on every successful save.
495 KI_TEST::SCOPED_TEMP_DIR tempDir( wxT( "kicad-atomicsave-winattrs" ) );
496 const wxString target = tempDir.PathStr() + wxFileName::GetPathSeparator() + wxT( "target" );
497 const std::string original = "original\n";
498 writeFileContents( target, original );
499
500 BOOST_REQUIRE( SetFileAttributesW( target.wc_str(),
501 FILE_ATTRIBUTE_READONLY | FILE_ATTRIBUTE_HIDDEN ) );
502
503 const std::string payload = "replacement\n";
504 wxString err;
505 BOOST_REQUIRE( KIPLATFORM::IO::AtomicWriteFile( target, payload.data(), payload.size(),
506 &err ) );
507 BOOST_REQUIRE( err.IsEmpty() );
508
509 DWORD attrs = GetFileAttributesW( target.wc_str() );
510 BOOST_REQUIRE( attrs != INVALID_FILE_ATTRIBUTES );
511 BOOST_REQUIRE( ( attrs & FILE_ATTRIBUTE_READONLY ) != 0 );
512 BOOST_REQUIRE( ( attrs & FILE_ATTRIBUTE_HIDDEN ) != 0 );
513 BOOST_REQUIRE_EQUAL( KI_TEST::LoadStringData( target ), payload );
514
515 // Clear READONLY so the SCOPED_TEMP_DIR teardown (std::filesystem::remove_all) can
516 // delete the file on Windows, where remove() does not clear the attribute itself.
517 SetFileAttributesW( target.wc_str(), FILE_ATTRIBUTE_NORMAL );
518}
519
520
521BOOST_AUTO_TEST_CASE( AtomicWriteFile_SavesWhenPermissionsCannotBeCopied )
522{
523 KI_TEST::SCOPED_TEMP_DIR tempDir( wxT( "kicad-atomicsave-noacl" ) );
524 const wxString target = tempDir.PathStr() + wxFileName::GetPathSeparator() + wxT( "target" );
525 const wxString probe = tempDir.PathStr() + wxFileName::GetPathSeparator() + wxT( "probe" );
526 writeFileContents( target, "original\n" );
527 writeFileContents( probe, "" );
528
529 // An OWNER RIGHTS entry without READ_CONTROL hides the DACL even from the file's owner,
530 // standing in for a cloud-drive volume that has no security descriptor to hand out
531 PSECURITY_DESCRIPTOR sd = nullptr;
532 BOOST_REQUIRE( ConvertStringSecurityDescriptorToSecurityDescriptorW(
533 L"D:P(A;;0x11019f;;;OW)(A;;0x11019f;;;WD)", SDDL_REVISION_1, &sd, nullptr ) );
534
535 const BOOL applied = SetFileSecurityW( target.wc_str(), DACL_SECURITY_INFORMATION, sd );
536 LocalFree( sd );
537 BOOST_REQUIRE( applied );
539
540 const std::string payload = "replacement\n";
541 wxString err;
542 BOOST_REQUIRE_MESSAGE( KIPLATFORM::IO::AtomicWriteFile( target, payload.data(), payload.size(), &err ),
543 err );
544 BOOST_REQUIRE_EQUAL( KI_TEST::LoadStringData( target ), payload );
545}
546
547#endif // _WIN32
548
549
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:483
bool Finish() override
Flushes the temp file to disk and atomically renames it over the final target path.
Definition richio.cpp:682
Hold an error message and may be used when throwing exceptions containing meaningful error messages.
A scoped application of a wxLog target that counts error-level messages.
wxString PathStr() const
Get the path to the temporary directory as a wxString.
Definition file_utils.h:64
int PRINTF_FUNC_N Print(int nestLevel, const char *fmt,...)
Format and write text to the output stream.
Definition richio.cpp:432
bool Finish() override
Runs prettification over the buffered bytes, writes them to the sibling temp file,...
Definition richio.cpp:710
bool DuplicatePermissions(const wxString &aSrc, const wxString &aDest)
Duplicates the file security data from one file to another ensuring that they are the same between bo...
Definition unix/io.cpp:55
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,...
std::string LoadStringData(const wxString &aPath)
Load the contents of a file into a string.
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
BOOST_CHECK_EQUAL(result, "25.4")
#define CHECK_WX_ASSERT(STATEMENT)
A test macro to check a wxASSERT is thrown.