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