API
 
Loading...
Searching...
No Matches
streamWriter_lifecycle_test.cpp
Go to the documentation of this file.
1/** \file streamWriter_lifecycle_test.cpp
2 * \brief Catch2 lifecycle and configuration tests for the streamWriter app.
3 * \author OpenAI Codex
4 *
5 * \ingroup streamWriter_files
6 */
7
8#include "../../../tests/testXWC.hpp"
9
10#include <chrono>
11#include <cstdlib>
12#include <filesystem>
13#include <fstream>
14#include <memory>
15#include <optional>
16#include <stdexcept>
17#include <thread>
18
19#define protected public
20#include "../streamWriter.hpp"
21#undef protected
22
23using namespace MagAOX::app;
24
25namespace libXWCTest
26{
27
28/** \addtogroup streamWriter_unit_test
29 * \brief Additional lifecycle tests for the streamWriter application.
30 *
31 * \ingroup application_unit_test
32 */
33
34/// Namespace for `streamWriter` lifecycle unit tests.
35/** \ingroup streamWriter_unit_test
36 */
37namespace streamWriterTest
38{
39
40namespace
41{
42
43/// Build a unique shmim name for one temporary streamWriter test stream.
44std::string uniqueShmimName( const std::string &suffix )
45{
46 static unsigned counter = 0;
47
48 ++counter;
49
50 return "streamWriter_test_" + suffix + "_" + std::to_string( ::getpid() ) + "_" + std::to_string( counter );
51}
52
53/// Point ImageStreamIO shared-memory files at a writable test sandbox.
54std::string ensureMilkShmDir()
55{
56 static const std::string shmDir = []()
57 {
58 const std::filesystem::path path = "/tmp/streamWriter_lifecycle_test/shm";
59
60 std::filesystem::create_directories( path );
61 return path.string();
62 }();
63
64 setenv( "MILK_SHM_DIR", shmDir.c_str(), 1 );
65
66 return shmDir;
67}
68
69/// Configuration values used to build a deterministic streamWriter test config.
70struct streamWriterConfig
71{
72 size_t m_maxCircBuffLength{ 8 }; ///< Configured circular-buffer length.
73 double m_maxCircBuffSize{ 16.0 }; ///< Configured circular-buffer size in MB.
74 size_t m_maxWriteChunkLength{ 4 }; ///< Configured write-chunk length.
75 double m_maxChunkTime{ 0.5 }; ///< Configured max chunk time in seconds.
76 double m_writeStopTimeout{ 1.0 }; ///< Configured stop-writing flush timeout.
77 bool m_startWriting{ false }; ///< Whether writing should start armed at startup.
78 int m_writerThreadPrio{ 0 }; ///< Configured writer thread priority.
79 std::string m_writerCpuset; ///< Optional writer cpuset.
80 bool m_compress{ true }; ///< Whether XRIF compression is enabled.
81 int m_lz4accel{ XRIF_LZ4_ACCEL_MIN }; ///< Configured LZ4 acceleration.
82 std::optional<std::string> m_outName; ///< Optional explicit output name.
83 std::optional<std::string> m_savePath; ///< Optional explicit save directory.
84 std::string m_shmimName{ "streamWriter_test_stream" }; ///< Shared-memory stream name.
85 int m_semaphoreNumber{ 5 }; ///< Shared-memory semaphore index.
86 unsigned m_semWaitNSec{ 1000000 }; ///< Semaphore timeout in nanoseconds.
87 bool m_warnMissedData{ false }; ///< Whether backlog summaries log warnings.
88 int m_framegrabberThreadPrio{ 0 }; ///< Configured framegrabber thread priority.
89 std::string m_framegrabberCpuset; ///< Optional framegrabber cpuset.
90 double m_telemeterMaxInterval{ 60.0 }; ///< Telemetry cadence in seconds.
91};
92
93/// RAII wrapper for a temporary uint16 ImageStreamIO stream used by `fgThreadExec()` tests.
94class tempStream
95{
96 public:
97 /// Create the temporary stream or throw if ImageStreamIO setup fails.
98 explicit tempStream( const std::string &name,
99 uint32_t width = 1,
100 uint32_t height = 1,
101 uint32_t depth = 1,
102 uint8_t dataType = _DATATYPE_UINT16 )
103 : m_name( name )
104 {
105 uint32_t imsize[3] = { width, height, depth };
106
107 ensureMilkShmDir();
108
109 if( ImageStreamIO_createIm_gpu( &m_image,
110 m_name.c_str(),
111 3,
112 imsize,
113 dataType,
114 -1,
115 1,
116 IMAGE_NB_SEMAPHORE,
117 0,
118 CIRCULAR_BUFFER | ZAXIS_TEMPORAL,
119 0 ) != IMAGESTREAMIO_SUCCESS )
120 {
121 throw std::runtime_error( "failed to create temporary ImageStreamIO stream" );
122 }
123
124 m_image.md[0].cnt0 = 0;
125 m_image.md[0].cnt1 = 0;
126 }
127
128 /// Destroy the temporary stream.
129 ~tempStream()
130 {
131 if( m_owner )
132 {
133 ImageStreamIO_destroyIm( &m_image );
134 }
135 }
136
137 /// Return the underlying temporary stream.
138 IMAGE *image()
139 {
140 return &m_image;
141 }
142
143 /// Return the number of pixels in one frame.
144 size_t pixelsPerFrame() const
145 {
146 return static_cast<size_t>( m_image.md[0].size[0] ) * static_cast<size_t>( m_image.md[0].size[1] );
147 }
148
149 /// Return the configured stream depth.
150 size_t depth() const
151 {
152 return static_cast<size_t>( m_image.md[0].size[2] );
153 }
154
155 /// Fill one frame with a deterministic ramp, update the metadata, and post the shmim semaphore.
156 void publishFrame(
157 size_t frameIndex, uint64_t cnt0, uint16_t baseValue, const timespec &atime, const timespec &writetime )
158 {
159 uint16_t *frame = reinterpret_cast<uint16_t *>( m_image.array.raw ) + frameIndex * pixelsPerFrame();
160
161 for( size_t n = 0; n < pixelsPerFrame(); ++n )
162 {
163 frame[n] = baseValue + n;
164 }
165
166 m_image.md[0].write = 1;
167 m_image.md[0].cnt0 = cnt0;
168 m_image.md[0].cnt1 = frameIndex;
169 m_image.md[0].atime = atime;
170 m_image.md[0].writetime = writetime;
171
172 m_image.cntarray[frameIndex] = cnt0;
173 m_image.atimearray[frameIndex] = atime;
174 m_image.writetimearray[frameIndex] = writetime;
175
176 m_image.md[0].write = 0;
177 ImageStreamIO_sempost( &m_image, -1 );
178 }
179
180 private:
181 std::string m_name; ///< Unique shmim name for the temporary stream.
182 IMAGE m_image{}; ///< ImageStreamIO handle for the temporary stream.
183 bool m_owner{ true }; ///< Whether this wrapper should destroy the underlying shmim on teardown.
184};
185
186/// Test harness exposing path setup and shutdown cleanup helpers.
187class streamWriterLifecycleTest : public streamWriter
188{
189 public:
190 /// Prepare an isolated MagAO-X directory tree for the named test case.
191 std::filesystem::path prepareSandbox( const std::string &name )
192 {
193 std::error_code ec;
194 std::filesystem::path root = std::filesystem::path( "/tmp/streamWriter_lifecycle_test" ) / name;
195
196 std::filesystem::remove_all( root, ec );
197 std::filesystem::create_directories( root / "config" );
198 std::filesystem::create_directories( root / "calib" );
199 std::filesystem::create_directories( root / "logs" );
200 std::filesystem::create_directories( root / "sys" );
201 std::filesystem::create_directories( root / "secrets" );
202 std::filesystem::create_directories( root / "telem" );
203 std::filesystem::create_directories( root / "cpuset" );
204
205 m_basePath = root.string();
206 m_configDir = ( root / "config" ).string();
207 m_calibDir = ( root / "calib" ).string();
208 m_sysPath = ( root / "sys" ).string();
209 m_secretsPath = ( root / "secrets" ).string();
210 m_cpusetPath = ( root / "cpuset" ).string();
211
212 m_configName = name;
213 m_outName = name;
214
215 m_log.logPath( ( root / "logs" ).string() );
216
217 return root;
218 }
219
220 /// Shut the worker threads down after a successful startup sequence.
221 int shutdownStartedApp()
222 {
223 m_shutdown = 1;
224 return appShutdown();
225 }
226
227 /// Prepare the direct `fgThreadExec()` harness resources without starting the writer thread.
228 int initializeFgHarness()
229 {
230 if( sem_init( &m_swSemaphore, 0, 0 ) < 0 )
231 {
232 return -1;
233 }
234
235 m_swSemaphoreInitialized = true;
236
237 return initialize_xrif();
238 }
239
240 /// Launch the production `fgThreadExec()` loop in the existing framegrabber thread slot.
241 void startFgHarnessThread()
242 {
243 m_fgThreadInit = false;
244 m_shutdown = 0;
245 m_restart = false;
246
247 m_fgThread = std::thread( &streamWriterLifecycleTest::fgThreadEntry, this );
248 }
249
250 /// Stop the direct `fgThreadExec()` harness and release any resources it owns.
251 void stopFgHarness()
252 {
253 m_writing = NOT_WRITING;
254 m_writePending = false;
255 m_restart = false;
256 m_shutdown = 1;
257
258 if( m_fgThread.joinable() )
259 {
260 m_fgThread.join();
261 }
262
263 release_circbufs();
264
265 if( m_swSemaphoreInitialized )
266 {
267 sem_destroy( &m_swSemaphore );
268 m_swSemaphoreInitialized = false;
269 }
270
271 if( m_xrif )
272 {
273 xrif_delete( m_xrif );
274 m_xrif = nullptr;
275 }
276
277 if( m_xrif_timing )
278 {
279 xrif_delete( m_xrif_timing );
280 m_xrif_timing = nullptr;
281 }
282 }
283
284 /// Return the current writer-semaphore value for assertions.
285 int writerSemaphoreValue()
286 {
287 int value = 0;
288 sem_getvalue( &m_swSemaphore, &value );
289 return value;
290 }
291
292 /// Drain the writer semaphore and return the number of queued posts removed.
293 int drainWriterSemaphore()
294 {
295 int drained = 0;
296
297 while( sem_trywait( &m_swSemaphore ) == 0 )
298 {
299 ++drained;
300 }
301
302 return drained;
303 }
304
305 private:
306 /// Thread entry trampoline for the direct framegrabber harness.
307 static void fgThreadEntry( streamWriterLifecycleTest *app )
308 {
309 app->fgThreadExec();
310 }
311
312 bool m_swSemaphoreInitialized{ false }; ///< Whether the direct harness initialized `m_swSemaphore`.
313};
314
315/// Cleanup helper that only shuts the app down if startup completed.
316class startupScope
317{
318 public:
319 /// Track a started `streamWriter` instance for cleanup.
320 explicit startupScope( streamWriterLifecycleTest &app ) : m_app( app )
321 {
322 }
323
324 /// Mark whether `appStartup()` succeeded.
325 void markStarted( bool started )
326 {
327 m_started = started;
328 }
329
330 /// Stop tracking once explicit shutdown has already been performed.
331 void disarm()
332 {
333 m_started = false;
334 }
335
336 /// Shut the app down if it was started successfully.
337 ~startupScope()
338 {
339 if( m_started )
340 {
341 static_cast<void>( m_app.shutdownStartedApp() );
342 }
343 }
344
345 private:
346 streamWriterLifecycleTest &m_app; ///< App instance being protected.
347 bool m_started{ false }; ///< Whether startup completed successfully.
348};
349
350/// Cleanup helper that stops the direct framegrabber harness on scope exit.
351class fgHarnessScope
352{
353 public:
354 /// Track a direct `fgThreadExec()` harness instance for cleanup.
355 explicit fgHarnessScope( streamWriterLifecycleTest &app ) : m_app( app )
356 {
357 }
358
359 /// Mark whether the direct harness was initialized and needs teardown.
360 void markActive( bool active )
361 {
362 m_active = active;
363 }
364
365 /// Stop tracking once teardown has already been handled explicitly.
366 void disarm()
367 {
368 m_active = false;
369 }
370
371 /// Stop the harness if it is still active at scope exit.
372 ~fgHarnessScope()
373 {
374 if( m_active )
375 {
376 m_app.stopFgHarness();
377 }
378 }
379
380 private:
381 streamWriterLifecycleTest &m_app; ///< App instance being protected.
382 bool m_active{ false }; ///< Whether the direct harness needs teardown.
383};
384
385/// Write and load a deterministic streamWriter config for the provided test app.
386std::filesystem::path loadConfig( streamWriterLifecycleTest &app,
387 const std::string &name,
388 const streamWriterConfig &cfg = streamWriterConfig() )
389{
390 const std::filesystem::path root = app.prepareSandbox( name );
391 const std::filesystem::path configPath = root / "config" / ( name + ".conf" );
392
393 app.setupConfig();
394
395 std::vector<std::string> sections{ "writer",
396 "writer",
397 "writer",
398 "writer",
399 "writer",
400 "writer",
401 "writer",
402 "writer",
403 "writer",
404 "framegrabber",
405 "framegrabber",
406 "framegrabber",
407 "framegrabber",
408 "framegrabber",
409 "telemeter" };
410
411 std::vector<std::string> keys{ "maxCircBuffLength",
412 "maxCircBuffSize",
413 "maxWriteChunkLength",
414 "maxChunkTime",
415 "stopTimeout",
416 "startWriting",
417 "threadPrio",
418 "compress",
419 "lz4accel",
420 "shmimName",
421 "semaphoreNumber",
422 "semWait",
423 "warnMissedData",
424 "threadPrio",
425 "maxInterval" };
426
427 std::vector<std::string> values{ std::to_string( cfg.m_maxCircBuffLength ),
428 std::to_string( cfg.m_maxCircBuffSize ),
429 std::to_string( cfg.m_maxWriteChunkLength ),
430 std::to_string( cfg.m_maxChunkTime ),
431 std::to_string( cfg.m_writeStopTimeout ),
432 cfg.m_startWriting ? "1" : "0",
433 std::to_string( cfg.m_writerThreadPrio ),
434 cfg.m_compress ? "1" : "0",
435 std::to_string( cfg.m_lz4accel ),
436 cfg.m_shmimName,
437 std::to_string( cfg.m_semaphoreNumber ),
438 std::to_string( cfg.m_semWaitNSec ),
439 cfg.m_warnMissedData ? "1" : "0",
440 std::to_string( cfg.m_framegrabberThreadPrio ),
441 std::to_string( cfg.m_telemeterMaxInterval ) };
442
443 if( !cfg.m_writerCpuset.empty() )
444 {
445 sections.push_back( "writer" );
446 keys.push_back( "cpuset" );
447 values.push_back( cfg.m_writerCpuset );
448 }
449
450 if( !cfg.m_framegrabberCpuset.empty() )
451 {
452 sections.push_back( "framegrabber" );
453 keys.push_back( "cpuset" );
454 values.push_back( cfg.m_framegrabberCpuset );
455 }
456
457 if( cfg.m_outName )
458 {
459 sections.push_back( "writer" );
460 keys.push_back( "outName" );
461 values.push_back( *cfg.m_outName );
462 }
463
464 if( cfg.m_savePath )
465 {
466 sections.push_back( "writer" );
467 keys.push_back( "savePath" );
468 values.push_back( *cfg.m_savePath );
469 }
470
471 mx::app::writeConfigFile( configPath.string(), sections, keys, values );
472
473 app.config.readConfig( configPath.string() );
474 app.loadConfig();
475
476 return root;
477}
478
479/// Wait for a test predicate to become true within a fixed timeout.
480template <typename PredicateT>
481bool waitFor( PredicateT predicate, int timeoutMs = 2000 )
482{
483 const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds( timeoutMs );
484
485 while( std::chrono::steady_clock::now() < deadline )
486 {
487 if( predicate() )
488 {
489 return true;
490 }
491
492 std::this_thread::sleep_for( std::chrono::milliseconds( 5 ) );
493 }
494
495 return predicate();
496}
497
498/// Return the current realtime clock value for timestamp-driven framegrabber tests.
499timespec currentRealtime()
500{
501 timespec ts{ 0, 0 };
502
503 if( clock_gettime( CLOCK_REALTIME, &ts ) < 0 )
504 {
505 throw std::runtime_error( "clock_gettime failed while preparing streamWriter test timestamps" );
506 }
507
508 return ts;
509}
510
511/// Return a timestamp offset by the requested nanoseconds.
512timespec offsetTimespec( const timespec &base, long long nanoseconds )
513{
514 timespec shifted = base;
515
516 shifted.tv_sec += nanoseconds / 1000000000LL;
517 shifted.tv_nsec += nanoseconds % 1000000000LL;
518
519 while( shifted.tv_nsec >= 1000000000L )
520 {
521 shifted.tv_nsec -= 1000000000L;
522 ++shifted.tv_sec;
523 }
524
525 while( shifted.tv_nsec < 0 )
526 {
527 shifted.tv_nsec += 1000000000L;
528 --shifted.tv_sec;
529 }
530
531 return shifted;
532}
533
534/// Return one uint16 pixel from the frame stored in the streamWriter circular buffer.
535uint16_t rawFrameWord( const streamWriterLifecycleTest &app, size_t frameIndex, size_t pixelIndex )
536{
537 return reinterpret_cast<uint16_t *>( app.m_rawImageCircBuff )[frameIndex * app.m_width * app.m_height + pixelIndex];
538}
539
540/// Return one timing word from the streamWriter timing circular buffer.
541uint64_t timingWord( const streamWriterLifecycleTest &app, size_t frameIndex, size_t timingIndex )
542{
543 return app.m_timingCircBuff[frameIndex * 5 + timingIndex];
544}
545
546/// Copy the currently scheduled raw-image save window for assertions before a restart frees the buffers.
547std::vector<uint16_t> copyPendingRawFrames( const streamWriterLifecycleTest &app )
548{
549 const size_t pixelsPerFrame = app.m_width * app.m_height;
550 const size_t nFrames = app.m_currSaveStop - app.m_currSaveStart;
551 auto *rawStart = reinterpret_cast<uint16_t *>( app.m_rawImageCircBuff ) + app.m_currSaveStart * pixelsPerFrame;
552
553 return std::vector<uint16_t>( rawStart, rawStart + nFrames * pixelsPerFrame );
554}
555
556/// Copy the currently scheduled timing save window for assertions before a restart frees the buffers.
557std::vector<uint64_t> copyPendingTimingFrames( const streamWriterLifecycleTest &app )
558{
559 const size_t nFrames = app.m_currSaveStop - app.m_currSaveStart;
560 auto *timingStart = app.m_timingCircBuff + app.m_currSaveStart * 5;
561
562 return std::vector<uint64_t>( timingStart, timingStart + nFrames * 5 );
563}
564
565} // namespace
566
567/// Verify `setupConfig()` and `loadConfig()` preserve defaults, overrides, and clamp invalid accelerations.
568/**
569 * \ingroup streamWriter_unit_test
570 */
571TEST_CASE( "streamWriter configuration loads defaults and overrides", "[streamWriter]" )
572{
573 // clang-format off
574 #ifdef STREAMWRITER_TEST_DOXYGEN_REF
577 #endif
578 // clang-format on
579
580 SECTION( "default-derived paths and names come from the shared-memory stream" )
581 {
582 streamWriterLifecycleTest app;
583 streamWriterConfig cfg;
584
585 cfg.m_lz4accel = XRIF_LZ4_ACCEL_MIN - 5;
586
587 const std::filesystem::path root = loadConfig( app, "load_defaults", cfg );
588
589 std::string expectedRawRel = mx::sys::getEnv( MAGAOX_env_rawimage );
590 if( expectedRawRel.empty() )
591 {
592 expectedRawRel = MAGAOX_rawimageRelPath;
593 }
594
595 REQUIRE( app.m_maxCircBuffLength == 8 );
596 REQUIRE( app.m_maxCircBuffSize == Approx( 16.0 ) );
597 REQUIRE( app.m_maxWriteChunkLength == 4 );
598 REQUIRE( app.m_maxChunkTime == Approx( 0.5 ) );
599 REQUIRE( app.m_writeStopTimeout == Approx( 1.0 ) );
600 REQUIRE( app.m_startWriting == false );
601 REQUIRE( app.m_shmimName == "streamWriter_test_stream" );
602 REQUIRE( app.m_outName == app.m_shmimName );
603 REQUIRE( app.m_rawimageDir == ( root / expectedRawRel ).string() );
604 REQUIRE( app.m_semaphoreNumber == 5 );
605 REQUIRE( app.m_semWaitNSec == 1000000 );
606 REQUIRE( app.m_warnMissedData == false );
607 REQUIRE( app.m_swThreadPrio == 0 );
608 REQUIRE( app.m_fgThreadPrio == 0 );
609 REQUIRE( app.m_lz4accel == XRIF_LZ4_ACCEL_MIN );
610 REQUIRE( app.m_shutdown == 0 );
611 }
612
613 SECTION( "explicit overrides replace the defaults and high accelerations clamp to the XRIF limit" )
614 {
615 streamWriterLifecycleTest app;
616 streamWriterConfig cfg;
617
618 cfg.m_outName = "camsci";
619 cfg.m_savePath = ( std::filesystem::path( "/tmp/streamWriter_lifecycle_test_override" ) / "science" ).string();
620 cfg.m_compress = false;
621 cfg.m_lz4accel = XRIF_LZ4_ACCEL_MAX + 17;
622 cfg.m_startWriting = true;
623 cfg.m_writeStopTimeout = 0.25;
624 cfg.m_writerThreadPrio = 3;
625 cfg.m_framegrabberThreadPrio = 2;
626
627 loadConfig( app, "load_override", cfg );
628
629 REQUIRE( app.m_outName == "camsci" );
630 REQUIRE( app.m_rawimageDir == *cfg.m_savePath );
631 REQUIRE( app.m_compress == false );
632 REQUIRE( app.m_lz4accel == XRIF_LZ4_ACCEL_MAX );
633 REQUIRE( app.m_startWriting == true );
634 REQUIRE( app.m_writeStopTimeout == Approx( 0.25 ) );
635 REQUIRE( app.m_swThreadPrio == 3 );
636 REQUIRE( app.m_fgThreadPrio == 2 );
637 }
638}
639
640/// Verify `appStartup()`, `appLogic()`, and `appShutdown()` cover the basic streamWriter lifecycle.
641/**
642 * \ingroup streamWriter_unit_test
643 */
644TEST_CASE( "streamWriter lifecycle handles startup validation and nominal shutdown", "[streamWriter]" )
645{
646 // clang-format off
647 #ifdef STREAMWRITER_TEST_DOXYGEN_REF
651 #endif
652 // clang-format on
653
654 SECTION( "appStartup fails when the save directory cannot be created" )
655 {
656 streamWriterLifecycleTest app;
657
658 const std::filesystem::path root = loadConfig( app, "startup_bad_save_path" );
659 const std::filesystem::path blocker = root / "save_parent_file";
660
661 std::ofstream blockerOut( blocker.string() );
662 blockerOut << "block";
663 blockerOut.close();
664
665 app.m_rawimageDir = ( blocker / "child" ).string();
666
667 REQUIRE( app.appStartup() < 0 );
668 }
669
670 SECTION( "appStartup fails when the write chunk length is not a divisor of the circular buffer length" )
671 {
672 streamWriterLifecycleTest app;
673
674 loadConfig( app, "startup_bad_chunk_divisor" );
675 app.m_maxCircBuffLength = 10;
676 app.m_maxWriteChunkLength = 4;
677
678 REQUIRE( app.appStartup() < 0 );
679 }
680
681 SECTION( "appStartup, appLogic, and appShutdown complete a nominal idle lifecycle" )
682 {
683 streamWriterLifecycleTest app;
684 startupScope startup( app );
685 streamWriterConfig cfg;
686
687 cfg.m_savePath =
688 ( std::filesystem::path( "/tmp/streamWriter_lifecycle_test" ) / "startup_success" / "raw" ).string();
689
690 loadConfig( app, "startup_success", cfg );
691
692 const int startupRv = app.appStartup();
693 startup.markStarted( startupRv == 0 );
694
695 REQUIRE( startupRv == 0 );
696 REQUIRE( std::filesystem::exists( *cfg.m_savePath ) );
697 REQUIRE( app.m_fgThread.joinable() );
698 REQUIRE( app.m_swThread.joinable() );
699 REQUIRE( app.m_xrif != nullptr );
700 REQUIRE( app.m_xrif_timing != nullptr );
701
702 REQUIRE( app.appLogic() == 0 );
703 REQUIRE( app.state() == stateCodes::READY );
704
705 app.m_writing = WRITING;
706
707 REQUIRE( app.appLogic() == 0 );
708 REQUIRE( app.state() == stateCodes::OPERATING );
709
710 REQUIRE( app.shutdownStartedApp() == 0 );
711 startup.disarm();
712
713 REQUIRE( app.m_xrif == nullptr );
714 REQUIRE( app.m_xrif_timing == nullptr );
715 REQUIRE( app.m_fgThread.joinable() == false );
716 REQUIRE( app.m_swThread.joinable() == false );
717 REQUIRE( app.m_writing == NOT_WRITING );
718 }
719
720 SECTION( "appStartup arms writing immediately when configured to start writing" )
721 {
722 streamWriterLifecycleTest app;
723 startupScope startup( app );
724 streamWriterConfig cfg;
725
726 cfg.m_startWriting = true;
727 cfg.m_savePath =
728 ( std::filesystem::path( "/tmp/streamWriter_lifecycle_test" ) / "startup_start_writing" / "raw" ).string();
729
730 loadConfig( app, "startup_start_writing", cfg );
731
732 const int startupRv = app.appStartup();
733 startup.markStarted( startupRv == 0 );
734
735 REQUIRE( startupRv == 0 );
736 REQUIRE( app.m_writing == START_WRITING );
737
738 REQUIRE( app.appLogic() == 0 );
739 REQUIRE( app.state() == stateCodes::OPERATING );
740 }
741}
742
743/// Verify streamWriter publishes backlog summaries, INDI status, and save telemetry from the main loop.
744/**
745 * \ingroup streamWriter_unit_test
746 */
747TEST_CASE( "streamWriter appLogic reports backlog and save status", "[streamWriter]" )
748{
749 // clang-format off
750 #ifdef STREAMWRITER_TEST_DOXYGEN_REF
756 #endif
757 // clang-format on
758
759 SECTION( "appLogic doubles the backlog summary interval while skips persist and resets when idle" )
760 {
761 streamWriterLifecycleTest app;
762 startupScope startup( app );
763 streamWriterConfig cfg;
764
765 cfg.m_savePath =
766 ( std::filesystem::path( "/tmp/streamWriter_lifecycle_test" ) / "backlog_summary" / "raw" ).string();
767
768 loadConfig( app, "backlog_summary", cfg );
769
770 const int startupRv = app.appStartup();
771 startup.markStarted( startupRv == 0 );
772 REQUIRE( startupRv == 0 );
773
774 app.m_skipSummaryIntervalSec = 10.0;
775 app.m_nextSkipSummaryTime = 0;
776
777 REQUIRE( app.appLogic() == 0 );
778 REQUIRE( app.m_skipSummaryIntervalSec == Approx( 10.0 ) );
779 REQUIRE( app.m_nextSkipSummaryTime > mx::sys::get_curr_time() );
780
781 app.m_skippedFrameCount.store( 4 );
782 app.m_repeatSemaphoreCount.store( 2 );
783 app.m_nextSkipSummaryTime = mx::sys::get_curr_time() - 1.0;
784
785 REQUIRE( app.appLogic() == 0 );
786 REQUIRE( app.m_skippedFrameCount.load() == 0 );
787 REQUIRE( app.m_repeatSemaphoreCount.load() == 0 );
788 REQUIRE( app.m_skipSummaryIntervalSec == Approx( 20.0 ) );
789 REQUIRE( app.m_nextSkipSummaryTime > mx::sys::get_curr_time() );
790
791 app.m_nextSkipSummaryTime = mx::sys::get_curr_time() - 1.0;
792
793 REQUIRE( app.appLogic() == 0 );
794 REQUIRE( app.m_skipSummaryIntervalSec == Approx( 10.0 ) );
795 }
796
797 SECTION( "save telemetry helpers accept idle and active writing statistics" )
798 {
799 streamWriterLifecycleTest app;
800 startupScope startup( app );
801 streamWriterConfig cfg;
802
803 cfg.m_savePath =
804 ( std::filesystem::path( "/tmp/streamWriter_lifecycle_test" ) / "indi_reporting" / "raw" ).string();
805
806 loadConfig( app, "indi_reporting", cfg );
807
808 const int startupRv = app.appStartup();
809 startup.markStarted( startupRv == 0 );
810 REQUIRE( startupRv == 0 );
811
812 app.m_writing = NOT_WRITING;
813 app.updateINDI();
814 REQUIRE( app.recordSavingState( true ) == 0 );
815
816 app.m_width = 100;
817 app.m_height = 100;
818 app.m_typeSize = 2;
819
820 app.m_xrif->compression_ratio = 2.5;
821 app.m_xrif->encode_rate = 200000.0;
822 app.m_xrif->difference_rate = 100000.0;
823 app.m_xrif->reorder_rate = 60000.0;
824 app.m_xrif->compress_rate = 40000.0;
825
826 app.m_writing = WRITING;
827 app.updateINDI();
828
829 app.m_currSaveStart = 7;
830
831 REQUIRE( app.recordSavingState( true ) == 0 );
832 REQUIRE( app.recordSavingStats( true ) == 0 );
833 REQUIRE( app.recordTelem( nullptr ) == 0 );
834 REQUIRE( app.checkRecordTimes() == 0 );
835 }
836}
837
838/// Verify `fgThreadExec()` ingests shmim frames, tracks gaps, and schedules save work.
839/**
840 * \ingroup streamWriter_unit_test
841 */
842TEST_CASE( "streamWriter fgThreadExec ingests stream data and manages write scheduling", "[streamWriter]" )
843{
844 // clang-format off
845 #ifdef STREAMWRITER_TEST_DOXYGEN_REF
849 #endif
850 // clang-format on
851
852 SECTION( "cube streams populate frame arrays, skipped-frame counters, and missing timestamps" )
853 {
854 streamWriterLifecycleTest app;
855 fgHarnessScope fgScope( app );
856 streamWriterConfig cfg;
857
858 cfg.m_shmimName = uniqueShmimName( "cube_ingest" );
859 cfg.m_maxCircBuffLength = 8;
860 cfg.m_maxWriteChunkLength = 4;
861 cfg.m_maxChunkTime = 0.25;
862 cfg.m_semWaitNSec = 1000000;
863 cfg.m_savePath = ( std::filesystem::path( "/tmp/streamWriter_lifecycle_test" ) / "fg_cube" / "raw" ).string();
864
865 tempStream source( cfg.m_shmimName, 2, 2, 8 );
866
867 loadConfig( app, "fg_cube_ingest", cfg );
868 REQUIRE( app.initializeFgHarness() == 0 );
869 fgScope.markActive( true );
870
871 app.startFgHarnessThread();
872
873 REQUIRE( waitFor( [&app]() { return app.m_rawImageCircBuff != nullptr && app.m_timingCircBuff != nullptr; } ) );
874 REQUIRE( app.m_width == 2 );
875 REQUIRE( app.m_height == 2 );
876 REQUIRE( app.m_dataType == _DATATYPE_UINT16 );
877 REQUIRE( app.m_typeSize == sizeof( uint16_t ) );
878 REQUIRE( app.m_circBuffLength == 8 );
879 REQUIRE( app.m_writeChunkLength == 4 );
880
881 const timespec atime0{ 111, 222 };
882 const timespec wtime0{ 333, 444 };
883 source.publishFrame( 0, 1, 100, atime0, wtime0 );
884
885 REQUIRE( waitFor( [&app]() { return app.m_currImage == 1; } ) );
886 REQUIRE( rawFrameWord( app, 0, 0 ) == 100 );
887 REQUIRE( rawFrameWord( app, 0, 3 ) == 103 );
888 REQUIRE( timingWord( app, 0, 0 ) == 1 );
889 REQUIRE( timingWord( app, 0, 1 ) == static_cast<uint64_t>( atime0.tv_sec ) );
890 REQUIRE( timingWord( app, 0, 2 ) == static_cast<uint64_t>( atime0.tv_nsec ) );
891 REQUIRE( timingWord( app, 0, 3 ) == static_cast<uint64_t>( wtime0.tv_sec ) );
892 REQUIRE( timingWord( app, 0, 4 ) == static_cast<uint64_t>( wtime0.tv_nsec ) );
893
894 source.publishFrame( 0, 1, 200, atime0, wtime0 );
895
896 REQUIRE( waitFor( [&app]() { return app.m_repeatSemaphoreCount.load() == 1; } ) );
897 REQUIRE( app.m_currImage == 1 );
898 REQUIRE( rawFrameWord( app, 0, 0 ) == 100 );
899
900 const timespec missingTime{ 0, 0 };
901 source.publishFrame( 1, 3, 300, missingTime, missingTime );
902
903 REQUIRE( waitFor( [&app]() { return app.m_currImage == 2; } ) );
904 REQUIRE( app.m_skippedFrameCount.load() == 1 );
905 REQUIRE( rawFrameWord( app, 1, 0 ) == 300 );
906 REQUIRE( rawFrameWord( app, 1, 3 ) == 303 );
907 REQUIRE( timingWord( app, 1, 0 ) == 3 );
908 REQUIRE( timingWord( app, 1, 1 ) != 0 );
909 REQUIRE( timingWord( app, 1, 3 ) == timingWord( app, 1, 1 ) );
910 REQUIRE( timingWord( app, 1, 4 ) == timingWord( app, 1, 2 ) );
911 REQUIRE( app.m_currImageTime != 0 );
912
913 app.stopFgHarness();
914 fgScope.disarm();
915 }
916
917 SECTION( "cube streams post save work on chunk boundaries, timeout flushes, and stop requests" )
918 {
919 streamWriterLifecycleTest app;
920 fgHarnessScope fgScope( app );
921 streamWriterConfig cfg;
922
923 cfg.m_shmimName = uniqueShmimName( "cube_writing" );
924 cfg.m_maxCircBuffLength = 8;
925 cfg.m_maxWriteChunkLength = 2;
926 cfg.m_maxChunkTime = 0.2;
927 cfg.m_writeStopTimeout = 0.05;
928 cfg.m_semWaitNSec = 1000000;
929 cfg.m_savePath =
930 ( std::filesystem::path( "/tmp/streamWriter_lifecycle_test" ) / "fg_writing" / "raw" ).string();
931
932 tempStream source( cfg.m_shmimName, 2, 2, 8 );
933
934 loadConfig( app, "fg_cube_writing", cfg );
935 REQUIRE( app.initializeFgHarness() == 0 );
936 fgScope.markActive( true );
937
938 app.startFgHarnessThread();
939
940 REQUIRE( waitFor( [&app]() { return app.m_rawImageCircBuff != nullptr; } ) );
941
942 const timespec baseAtime = currentRealtime();
943 const timespec baseWtime = offsetTimespec( baseAtime, 1000 );
944
945 app.m_writing = START_WRITING;
946 source.publishFrame( 0, 1, 10, baseAtime, baseWtime );
947 REQUIRE( waitFor( [&app]() { return app.m_currImage == 1; } ) );
948 REQUIRE( app.m_writing == WRITING );
949
950 source.publishFrame( 1, 2, 20, offsetTimespec( baseAtime, 1000000 ), offsetTimespec( baseWtime, 1000000 ) );
951 REQUIRE( waitFor( [&app]() { return app.writerSemaphoreValue() > 0; } ) );
952 REQUIRE( app.m_currSaveStart == 0 );
953 REQUIRE( app.m_currSaveStop == 2 );
954 REQUIRE( app.m_currSaveStopFrameNo == 2 );
955 REQUIRE( app.drainWriterSemaphore() >= 1 );
956
957 const timespec stopAtime = currentRealtime();
958 const timespec stopWtime = offsetTimespec( stopAtime, 1000 );
959 app.m_writing = START_WRITING;
960 source.publishFrame( 2, 3, 30, stopAtime, stopWtime );
961 REQUIRE( waitFor( [&app]() { return app.m_currImage == 3; } ) );
962 REQUIRE( app.m_writing == WRITING );
963
964 app.m_writing = STOP_WRITING;
965 app.m_stopWriteDeadline = mx::sys::get_curr_time() + cfg.m_writeStopTimeout;
966 source.publishFrame( 3, 4, 40, offsetTimespec( stopAtime, 1000000 ), offsetTimespec( stopWtime, 1000000 ) );
967 REQUIRE( waitFor( [&app]() { return app.writerSemaphoreValue() > 0; } ) );
968 REQUIRE( app.m_currSaveStart == 2 );
969 REQUIRE( app.m_currSaveStop == 4 );
970 REQUIRE( app.m_currSaveStopFrameNo == 4 );
971 REQUIRE( app.drainWriterSemaphore() >= 1 );
972
973 const timespec timeoutStopAtime = currentRealtime();
974 const timespec timeoutStopWtime = offsetTimespec( timeoutStopAtime, 1000 );
975 app.m_writing = START_WRITING;
976 source.publishFrame( 4, 5, 50, timeoutStopAtime, timeoutStopWtime );
977 REQUIRE( waitFor( [&app]() { return app.m_currImage == 5; } ) );
978 REQUIRE( app.m_writing == WRITING );
979
980 app.m_writing = STOP_WRITING;
981 app.m_stopWriteDeadline = mx::sys::get_curr_time() + cfg.m_writeStopTimeout;
982 std::this_thread::sleep_for( std::chrono::milliseconds( 10 ) );
983 REQUIRE( app.writerSemaphoreValue() == 0 );
984 REQUIRE( waitFor( [&app]() { return app.writerSemaphoreValue() > 0; } ) );
985 REQUIRE( app.m_currSaveStart == 4 );
986 REQUIRE( app.m_currSaveStop == 5 );
987 REQUIRE( app.m_currSaveStopFrameNo == 5 );
988 REQUIRE( app.drainWriterSemaphore() >= 1 );
989
990 app.m_writing = NOT_WRITING;
991 app.stopFgHarness();
992 fgScope.disarm();
993 }
994
995 SECTION( "cube streams flush partial chunks on timeout without dropping the queued frame" )
996 {
997 streamWriterLifecycleTest app;
998 fgHarnessScope fgScope( app );
999 streamWriterConfig cfg;
1000
1001 cfg.m_shmimName = uniqueShmimName( "cube_timeout" );
1002 cfg.m_maxCircBuffLength = 8;
1003 cfg.m_maxWriteChunkLength = 4;
1004 cfg.m_maxChunkTime = 0.2;
1005 cfg.m_semWaitNSec = 1000000;
1006 cfg.m_savePath =
1007 ( std::filesystem::path( "/tmp/streamWriter_lifecycle_test" ) / "fg_timeout" / "raw" ).string();
1008
1009 tempStream source( cfg.m_shmimName, 2, 2, 8 );
1010
1011 loadConfig( app, "fg_cube_timeout", cfg );
1012 REQUIRE( app.initializeFgHarness() == 0 );
1013 fgScope.markActive( true );
1014
1015 app.startFgHarnessThread();
1016
1017 REQUIRE( waitFor( [&app]() { return app.m_rawImageCircBuff != nullptr; } ) );
1018
1019 const timespec timeoutAtime = currentRealtime();
1020 const timespec timeoutWtime = offsetTimespec( timeoutAtime, 1000 );
1021
1022 app.m_writing = START_WRITING;
1023 source.publishFrame( 0, 1, 10, timeoutAtime, timeoutWtime );
1024 REQUIRE( waitFor( [&app]() { return app.m_currImage == 1; } ) );
1025 REQUIRE( app.m_writing == WRITING );
1026
1027 REQUIRE( waitFor( [&app]() { return app.writerSemaphoreValue() == 1 && app.m_writing == START_WRITING; } ) );
1028 REQUIRE( app.m_currSaveStart == 0 );
1029 REQUIRE( app.m_currSaveStop == 1 );
1030 REQUIRE( app.m_currSaveStopFrameNo == 1 );
1031 REQUIRE( rawFrameWord( app, 0, 0 ) == 10 );
1032 REQUIRE( rawFrameWord( app, 0, 3 ) == 13 );
1033 REQUIRE( app.drainWriterSemaphore() == 1 );
1034
1035 app.m_writing = NOT_WRITING;
1036 app.stopFgHarness();
1037 fgScope.disarm();
1038 }
1039
1040 SECTION( "replaced shmims flush the pending chunk and reconnect ready to keep writing" )
1041 {
1042 streamWriterLifecycleTest app;
1043 fgHarnessScope fgScope( app );
1044 streamWriterConfig cfg;
1045 std::unique_ptr<tempStream> source;
1046
1047 cfg.m_shmimName = uniqueShmimName( "cube_restart" );
1048 cfg.m_maxCircBuffLength = 8;
1049 cfg.m_maxWriteChunkLength = 4;
1050 cfg.m_maxChunkTime = 10.0;
1051 cfg.m_semWaitNSec = 1000000;
1052 cfg.m_savePath =
1053 ( std::filesystem::path( "/tmp/streamWriter_lifecycle_test" ) / "fg_restart" / "raw" ).string();
1054
1055 source = std::make_unique<tempStream>( cfg.m_shmimName, 2, 2, 8 );
1056
1057 loadConfig( app, "fg_cube_restart", cfg );
1058 REQUIRE( app.initializeFgHarness() == 0 );
1059 fgScope.markActive( true );
1060
1061 app.startFgHarnessThread();
1062
1063 REQUIRE( waitFor( [&app]()
1064 { return app.m_rawImageCircBuff != nullptr && app.m_width == 2 && app.m_height == 2; } ) );
1065
1066 const timespec baseAtime = currentRealtime();
1067 const timespec baseWtime = offsetTimespec( baseAtime, 1000 );
1068 const timespec nextAtime = offsetTimespec( baseAtime, 1000000 );
1069 const timespec nextWtime = offsetTimespec( baseWtime, 1000000 );
1070
1071 app.m_writing = START_WRITING;
1072 source->publishFrame( 0, 1, 100, baseAtime, baseWtime );
1073 REQUIRE( waitFor( [&app]() { return app.m_currImage == 1; } ) );
1074 REQUIRE( app.m_writing == WRITING );
1075
1076 source->publishFrame( 1, 2, 200, nextAtime, nextWtime );
1077 REQUIRE( waitFor( [&app]() { return app.m_currImage == 2; } ) );
1078 REQUIRE( app.writerSemaphoreValue() == 0 );
1079
1080 source.reset();
1081 source = std::make_unique<tempStream>( cfg.m_shmimName, 3, 1, 8 );
1082
1083 REQUIRE(
1084 waitFor( [&app]() { return app.writerSemaphoreValue() > 0 && app.m_writing == STOP_WRITING; }, 3000 ) );
1085 REQUIRE( app.m_currSaveStart == 0 );
1086 REQUIRE( app.m_currSaveStop == 2 );
1087 REQUIRE( app.m_currSaveStopFrameNo == 2 );
1088
1089 const std::vector<uint16_t> pendingRaw = copyPendingRawFrames( app );
1090 const std::vector<uint64_t> pendingTiming = copyPendingTimingFrames( app );
1091
1092 REQUIRE( pendingRaw == std::vector<uint16_t>{ 100, 101, 102, 103, 200, 201, 202, 203 } );
1093 REQUIRE( pendingTiming == std::vector<uint64_t>{ 1,
1094 static_cast<uint64_t>( baseAtime.tv_sec ),
1095 static_cast<uint64_t>( baseAtime.tv_nsec ),
1096 static_cast<uint64_t>( baseWtime.tv_sec ),
1097 static_cast<uint64_t>( baseWtime.tv_nsec ),
1098 2,
1099 static_cast<uint64_t>( nextAtime.tv_sec ),
1100 static_cast<uint64_t>( nextAtime.tv_nsec ),
1101 static_cast<uint64_t>( nextWtime.tv_sec ),
1102 static_cast<uint64_t>( nextWtime.tv_nsec ) } );
1103
1104 app.m_writing = START_WRITING;
1105 app.m_resumeAfterReconnect = true;
1106 app.m_writePending = false;
1107 REQUIRE( app.drainWriterSemaphore() >= 1 );
1108
1109 REQUIRE( waitFor( [&app]() { return app.m_width == 3 && app.m_height == 1 && app.m_writing == START_WRITING; },
1110 3000 ) );
1111 REQUIRE( app.writerSemaphoreValue() == 0 );
1112
1113 const timespec reconnectedAtime = currentRealtime();
1114 const timespec reconnectedWtime = offsetTimespec( reconnectedAtime, 1000 );
1115 source->publishFrame( 0, 101, 500, reconnectedAtime, reconnectedWtime );
1116
1117 REQUIRE( waitFor( [&app]() { return app.m_currImage == 1; } ) );
1118 REQUIRE( app.m_writing == WRITING );
1119 REQUIRE( rawFrameWord( app, 0, 0 ) == 500 );
1120 REQUIRE( rawFrameWord( app, 0, 2 ) == 502 );
1121 REQUIRE( timingWord( app, 0, 0 ) == 101 );
1122 REQUIRE( app.writerSemaphoreValue() == 0 );
1123
1124 app.stopFgHarness();
1125 fgScope.disarm();
1126 }
1127
1128 SECTION( "restart cleanup times out instead of hanging when no writer thread drains the queued flush" )
1129 {
1130 streamWriterLifecycleTest app;
1131 fgHarnessScope fgScope( app );
1132 streamWriterConfig cfg;
1133 std::unique_ptr<tempStream> source;
1134
1135 cfg.m_shmimName = uniqueShmimName( "cube_restart_timeout" );
1136 cfg.m_maxCircBuffLength = 8;
1137 cfg.m_maxWriteChunkLength = 4;
1138 cfg.m_maxChunkTime = 10.0;
1139 cfg.m_writeStopTimeout = 0.1;
1140 cfg.m_semWaitNSec = 1000000;
1141 cfg.m_savePath =
1142 ( std::filesystem::path( "/tmp/streamWriter_lifecycle_test" ) / "fg_restart_timeout" / "raw" ).string();
1143
1144 source = std::make_unique<tempStream>( cfg.m_shmimName, 2, 2, 8 );
1145
1146 loadConfig( app, "fg_cube_restart_timeout", cfg );
1147 REQUIRE( app.initializeFgHarness() == 0 );
1148 fgScope.markActive( true );
1149 app.m_writeCompletionTimeout = 0.1;
1150
1151 app.startFgHarnessThread();
1152
1153 REQUIRE( waitFor( [&app]()
1154 { return app.m_rawImageCircBuff != nullptr && app.m_width == 2 && app.m_height == 2; } ) );
1155
1156 const timespec baseAtime = currentRealtime();
1157 const timespec baseWtime = offsetTimespec( baseAtime, 1000 );
1158
1159 app.m_writing = START_WRITING;
1160 source->publishFrame( 0, 1, 100, baseAtime, baseWtime );
1161 REQUIRE( waitFor( [&app]() { return app.m_currImage == 1; } ) );
1162 source->publishFrame( 1, 2, 200, offsetTimespec( baseAtime, 1000000 ), offsetTimespec( baseWtime, 1000000 ) );
1163 REQUIRE( waitFor( [&app]() { return app.m_currImage == 2; } ) );
1164 REQUIRE( app.m_writing == WRITING );
1165 REQUIRE( app.writerSemaphoreValue() == 0 );
1166
1167 source.reset();
1168 source = std::make_unique<tempStream>( cfg.m_shmimName, 3, 1, 8 );
1169
1170 REQUIRE( waitFor( [&app]() { return app.m_shutdown != 0; }, 3000 ) );
1171 REQUIRE( app.m_writing == STOP_WRITING );
1172 REQUIRE( app.m_writePending == true );
1173 REQUIRE( app.writerSemaphoreValue() > 0 );
1174
1175 app.stopFgHarness();
1176 fgScope.disarm();
1177 }
1178
1179 SECTION( "two-dimensional streams ingest metadata from the shared image header" )
1180 {
1181 streamWriterLifecycleTest app;
1182 fgHarnessScope fgScope( app );
1183 streamWriterConfig cfg;
1184
1185 cfg.m_shmimName = uniqueShmimName( "image2d" );
1186 cfg.m_maxCircBuffLength = 8;
1187 cfg.m_maxWriteChunkLength = 4;
1188 cfg.m_maxChunkTime = 0.25;
1189 cfg.m_semWaitNSec = 1000000;
1190 cfg.m_savePath = ( std::filesystem::path( "/tmp/streamWriter_lifecycle_test" ) / "fg_2d" / "raw" ).string();
1191
1192 tempStream source( cfg.m_shmimName, 3, 2, 1 );
1193
1194 loadConfig( app, "fg_2d_stream", cfg );
1195 REQUIRE( app.initializeFgHarness() == 0 );
1196 fgScope.markActive( true );
1197
1198 app.startFgHarnessThread();
1199
1200 REQUIRE( waitFor( [&app]() { return app.m_rawImageCircBuff != nullptr; } ) );
1201
1202 const timespec atime{ 900, 1234 };
1203 const timespec writetime{ 901, 5678 };
1204 source.publishFrame( 0, 21, 1000, atime, writetime );
1205
1206 REQUIRE( waitFor( [&app]() { return app.m_currImage == 1; } ) );
1207 REQUIRE( app.m_width == 3 );
1208 REQUIRE( app.m_height == 2 );
1209 REQUIRE( rawFrameWord( app, 0, 0 ) == 1000 );
1210 REQUIRE( rawFrameWord( app, 0, 5 ) == 1005 );
1211 REQUIRE( timingWord( app, 0, 0 ) == 21 );
1212 REQUIRE( timingWord( app, 0, 1 ) == static_cast<uint64_t>( atime.tv_sec ) );
1213 REQUIRE( timingWord( app, 0, 2 ) == static_cast<uint64_t>( atime.tv_nsec ) );
1214 REQUIRE( timingWord( app, 0, 3 ) == static_cast<uint64_t>( writetime.tv_sec ) );
1215 REQUIRE( timingWord( app, 0, 4 ) == static_cast<uint64_t>( writetime.tv_nsec ) );
1216 REQUIRE( app.m_currImageTime == static_cast<uint64_t>( writetime.tv_sec ) * 1000000000ULL +
1217 static_cast<uint64_t>( writetime.tv_nsec ) );
1218
1219 app.stopFgHarness();
1220 fgScope.disarm();
1221 }
1222}
1223
1224} // namespace streamWriterTest
1225
1226} // namespace libXWCTest
int recordTelem(const telem_saving_state *)
virtual int appStartup()
Startup functions.
int allocate_circbufs()
Worker function to allocate the circular buffers.
int recordSavingStats(bool force=false)
virtual int appLogic()
Implementation of the FSM for the Siglent SDG.
int allocate_xrif()
Worker function to configure and allocate the xrif handles.
virtual void setupConfig()
Setup the configuration system (called by MagAOXApp::setup())
int recordSavingState(bool force=false)
virtual void loadConfig()
load the configuration system results (called by MagAOXApp::setup())
void fgThreadExec()
Execute the frame grabber main loop.
virtual int appShutdown()
Do any needed shutdown tasks. Currently nothing in this app.
#define MAGAOX_rawimageRelPath
The relative path to the raw images directory.
Definition paths.hpp:92
#define MAGAOX_env_rawimage
Environment variable setting the relative raw image path.
@ OPERATING
The device is operating, other than homing.
@ READY
The device is ready for operation, but is not operating.
TEST_CASE("streamWriter configuration loads defaults and overrides", "[streamWriter]")
Verify setupConfig() and loadConfig() preserve defaults, overrides, and clamp invalid accelerations.
#define XWCTEST_DOXYGEN_REF(fxn)
This inserts an unused call to a function signature to make doxygen make the link.
Definition testXWC.hpp:18
Namespace for all libXWC tests.
#define NOT_WRITING
#define STOP_WRITING
#define WRITING
#define START_WRITING