mxlib
c++ tools for analyzing astronomical data and other tasks by Jared R. Males. [git repo]
Loading...
Searching...
No Matches
fitsFile_test.cpp
Go to the documentation of this file.
1/** \file fitsFile_test.cpp
2 * \brief Tests FITS file I/O.
3 */
4#include "../../../catch2/catch.hpp"
5
6#include <algorithm>
7#include <filesystem>
8
9using namespace Catch::Matchers;
10
12using namespace mx::fits;
13
16
17namespace mx
18{
19namespace unitTest
20{
21namespace fitsTest
22{
23namespace fitsFileTest
24{
25
26// Test harnass for fitsFile
27template <typename dataT>
28class fitsFile_test : public fitsFile<dataT>
29{
30 public:
31 typedef fitsFile<dataT>::pixarrT pixarrTT;
32
33 // Interface to protected calcPixarrs
34 mx::error_t calcPixarrs( pixarrTT &pixarrs )
35 {
36 return fitsFile<dataT>::calcPixarrs( pixarrs );
37 }
38
39 mx::error_t allocatePixarrs( pixarrTT &pixarrs, int naxis )
40 {
41 return pixarrs.allocate( naxis );
42 }
43
44 void noComment( bool noComment )
45 {
47 }
48
49 bool isOpen() const
50 {
52 }
53
54 // Interface to set protected axis dimensions
55 void setnax( int naxis, long naxes0, long naxes1, long naxes2 )
56 {
58
59 if( naxis > 0 )
60 {
61 fitsFile<dataT>::m_naxes[0] = naxes0;
62 }
63
64 if( naxis > 1 )
65 {
66 fitsFile<dataT>::m_naxes[1] = naxes1;
67 }
68
69 if( naxis > 2 )
70 {
71 fitsFile<dataT>::m_naxes[2] = naxes2;
72 }
73 }
74
75 mx::error_t writeTestFrame( const std::string &fname, int n )
76 {
77 mx::improc::eigenImage<float> im( 1024, 1024 );
78
79 for( int cc = 0; cc < im.cols(); ++cc )
80 {
81 for( int rr = 0; rr < im.rows(); ++rr )
82 {
83 im( rr, cc ) = cc * im.rows() + rr;
84 }
85 }
86
87 fitsHeader fh;
88 fh.append( std::format( "FLOAT{}", n ), static_cast<float>( 1.1 ), "testing 1.1" );
89 fh.append( std::format( "INT{}", n ), static_cast<int>( 2 ), "testing 1.1" );
90
91 return this->write( std::format( "{}{}.fits", fname, n ), im, fh );
92 }
93};
94
95/** \cond */
96class temporaryDirectory
97{
98 public:
99 temporaryDirectory( const std::string &name ) : m_path( std::filesystem::temp_directory_path() / name )
100 {
101 std::filesystem::remove_all( m_path );
102 std::filesystem::create_directories( m_path );
103 }
104
105 ~temporaryDirectory()
106 {
107 std::error_code error;
108 std::filesystem::remove_all( m_path, error );
109 }
110
111 const std::filesystem::path &path() const
112 {
113 return m_path;
114 }
115
116 private:
117 std::filesystem::path m_path;
118};
119
120template <int exceptionKind>
121void throwResizeException()
122{
123 if constexpr( exceptionKind == 0 )
124 {
125 throw std::bad_alloc();
126 }
127 else if constexpr( exceptionKind == 1 )
128 {
129 throw std::runtime_error( "resize failure" );
130 }
131 else
132 {
133 throw 1;
134 }
135}
136
137template <int exceptionKind>
138struct throwingCube
139{
140 using Scalar = float;
141
142 void resize( int, int, int )
143 {
144 throwResizeException<exceptionKind>();
145 }
146};
147
148template <int exceptionKind>
149struct throwingImage
150{
151 using Scalar = float;
152
153 void resize( int, int )
154 {
155 throwResizeException<exceptionKind>();
156 }
157};
158
159class fitsFileOpsGuard
160{
161 public:
162 fitsFileOpsGuard() : m_saved( fitsFileDetail::fitsFileCfitsioOpsInstance() )
163 {
164 }
165
166 ~fitsFileOpsGuard()
167 {
168 fitsFileDetail::fitsFileCfitsioOpsInstance() = m_saved;
169 }
170
171 private:
172 fitsFileDetail::fitsFileCfitsioOps m_saved;
173};
174
175int failOpen( fitsfile **, const char *, int, int *status )
176{
177 *status = FILE_NOT_OPENED;
178 return *status;
179}
180
181int failGetImageDim( fitsfile *, int *, int *status )
182{
183 *status = BAD_NAXIS;
184 return *status;
185}
186
187int failGetImageSize( fitsfile *, int, long *, int *status )
188{
189 *status = BAD_DIMEN;
190 return *status;
191}
192
193int failClose( fitsfile *, int *status )
194{
195 *status = FILE_NOT_CLOSED;
196 return *status;
197}
198
199int failGetHeaderSpace( fitsfile *, int *, int *, int *status )
200{
201 *status = READ_ERROR;
202 return *status;
203}
204
205int getHeaderSpaceCall = 0;
206int failGetHeaderSpaceOnCall = 1;
207
208int controlledGetHeaderSpace( fitsfile *file, int *keysExist, int *moreKeys, int *status )
209{
210 ++getHeaderSpaceCall;
211 if( getHeaderSpaceCall == failGetHeaderSpaceOnCall )
212 {
213 *status = READ_ERROR;
214 return *status;
215 }
216
217 return ffghsp( file, keysExist, moreKeys, status );
218}
219
220int failReadKey( fitsfile *, int, char *, char *, char *, int *status )
221{
222 *status = READ_ERROR;
223 return *status;
224}
225
226int failCreateFile( fitsfile **, const char *, int *status )
227{
228 *status = FILE_NOT_CREATED;
229 return *status;
230}
231
232int failCreateImage( fitsfile *file, int bitpix, int naxis, long *naxes, int *status )
233{
234 ffcrim( file, bitpix, naxis, naxes, status );
235 if( *status != 0 )
236 {
237 return *status;
238 }
239 *status = BAD_NAXIS;
240 return *status;
241}
242
243int failWritePixels( fitsfile *, int, long *, LONGLONG, void *, int *status )
244{
245 *status = WRITE_ERROR;
246 return *status;
247}
248
249int readSubsetCall = 0;
250int failReadSubsetOnCall = 1;
251int readSubsetStatus = READ_ERROR;
252
253int controlledReadSubset( fitsfile *file,
254 int dataType,
255 long *firstPixel,
256 long *lastPixel,
257 long *increment,
258 void *nullValue,
259 void *data,
260 int *anyNull,
261 int *status )
262{
263 ++readSubsetCall;
264 if( readSubsetCall == failReadSubsetOnCall )
265 {
266 *status = readSubsetStatus;
267 return *status;
268 }
269
270 return ffgsv( file, dataType, firstPixel, lastPixel, increment, nullValue, data, anyNull, status );
271}
272
273enum class allocationFailure
274{
275 none,
276 badAllocation,
277 standardException,
278 unknownException,
279 nullPointer
280};
281
282allocationFailure allocationFailureKind = allocationFailure::none;
283int allocationCall = 0;
284int failAllocationOnCall = 1;
285
286long *controlledAllocateLongs( size_t count )
287{
288 ++allocationCall;
289 if( allocationCall == failAllocationOnCall )
290 {
291 if( allocationFailureKind == allocationFailure::badAllocation )
292 {
293 throw std::bad_alloc();
294 }
295 if( allocationFailureKind == allocationFailure::standardException )
296 {
297 throw std::runtime_error( "coordinate allocation failure" );
298 }
299 if( allocationFailureKind == allocationFailure::unknownException )
300 {
301 throw 1;
302 }
303 if( allocationFailureKind == allocationFailure::nullPointer )
304 {
305 return nullptr;
306 }
307 }
308
309 return new long[count];
310}
311/** \endcond */
312
313/// Calculating subimage sizes
314/** Verify calculation of subimage sizes
315 *
316 * \ingroup fitsFile_unit_tests
317 */
318TEST_CASE( "Calculating subimage sizes", "[ioutils::fits::fitsFile]" )
319{
320// document what protected members are tested here
321#ifdef MXLIB_DOXYGEN_PROTECTED_REF
324 ff.calcPixarrs( pixarrs );
325 ff.naxis();
326 ff.naxes( 0 );
327 ff.setReadSize( 10, 9, 7, 10 ) ff.setCubeReadSize( 5, 3 );
328#endif
329
330 SECTION( "a 1D image to read" )
331 {
332 SECTION( "reading the whole image with default setup" )
333 {
334 fitsFile_test<float> fft;
335
336 fft.setnax( 1, 64, 0, 0 );
337
338 REQUIRE( fft.naxis() == 1 );
339 REQUIRE( fft.naxes( 0 ) == 64 );
340 REQUIRE( fft.naxes( 1 ) == -1 );
341
342 fitsFile_test<float>::pixarrTT pixarrs;
343 fft.calcPixarrs( pixarrs );
344
345 REQUIRE( pixarrs.fpix[0] == 1 );
346 REQUIRE( pixarrs.lpix[0] == 64 );
347 REQUIRE( pixarrs.inc[0] == 1 );
348 }
349
350 SECTION( "reading a subimage" )
351 {
352 fitsFile_test<float> fft;
353
354 fft.setnax( 1, 64, 0, 0 );
355
356 REQUIRE( fft.naxis() == 1 );
357 REQUIRE( fft.naxes( 0 ) == 64 );
358 REQUIRE( fft.naxes( 1 ) == -1 );
359
360 fft.setReadSize( 10, 0, 7, 0 );
361
362 fitsFile_test<float>::pixarrTT pixarrs;
363 fft.calcPixarrs( pixarrs );
364
365 REQUIRE( pixarrs.fpix[0] == 11 );
366 REQUIRE( pixarrs.lpix[0] == 17 );
367 REQUIRE( pixarrs.inc[0] == 1 );
368 }
369 }
370
371 SECTION( "a 2D image to read" )
372 {
373 SECTION( "reading the whole image with default setup" )
374 {
375 fitsFile_test<float> fft;
376
377 fft.setnax( 2, 64, 64, 0 );
378
379 REQUIRE( fft.naxis() == 2 );
380 REQUIRE( fft.naxes( 0 ) == 64 );
381 REQUIRE( fft.naxes( 1 ) == 64 );
382 REQUIRE( fft.naxes( 2 ) == -1 );
383
384 fitsFile_test<float>::pixarrTT pixarrs;
385 fft.calcPixarrs( pixarrs );
386
387 REQUIRE( pixarrs.fpix[0] == 1 );
388 REQUIRE( pixarrs.fpix[1] == 1 );
389 REQUIRE( pixarrs.lpix[0] == 64 );
390 REQUIRE( pixarrs.lpix[1] == 64 );
391 REQUIRE( pixarrs.inc[0] == 1 );
392 REQUIRE( pixarrs.inc[1] == 1 );
393 }
394
395 SECTION( "reading a subimage" )
396 {
397 fitsFile_test<float> fft;
398
399 fft.setnax( 2, 64, 64, 0 );
400
401 REQUIRE( fft.naxis() == 2 );
402 REQUIRE( fft.naxes( 0 ) == 64 );
403 REQUIRE( fft.naxes( 1 ) == 64 );
404 REQUIRE( fft.naxes( 2 ) == -1 );
405
406 fft.setReadSize( 10, 9, 7, 10 );
407
408 fitsFile_test<float>::pixarrTT pixarrs;
409 fft.calcPixarrs( pixarrs );
410
411 REQUIRE( pixarrs.fpix[0] == 11 );
412 REQUIRE( pixarrs.fpix[1] == 10 );
413 REQUIRE( pixarrs.lpix[0] == 17 );
414 REQUIRE( pixarrs.lpix[1] == 19 );
415 REQUIRE( pixarrs.inc[0] == 1 );
416 REQUIRE( pixarrs.inc[1] == 1 );
417 }
418 }
419
420 SECTION( "a 3D image to read" )
421 {
422 WHEN( "reading the whole image with default setup" )
423 {
424 fitsFile_test<float> fft;
425
426 fft.setnax( 3, 64, 64, 64 );
427
428 REQUIRE( fft.naxis() == 3 );
429 REQUIRE( fft.naxes( 0 ) == 64 );
430 REQUIRE( fft.naxes( 1 ) == 64 );
431 REQUIRE( fft.naxes( 2 ) == 64 );
432 REQUIRE( fft.naxes( 3 ) == -1 );
433
434 fitsFile_test<float>::pixarrTT pixarrs;
435 fft.calcPixarrs( pixarrs );
436
437 REQUIRE( pixarrs.fpix[0] == 1 );
438 REQUIRE( pixarrs.fpix[1] == 1 );
439 REQUIRE( pixarrs.fpix[2] == 1 );
440
441 REQUIRE( pixarrs.lpix[0] == 64 );
442 REQUIRE( pixarrs.lpix[1] == 64 );
443 REQUIRE( pixarrs.lpix[2] == 64 );
444 REQUIRE( pixarrs.inc[0] == 1 );
445 REQUIRE( pixarrs.inc[1] == 1 );
446 REQUIRE( pixarrs.inc[2] == 1 );
447 }
448
449 SECTION( "reading a subimage" )
450 {
451 fitsFile_test<float> fft;
452
453 fft.setnax( 3, 64, 64, 64 );
454
455 REQUIRE( fft.naxis() == 3 );
456 REQUIRE( fft.naxes( 0 ) == 64 );
457 REQUIRE( fft.naxes( 1 ) == 64 );
458 REQUIRE( fft.naxes( 2 ) == 64 );
459 REQUIRE( fft.naxes( 3 ) == -1 );
460
461 fft.setReadSize( 10, 9, 7, 10 );
462 fft.setCubeReadSize( 5, 3 );
463
464 fitsFile_test<float>::pixarrTT pixarrs;
465 fft.calcPixarrs( pixarrs );
466
467 REQUIRE( pixarrs.fpix[0] == 11 );
468 REQUIRE( pixarrs.fpix[1] == 10 );
469 REQUIRE( pixarrs.fpix[2] == 6 );
470 REQUIRE( pixarrs.lpix[0] == 17 );
471 REQUIRE( pixarrs.lpix[1] == 19 );
472 REQUIRE( pixarrs.lpix[2] == 8 );
473 REQUIRE( pixarrs.inc[0] == 1 );
474 REQUIRE( pixarrs.inc[1] == 1 );
475 REQUIRE( pixarrs.inc[2] == 1 );
476 }
477
478 SECTION( "reading complete images from selected cube planes" )
479 {
480 fitsFile_test<float> fft;
481 fft.setnax( 3, 64, 32, 8 );
482 fft.setCubeReadSize( 2, 3 );
483
484 fitsFile_test<float>::pixarrTT pixarrs;
485 fft.calcPixarrs( pixarrs );
486 REQUIRE( pixarrs.fpix[0] == 1 );
487 REQUIRE( pixarrs.lpix[0] == 64 );
488 REQUIRE( pixarrs.fpix[1] == 1 );
489 REQUIRE( pixarrs.lpix[1] == 32 );
490 REQUIRE( pixarrs.fpix[2] == 3 );
491 REQUIRE( pixarrs.lpix[2] == 5 );
492 }
493
494 SECTION( "reading subimages from every cube plane" )
495 {
496 fitsFile_test<float> fft;
497 fft.setnax( 3, 64, 32, 8 );
498 fft.setReadSize( 10, 5, 7, 4 );
499
500 fitsFile_test<float>::pixarrTT pixarrs;
501 fft.calcPixarrs( pixarrs );
502 REQUIRE( pixarrs.fpix[0] == 11 );
503 REQUIRE( pixarrs.lpix[0] == 17 );
504 REQUIRE( pixarrs.fpix[1] == 6 );
505 REQUIRE( pixarrs.lpix[1] == 9 );
506 REQUIRE( pixarrs.fpix[2] == 1 );
507 REQUIRE( pixarrs.lpix[2] == 8 );
508 }
509 }
510}
511
512/** \brief Verifies fitsFile construction, state transitions, dimensions, and read-window resets.
513 *
514 * Exercises mx::fits::fitsFile constructors, fileName, open, close, getDimensions, getSize, setReadSize,
515 * setCubeReadSize, and repeated protected pixel-array allocation through the test fixture.
516 *
517 * \ingroup fitsFile_unit_tests
518 */
519TEST_CASE( "Managing FITS file state and dimensions", "[ioutils::fits::fitsFile]" )
520{
521 temporaryDirectory testDirectory( "mxlib-fitsFile-state-test" );
522 const std::filesystem::path imagePath = testDirectory.path() / "image.fits";
523
524 mx::improc::eigenImage<float> image( 2, 3 );
525 image << 1.0F, 2.0F, 3.0F, 4.0F, 5.0F, 6.0F;
526 fitsFile<float> writer;
527 REQUIRE( writer.write( imagePath.string(), image ) == mx::error_t::noerror );
528
530 fitsFile<float> defaultWithError( error );
531 REQUIRE( error == mx::error_t::noerror );
532 REQUIRE( defaultWithError.fileName().empty() );
533 REQUIRE( defaultWithError.open() == mx::error_t::invalidconfig );
534 REQUIRE( defaultWithError.close() == mx::error_t::noerror );
535 REQUIRE( defaultWithError.getDimensions( error ) == -1 );
536 REQUIRE( error == mx::error_t::invalidconfig );
537 REQUIRE( defaultWithError.getSize( error ) == -1 );
538 REQUIRE( error == mx::error_t::invalidconfig );
539 REQUIRE( defaultWithError.getSize( 0, error ) == -1 );
540 REQUIRE( error == mx::error_t::invalidconfig );
541
542 fitsFile<float> named( imagePath.string(), error );
543 REQUIRE( error == mx::error_t::noerror );
544 REQUIRE( named.fileName() == imagePath.string() );
545
546 fitsFile<float> namedClosed( imagePath.string(), false );
547 REQUIRE( namedClosed.fileName() == imagePath.string() );
548 REQUIRE( namedClosed.open() == mx::error_t::noerror );
549 REQUIRE( namedClosed.open() == mx::error_t::noerror );
550 REQUIRE( namedClosed.getDimensions( error ) == 2 );
551 REQUIRE( namedClosed.getSize( error ) == 6 );
552 REQUIRE( namedClosed.getSize( 0, error ) == 2 );
553 REQUIRE( namedClosed.getSize( 1, error ) == 3 );
554 REQUIRE( namedClosed.getSize( 2, error ) == -1 );
555 REQUIRE( error == mx::error_t::invalidarg );
556
557 namedClosed.setReadSize( 0, 0, 1, 2 );
558 REQUIRE( namedClosed.getSize( error ) == 2 );
559 REQUIRE( namedClosed.getSize( 0, error ) == 1 );
560 REQUIRE( namedClosed.getSize( 1, error ) == 2 );
561 namedClosed.setReadSize();
562 REQUIRE( namedClosed.getSize( error ) == 6 );
563 namedClosed.setCubeReadSize( 0, 1 );
564 namedClosed.setCubeReadSize();
565
566 REQUIRE( namedClosed.fileName( imagePath.string(), false ) == mx::error_t::noerror );
567 REQUIRE( namedClosed.close() == mx::error_t::noerror );
568
569 fitsFile<float> opened( imagePath.string(), true, error );
570 REQUIRE( error == mx::error_t::noerror );
571 REQUIRE( opened.getDimensions( error ) == 2 );
572
573 fitsFile<float> missing( ( testDirectory.path() / "missing.fits" ).string(), true );
574 REQUIRE( missing.naxis() == 0 );
575
576 fitsFile_test<float> fixture;
577 fitsFile_test<float>::pixarrTT pixelArrays;
578 REQUIRE( fixture.allocatePixarrs( pixelArrays, 0 ) == mx::error_t::paramnotset );
579 REQUIRE( fixture.allocatePixarrs( pixelArrays, 2 ) == mx::error_t::noerror );
580 REQUIRE( fixture.allocatePixarrs( pixelArrays, 3 ) == mx::error_t::noerror );
581}
582
583/** \brief Verifies Eigen image and cube resize adapters preserve each exception category.
584 *
585 * Exercises mx::fits::eigenArrResize for bad allocation, standard, and non-standard exceptions from both supported
586 * resize signatures.
587 *
588 * \ingroup fitsFile_unit_tests
589 */
590TEST_CASE( "Propagating FITS destination resize exceptions", "[ioutils::fits::fitsFile]" )
591{
592 throwingCube<0> badAllocationCube;
593 throwingCube<1> standardExceptionCube;
594 throwingCube<2> unknownExceptionCube;
595 eigenArrResize<throwingCube<0>, verbose::vv, true> badAllocationCubeResize;
596 eigenArrResize<throwingCube<1>, verbose::vv, true> standardExceptionCubeResize;
597 eigenArrResize<throwingCube<2>, verbose::vv, true> unknownExceptionCubeResize;
598
599 REQUIRE_THROWS_AS( badAllocationCubeResize.resize( badAllocationCube, 1, 1, 1 ), std::bad_alloc );
600 REQUIRE_THROWS_AS( standardExceptionCubeResize.resize( standardExceptionCube, 1, 1, 1 ), std::runtime_error );
601 REQUIRE_THROWS_AS( unknownExceptionCubeResize.resize( unknownExceptionCube, 1, 1, 1 ), int );
602
603 throwingImage<0> badAllocationImage;
604 throwingImage<1> standardExceptionImage;
605 throwingImage<2> unknownExceptionImage;
606 eigenArrResize<throwingImage<0>, verbose::vv> badAllocationImageResize;
607 eigenArrResize<throwingImage<1>, verbose::vv> standardExceptionImageResize;
608 eigenArrResize<throwingImage<2>, verbose::vv> unknownExceptionImageResize;
609
610 REQUIRE_THROWS_AS( badAllocationImageResize.resize( badAllocationImage, 1, 1, 1 ), std::bad_alloc );
611 REQUIRE_THROWS_AS( standardExceptionImageResize.resize( standardExceptionImage, 1, 1, 1 ), std::runtime_error );
612 REQUIRE_THROWS_AS( unknownExceptionImageResize.resize( unknownExceptionImage, 1, 1, 1 ), int );
613}
614
615/** \brief Verifies fitsFile reports CFITSIO open, dimension, size, and close failures and remains recoverable.
616 *
617 * Exercises mx::fits::fitsFile::open and mx::fits::fitsFile::close with deterministic CFITSIO failures.
618 *
619 * \ingroup fitsFile_unit_tests
620 */
621TEST_CASE( "Propagating FITS file lifecycle failures", "[ioutils::fits::fitsFile]" )
622{
623 temporaryDirectory testDirectory( "mxlib-fitsFile-lifecycle-failure-test" );
624 const std::filesystem::path imagePath = testDirectory.path() / "image.fits";
625 mx::improc::eigenImage<float> image( 2, 2 );
626 image.setOnes();
627 fitsFile<float> writer;
628 REQUIRE( writer.write( imagePath.string(), image ) == mx::error_t::noerror );
629
630 SECTION( "open failure" )
631 {
632 fitsFileOpsGuard operationsGuard;
633 fitsFileDetail::fitsFileCfitsioOpsInstance().openFile = failOpen;
634 fitsFile_test<float> reader;
635 REQUIRE( reader.open( imagePath.string() ) == mx::error_t::fits_file_not_opened );
636 REQUIRE_FALSE( reader.isOpen() );
637 }
638
639 SECTION( "image-axis count failure" )
640 {
641 fitsFileOpsGuard operationsGuard;
642 fitsFileDetail::fitsFileCfitsioOpsInstance().getImageDim = failGetImageDim;
643 fitsFile_test<float> reader;
644 REQUIRE( reader.open( imagePath.string() ) == mx::error_t::fits_bad_naxis );
645 REQUIRE_FALSE( reader.isOpen() );
646 }
647
648 SECTION( "image-axis size failure" )
649 {
650 fitsFileOpsGuard operationsGuard;
651 fitsFileDetail::fitsFileCfitsioOpsInstance().getImageSize = failGetImageSize;
652 fitsFile_test<float> reader;
653 REQUIRE( reader.open( imagePath.string() ) == mx::error_t::fits_bad_dimen );
654 REQUIRE_FALSE( reader.isOpen() );
655 }
656
657 SECTION( "close failure" )
658 {
659 fitsFile_test<float> reader;
660 REQUIRE( reader.open( imagePath.string() ) == mx::error_t::noerror );
661 {
662 fitsFileOpsGuard operationsGuard;
663 fitsFileDetail::fitsFileCfitsioOpsInstance().closeFile = failClose;
664 REQUIRE( reader.close() == mx::error_t::fits_file_not_closed );
665 REQUIRE( reader.isOpen() );
666 }
667 REQUIRE( reader.close() == mx::error_t::noerror );
668 REQUIRE_FALSE( reader.isOpen() );
669 }
670
671 fitsFileDetail::resetFitsFileCfitsioOps();
672}
673
674/** \brief Verifies fitsFile preserves allocation failures and rejects null pixel-coordinate arrays.
675 *
676 * Exercises the protected pixel-coordinate allocation used by mx::fits::fitsFile::read.
677 *
678 * \ingroup fitsFile_unit_tests
679 */
680TEST_CASE( "Propagating FITS pixel-coordinate allocation failures", "[ioutils::fits::fitsFile]" )
681{
682 fitsFileOpsGuard operationsGuard;
683 fitsFileDetail::fitsFileCfitsioOpsInstance().allocateLongs = controlledAllocateLongs;
684 allocationCall = 0;
685 failAllocationOnCall = 1;
686
687 fitsFile_test<float> fixture;
688 fitsFile_test<float>::pixarrTT pixelArrays;
689
690 SECTION( "bad allocation" )
691 {
692 allocationFailureKind = allocationFailure::badAllocation;
693 REQUIRE_THROWS_AS( fixture.allocatePixarrs( pixelArrays, 2 ), std::bad_alloc );
694 }
695
696 SECTION( "standard exception" )
697 {
698 allocationFailureKind = allocationFailure::standardException;
699 REQUIRE_THROWS_AS( fixture.allocatePixarrs( pixelArrays, 2 ), std::runtime_error );
700 }
701
702 SECTION( "unknown exception" )
703 {
704 allocationFailureKind = allocationFailure::unknownException;
705 REQUIRE_THROWS_AS( fixture.allocatePixarrs( pixelArrays, 2 ), int );
706 }
707
708 SECTION( "null first-pixel array" )
709 {
710 allocationFailureKind = allocationFailure::nullPointer;
711 failAllocationOnCall = 1;
712 REQUIRE( fixture.allocatePixarrs( pixelArrays, 2 ) == mx::error_t::allocerr );
713 }
714
715 SECTION( "null last-pixel array" )
716 {
717 allocationFailureKind = allocationFailure::nullPointer;
718 failAllocationOnCall = 2;
719 REQUIRE( fixture.allocatePixarrs( pixelArrays, 2 ) == mx::error_t::allocerr );
720 }
721
722 SECTION( "null increment array" )
723 {
724 allocationFailureKind = allocationFailure::nullPointer;
725 failAllocationOnCall = 3;
726 REQUIRE( fixture.allocatePixarrs( pixelArrays, 2 ) == mx::error_t::allocerr );
727 }
728
729 SECTION( "allocation error propagates through read-coordinate calculation" )
730 {
731 allocationFailureKind = allocationFailure::nullPointer;
732 failAllocationOnCall = 1;
733 fixture.setnax( 2, 2, 2, 0 );
734 REQUIRE( fixture.calcPixarrs( pixelArrays ) == mx::error_t::allocerr );
735 }
736}
737
738/** \brief Verifies raw, Eigen, cube, and header reads propagate deterministic CFITSIO failures.
739 *
740 * Exercises mx::fits::fitsFile::read and mx::fits::fitsFile::readHeader error paths.
741 *
742 * \ingroup fitsFile_unit_tests
743 */
744TEST_CASE( "Propagating FITS read failures", "[ioutils::fits::fitsFile]" )
745{
746 temporaryDirectory testDirectory( "mxlib-fitsFile-read-failure-test" );
747 const std::filesystem::path imagePath = testDirectory.path() / "image.fits";
748 mx::improc::eigenImage<float> image( 2, 2 );
749 image << 1.0F, 2.0F, 3.0F, 4.0F;
750 fitsHeader<> writeHeader;
751 REQUIRE( writeHeader.append( "TESTKEY", 42, "test value" ) == mx::error_t::noerror );
752 fitsFile<float> writer;
753 REQUIRE( writer.write( imagePath.string(), image, writeHeader ) == mx::error_t::noerror );
754
755 fitsFileOpsGuard operationsGuard;
756
757 SECTION( "raw and Eigen subset failures" )
758 {
759 fitsFileDetail::fitsFileCfitsioOpsInstance().readSubset = controlledReadSubset;
760 readSubsetCall = 0;
761 failReadSubsetOnCall = 1;
762 readSubsetStatus = READ_ERROR;
763
764 std::vector<float> raw( 4 );
765 fitsFile<float> rawReader( imagePath.string(), false );
766 REQUIRE( rawReader.read( raw.data() ) == mx::error_t::fits_read_error );
767
768 readSubsetCall = 0;
770 fitsFile<float> eigenReader;
771 REQUIRE( eigenReader.read( readImage, imagePath.string() ) == mx::error_t::fits_read_error );
772
773 readSubsetCall = 0;
774 fitsHeader<> readHeader;
775 REQUIRE( eigenReader.read( readImage, readHeader, imagePath.string() ) == mx::error_t::fits_read_error );
776 }
777
778 SECTION( "end-of-file status is accepted" )
779 {
780 fitsFileDetail::fitsFileCfitsioOpsInstance().readSubset = controlledReadSubset;
781 readSubsetCall = 0;
782 failReadSubsetOnCall = 1;
783 readSubsetStatus = END_OF_FILE;
784 std::vector<float> raw( 4 );
785 fitsFile<float> reader;
786 REQUIRE( reader.read( raw.data(), imagePath.string() ) == mx::error_t::noerror );
787 }
788
789 SECTION( "first and later cube plane failures" )
790 {
791 fitsFileDetail::fitsFileCfitsioOpsInstance().readSubset = controlledReadSubset;
792 const std::vector<std::string> files{ imagePath.string(), imagePath.string() };
794 fitsFile<float> reader;
795
796 readSubsetCall = 0;
797 failReadSubsetOnCall = 1;
798 readSubsetStatus = READ_ERROR;
799 REQUIRE( reader.read( cube, files ) == mx::error_t::fits_read_error );
800
801 readSubsetCall = 0;
802 failReadSubsetOnCall = 2;
803 REQUIRE( reader.read( cube, files ) == mx::error_t::fits_read_error );
804 }
805
806 SECTION( "cube coordinate allocation failure" )
807 {
808 fitsFileDetail::fitsFileCfitsioOpsInstance().allocateLongs = controlledAllocateLongs;
809 allocationCall = 0;
810 failAllocationOnCall = 1;
811 allocationFailureKind = allocationFailure::nullPointer;
813 fitsFile<float> reader;
814 REQUIRE( reader.read( cube, std::vector<std::string>{ imagePath.string() } ) == mx::error_t::allocerr );
815 }
816
817 SECTION( "first and later cube header failures" )
818 {
819 fitsFileDetail::fitsFileCfitsioOpsInstance().getHeaderSpace = controlledGetHeaderSpace;
820 const std::vector<std::string> files{ imagePath.string(), imagePath.string() };
821 std::vector<fitsHeader<>> headers( files.size() );
823 fitsFile<float> reader;
824
825 getHeaderSpaceCall = 0;
826 failGetHeaderSpaceOnCall = 1;
827 REQUIRE( reader.read( cube, headers, files ) == mx::error_t::fits_read_error );
828
829 getHeaderSpaceCall = 0;
830 failGetHeaderSpaceOnCall = 2;
831 REQUIRE( reader.read( cube, headers, files ) == mx::error_t::fits_read_error );
832 }
833
834 SECTION( "header-space and header-card failures" )
835 {
836 fitsHeader<> readHeader;
837 fitsFile<float> reader;
838 fitsFileDetail::fitsFileCfitsioOpsInstance().getHeaderSpace = failGetHeaderSpace;
839 REQUIRE( reader.readHeader( readHeader, imagePath.string() ) == mx::error_t::fits_read_error );
840
841 fitsFileDetail::fitsFileCfitsioOpsInstance().getHeaderSpace = ffghsp;
842 fitsFileDetail::fitsFileCfitsioOpsInstance().readKey = failReadKey;
843 REQUIRE( reader.readHeader( readHeader, imagePath.string() ) == mx::error_t::fits_read_error );
844 }
845
846 SECTION( "header failure after a successful Eigen read" )
847 {
848 fitsFileDetail::fitsFileCfitsioOpsInstance().getHeaderSpace = failGetHeaderSpace;
850 fitsHeader<> readHeader;
851 fitsFile<float> reader;
852 REQUIRE( reader.read( readImage, readHeader, imagePath.string() ) == mx::error_t::fits_read_error );
853 }
854
855 SECTION( "header open failure" )
856 {
857 fitsHeader<> readHeader;
858 fitsFile<float> reader;
859 REQUIRE( reader.fileName( imagePath.string(), false ) == mx::error_t::noerror );
860 fitsFileDetail::fitsFileCfitsioOpsInstance().openFile = failOpen;
861 REQUIRE( reader.readHeader( readHeader ) == mx::error_t::fits_file_not_opened );
862 }
863}
864
865/** \brief Verifies FITS writes propagate creation, image, pixel, header, and close failures.
866 *
867 * Exercises mx::fits::fitsFile::write error paths.
868 *
869 * \ingroup fitsFile_unit_tests
870 */
871TEST_CASE( "Propagating FITS write failures", "[ioutils::fits::fitsFile]" )
872{
873 temporaryDirectory testDirectory( "mxlib-fitsFile-write-failure-test" );
874 mx::improc::eigenImage<float> image( 2, 2 );
875 image.setOnes();
876
877 SECTION( "file creation failure" )
878 {
879 fitsFileOpsGuard operationsGuard;
880 fitsFileDetail::fitsFileCfitsioOpsInstance().createFile = failCreateFile;
881 fitsFile_test<float> writer;
882 REQUIRE( writer.write( ( testDirectory.path() / "create.fits" ).string(), image ) ==
884 REQUIRE_FALSE( writer.isOpen() );
885 }
886
887 SECTION( "image creation failure" )
888 {
889 fitsFile_test<float> writer;
890 {
891 fitsFileOpsGuard operationsGuard;
892 fitsFileDetail::fitsFileCfitsioOpsInstance().createImage = failCreateImage;
893 REQUIRE( writer.write( ( testDirectory.path() / "image.fits" ).string(), image ) ==
895 REQUIRE( writer.isOpen() );
896 }
897 REQUIRE( writer.close() == mx::error_t::noerror );
898 }
899
900 SECTION( "pixel write failure" )
901 {
902 fitsFile_test<float> writer;
903 {
904 fitsFileOpsGuard operationsGuard;
905 fitsFileDetail::fitsFileCfitsioOpsInstance().writePixels = failWritePixels;
906 REQUIRE( writer.write( ( testDirectory.path() / "pixels.fits" ).string(), image ) ==
908 REQUIRE( writer.isOpen() );
909 }
910 REQUIRE( writer.close() == mx::error_t::noerror );
911 }
912
913 SECTION( "close failure" )
914 {
915 fitsFile_test<float> writer;
916 {
917 fitsFileOpsGuard operationsGuard;
918 fitsFileDetail::fitsFileCfitsioOpsInstance().closeFile = failClose;
919 REQUIRE( writer.write( ( testDirectory.path() / "close.fits" ).string(), image ) ==
921 REQUIRE( writer.isOpen() );
922 }
923 REQUIRE( writer.close() == mx::error_t::noerror );
924 }
925
926 SECTION( "header-card failure" )
927 {
928 fitsHeader<> header;
929 REQUIRE( header.append( "UNTYPED" ) == mx::error_t::noerror );
930 fitsFile_test<float> writer;
931 REQUIRE( writer.write( ( testDirectory.path() / "header.fits" ).string(), image, header ) !=
933 if( writer.isOpen() )
934 {
935 REQUIRE( writer.close() == mx::error_t::noerror );
936 }
937 }
938}
939
940/// Basic writing and reading
941/**
942 * \ingroup fitsFile_unit_tests
943 */
944TEST_CASE( "Basic writing and reading", "[ioutils::fits::fitsFile]" )
945{
946 // clang-format off
947 // document what fitsFile_test is doing
948 #ifdef MXLIB_DOXYGEN_PROTECTED_REF
950 fitsHeader fh;
951 fh.append( "dummy", 2, "dummy" );
953 ff.write( "dummy", im, fh );
954 #endif
955 // clang-format on
956
957 SECTION( "write a frame and read it back in" )
958 {
959 fitsFile_test<float> fft;
960
961 REQUIRE( fft.writeTestFrame( "/tmp/fitsFile_test", 1 ) == mx::error_t::noerror );
962
964 fitsHeader fh;
966
967 REQUIRE( ff.read( im, fh, "/tmp/fitsFile_test1.fits" ) == mx::error_t::noerror );
968
969 REQUIRE_THAT( fh["FLOAT1"].Float(), WithinAbs( 1.1, 1e-5 ) );
970 REQUIRE( fh["INT1"].Int() == 2 );
971 REQUIRE( im.rows() == 1024 );
972 REQUIRE( im.cols() == 1024 );
973 }
974}
975
976/// Cube writing and reading
977/**
978 * \ingroup fitsFile_unit_tests
979 */
980TEST_CASE( "Cube writing and reading", "[ioutils::fits::fitsFile]" )
981{
982 // clang-format off
983 // document what fitsFile_test is doing
984 #ifdef MXLIB_DOXYGEN_PROTECTED_REF
986 fitsHeader fh;
987 fh.append( "dummy", 2, "dummy" );
989 ff.write( "dummy", im, fh );
990 #endif
991 // clang-format on
992
993 SECTION( "write 5 frames and read them back in as a cube" )
994 {
995 fitsFile_test<float> fft;
996
997 REQUIRE( fft.writeTestFrame( "/tmp/fitsFile_test", 1 ) == mx::error_t::noerror );
998 REQUIRE( fft.writeTestFrame( "/tmp/fitsFile_test", 2 ) == mx::error_t::noerror );
999 REQUIRE( fft.writeTestFrame( "/tmp/fitsFile_test", 3 ) == mx::error_t::noerror );
1000 REQUIRE( fft.writeTestFrame( "/tmp/fitsFile_test", 4 ) == mx::error_t::noerror );
1001 REQUIRE( fft.writeTestFrame( "/tmp/fitsFile_test", 5 ) == mx::error_t::noerror );
1002
1003 std::vector<std::string> flist( { "/tmp/fitsFile_test1.fits",
1004 "/tmp/fitsFile_test2.fits",
1005 "/tmp/fitsFile_test3.fits",
1006 "/tmp/fitsFile_test4.fits",
1007 "/tmp/fitsFile_test5.fits" } );
1008
1009 fitsFile<float> ff;
1011
1012 std::vector<mx::fits::fitsHeader<mx::verbose::d>> headers( flist.size() );
1013 for( size_t index = 0; index < headers.size(); ++index )
1014 {
1015 headers[index].append( std::format( "FLOAT{}", index + 1 ) );
1016 }
1017
1018 REQUIRE( ff.read( imc, headers, flist ) == mx::error_t::noerror );
1019
1020 REQUIRE( imc.rows() == 1024 );
1021 REQUIRE( imc.cols() == 1024 );
1022 REQUIRE( imc.planes() == 5 );
1023 for( size_t index = 0; index < headers.size(); ++index )
1024 {
1025 REQUIRE_THAT( headers[index][std::format( "FLOAT{}", index + 1 )].Float(), WithinAbs( 1.1, 1e-5 ) );
1026 }
1027
1028 std::vector<float> rawImages( 1024 * 1024 * flist.size() );
1029 std::vector<mx::fits::fitsHeader<mx::verbose::d>> rawHeaders( flist.size() );
1030 for( size_t index = 0; index < rawHeaders.size(); ++index )
1031 {
1032 rawHeaders[index].append( std::format( "INT{}", index + 1 ) );
1033 }
1034
1035 REQUIRE( ff.read( rawImages.data(), rawHeaders, flist ) == mx::error_t::noerror );
1036 REQUIRE( rawImages.front() == 0.0F );
1037 REQUIRE( rawImages[1024 * 1024 - 1] == 1048575.0F );
1038 REQUIRE( rawImages[1024 * 1024] == 0.0F );
1039 for( size_t index = 0; index < rawHeaders.size(); ++index )
1040 {
1041 REQUIRE( rawHeaders[index][std::format( "INT{}", index + 1 )].Int() == 2 );
1042 }
1043 }
1044}
1045
1046/// Validate HCI bulk image/header read arguments and later-file failures.
1047/**
1048 * \ingroup fitsFile_unit_tests
1049 */
1050TEST_CASE( "Validating HCI batch image and header reads", "[ioutils::fits::fitsFile][hciReduce]" )
1051{
1052 fitsFile_test<float> fixture;
1053 REQUIRE( fixture.writeTestFrame( "/tmp/fitsFile-batch-validation", 1 ) == mx::error_t::noerror );
1054
1055 const std::vector<std::string> oneFile{ "/tmp/fitsFile-batch-validation1.fits" };
1057
1058 SECTION( "empty file lists are rejected" )
1059 {
1061 std::vector<mx::fits::fitsHeader<mx::verbose::d>> headers;
1062 const std::vector<std::string> noFiles;
1063
1064 REQUIRE( fitsFile.read( cube, headers, noFiles ) == mx::error_t::invalidarg );
1065 }
1066
1067 SECTION( "header vectors must contain one header per input image" )
1068 {
1069 std::vector<float> data( 1024 * 1024 );
1070 std::vector<mx::fits::fitsHeader<mx::verbose::d>> headers;
1071 REQUIRE( fitsFile.read( data.data(), headers, oneFile ) == mx::error_t::invalidarg );
1072
1074 REQUIRE( fitsFile.read( cube, headers, oneFile ) == mx::error_t::invalidarg );
1075 }
1076
1077 SECTION( "a missing later input file is returned as an error" )
1078 {
1080 std::vector<mx::fits::fitsHeader<mx::verbose::d>> headers( 2 );
1081 const std::vector<std::string> files{ oneFile.front(), "/tmp/fitsFile-batch-validation-missing.fits" };
1082
1083 REQUIRE( fitsFile.read( cube, headers, files ) != mx::error_t::noerror );
1084 }
1085}
1086
1087/** \brief Verifies raw-array overloads, one-dimensional images, subimages, and headerless batch reads.
1088 *
1089 * Exercises mx::fits::fitsFile::read through its raw pointer overloads, Eigen one-dimensional resize path,
1090 * configured two-dimensional subset, and raw multi-file overload without headers.
1091 *
1092 * \ingroup fitsFile_unit_tests
1093 */
1094TEST_CASE( "Reading raw FITS arrays and configured subsets", "[ioutils::fits::fitsFile]" )
1095{
1096 temporaryDirectory testDirectory( "mxlib-fitsFile-raw-read-test" );
1098
1099 const std::filesystem::path vectorPath = testDirectory.path() / "vector.fits";
1100 std::vector<float> vectorData{ 1.0F, 2.0F, 3.0F, 4.0F };
1101 REQUIRE( fitsFile.write( vectorPath.string(), vectorData.data(), 4, 0, 0 ) == mx::error_t::noerror );
1102
1103 mx::error_t vectorSizeError = mx::error_t::error;
1104 REQUIRE( fitsFile.open( vectorPath.string() ) == mx::error_t::noerror );
1105 REQUIRE( fitsFile.getSize( vectorSizeError ) == 4 );
1106 REQUIRE( vectorSizeError == mx::error_t::noerror );
1107 fitsFile.setReadSize( 1, 0, 2, 0 );
1108 REQUIRE( fitsFile.getSize( vectorSizeError ) == 2 );
1109 REQUIRE( fitsFile.getSize( 0, vectorSizeError ) == 2 );
1111 REQUIRE( fitsFile.close() == mx::error_t::noerror );
1112
1114 REQUIRE( fitsFile.read( vectorImage, vectorPath.string() ) == mx::error_t::noerror );
1115 REQUIRE( vectorImage.rows() == 4 );
1116 REQUIRE( vectorImage.cols() == 1 );
1117 REQUIRE( vectorImage( 3, 0 ) == 4.0F );
1118
1119 mx::improc::eigenImage<float> image( 2, 3 );
1120 image << 1.0F, 2.0F, 3.0F, 4.0F, 5.0F, 6.0F;
1121 fitsHeader<> header;
1122 REQUIRE( header.append( "TESTKEY", 23, "raw read header" ) == mx::error_t::noerror );
1123
1124 const std::filesystem::path firstPath = testDirectory.path() / "first.fits";
1125 const std::filesystem::path secondPath = testDirectory.path() / "second.fits";
1126 REQUIRE( fitsFile.write( firstPath.string(), image.data(), image.rows(), image.cols(), 1, header ) ==
1128 REQUIRE( fitsFile.write( secondPath.string(), image.data(), image.rows(), image.cols(), 1 ) ==
1130
1131 const std::filesystem::path directPath = testDirectory.path() / "direct.fits";
1132 REQUIRE( fitsFile.fileName( directPath.string(), false ) == mx::error_t::noerror );
1133 REQUIRE( fitsFile.write( image.data(), image.rows(), image.cols(), 1 ) == mx::error_t::noerror );
1134 REQUIRE( fitsFile.open() == mx::error_t::noerror );
1135 REQUIRE( fitsFile.write( image.data(), image.rows(), image.cols(), 1, header ) == mx::error_t::noerror );
1136
1137 std::vector<float> raw( 6 );
1138 REQUIRE( fitsFile.fileName( firstPath.string(), false ) == mx::error_t::noerror );
1139 REQUIRE( fitsFile.read( raw.data() ) == mx::error_t::noerror );
1140 REQUIRE( std::equal( image.data(), image.data() + image.size(), raw.data() ) );
1141
1142 fitsHeader<> readHeader;
1143 readHeader.append( "TESTKEY" );
1144 REQUIRE( fitsFile.read( raw.data(), readHeader ) == mx::error_t::noerror );
1145 REQUIRE( readHeader["TESTKEY"].value<int>() == 23 );
1146 REQUIRE( fitsFile.read( raw.data(), secondPath.string() ) == mx::error_t::noerror );
1147 REQUIRE( fitsFile.read( raw.data(), readHeader, firstPath.string() ) == mx::error_t::noerror );
1148 REQUIRE( readHeader["TESTKEY"].value<int>() == 23 );
1149
1150 fitsFile.setReadSize( 1, 0, 1, 2 );
1152 REQUIRE( fitsFile.read( subset, firstPath.string() ) == mx::error_t::noerror );
1153 REQUIRE( subset.rows() == 1 );
1154 REQUIRE( subset.cols() == 2 );
1155 REQUIRE( subset( 0, 0 ) == image( 1, 0 ) );
1156 REQUIRE( subset( 0, 1 ) == image( 1, 1 ) );
1158
1159 std::vector<float> batch( 12 );
1160 const std::vector<std::string> files{ firstPath.string(), secondPath.string() };
1161 REQUIRE( fitsFile.read( batch.data(), files ) == mx::error_t::noerror );
1162 REQUIRE( std::equal( image.data(), image.data() + image.size(), batch.data() ) );
1163 REQUIRE( std::equal( image.data(), image.data() + image.size(), batch.data() + image.size() ) );
1164
1165 const std::vector<std::string> noFiles;
1166 REQUIRE( fitsFile.read( batch.data(), noFiles ) == mx::error_t::invalidarg );
1167 std::vector<fitsHeader<>> noHeaders;
1168 REQUIRE( fitsFile.read( batch.data(), noHeaders, noFiles ) == mx::error_t::invalidarg );
1169
1170 mx::fits::fitsFile<float> lazyReader( firstPath.string(), false );
1172 REQUIRE( lazyReader.read( lazyImage ) == mx::error_t::noerror );
1173 REQUIRE( lazyImage.isApprox( image ) );
1174
1175 const std::string missingPath = ( testDirectory.path() / "missing.fits" ).string();
1176 REQUIRE( fitsFile.read( lazyImage, missingPath ) != mx::error_t::noerror );
1177 REQUIRE( fitsFile.read( lazyImage, readHeader, missingPath ) != mx::error_t::noerror );
1178
1180 REQUIRE( fitsFile.read( imageCube, std::vector<std::string>{ missingPath } ) != mx::error_t::noerror );
1181
1182 fitsFile.setReadSize( 100, 100, 1, 1 );
1183 REQUIRE( fitsFile.read( imageCube, std::vector<std::string>{ firstPath.string() } ) != mx::error_t::noerror );
1185
1186 mx::improc::eigenImage<float> smallImage( 1, 1 );
1187 smallImage.setConstant( 9.0F );
1188 const std::filesystem::path smallPath = testDirectory.path() / "small.fits";
1189 REQUIRE( fitsFile.write( smallPath.string(), smallImage ) == mx::error_t::noerror );
1190 REQUIRE( fitsFile.read( imageCube, std::vector<std::string>{ firstPath.string(), smallPath.string() } ) !=
1192
1193 fitsFile_test<float> noCommentReader;
1194 noCommentReader.noComment( true );
1195 fitsHeader<> headerWithoutComments;
1196 headerWithoutComments.append( "TESTKEY" );
1197 REQUIRE( noCommentReader.readHeader( headerWithoutComments, firstPath.string() ) == mx::error_t::noerror );
1198 REQUIRE( headerWithoutComments["TESTKEY"].value<int>() == 23 );
1199 REQUIRE( headerWithoutComments["TESTKEY"].comment().empty() );
1200}
1201
1202/// Verify hciReduce's float Eigen image and cube writes round-trip with optional FITS headers.
1203/** Exercises mx::fits::fitsFile::write Eigen-array overloads with mx::verbose::vv. */
1204/**
1205 * \ingroup fitsFile_unit_tests
1206 */
1207TEST_CASE( "Verbose float Eigen FITS writes round-trip", "[ioutils::fits::fitsFile][hciReduce]" )
1208{
1209 temporaryDirectory testDirectory( "mxlib-fitsFile-write-test" );
1211
1212 mx::improc::eigenImage<float> image( 2, 3 );
1213 image << 1.0F, 2.0F, 3.0F, 4.0F, 5.0F, 6.0F;
1214
1215 const std::filesystem::path imagePath = testDirectory.path() / "image.fits";
1216 REQUIRE( fitsFile.write( imagePath.string(), image ) == mx::error_t::noerror );
1217
1219 REQUIRE( fitsFile.read( readImage, imagePath.string() ) == mx::error_t::noerror );
1220 REQUIRE( readImage.isApprox( image ) );
1221
1222 REQUIRE( fitsFile.open( imagePath.string() ) == mx::error_t::noerror );
1223 REQUIRE( fitsFile.write( imagePath.string(), image ) == mx::error_t::noerror );
1224
1226 header.append( "TESTKEY", 17, "test header value" );
1227 header.append( "PREPROC MASK", true, "apply preprocessing mask" );
1228 header.append( "QTHRESH", 0.75F, "quality threshold" );
1229 header.append( "", fitsCommentType(), "HCI preprocessing parameters" );
1230 header.append( "HISTORY", fitsHistoryType(), "coadded target000001.fits" );
1231
1232 const std::filesystem::path headerImagePath = testDirectory.path() / "image-with-header.fits";
1233 REQUIRE( fitsFile.write( headerImagePath.string(), image, header ) == mx::error_t::noerror );
1234 const std::filesystem::path unavailableImagePath = testDirectory.path() / "missing" / "image.fits";
1235 REQUIRE( fitsFile.write( unavailableImagePath.string(), image ) != mx::error_t::noerror );
1236 REQUIRE( fitsFile.write( unavailableImagePath.string(), image, header ) != mx::error_t::noerror );
1237
1238 fitsHeader<verbose::vv> readHeader;
1239 REQUIRE( fitsFile.read( readImage, readHeader, headerImagePath.string() ) == mx::error_t::noerror );
1240 REQUIRE( readImage.isApprox( image ) );
1241 REQUIRE( readHeader["TESTKEY"].value<int>() == 17 );
1242 REQUIRE( readHeader["PREPROC MASK"].String() == "1" );
1243 REQUIRE_THAT( readHeader["QTHRESH"].value<float>(), WithinAbs( 0.75F, 1e-6F ) );
1244
1245 bool foundHistory = false;
1246 for( auto &card : readHeader )
1247 {
1248 if( card.type() == fitsType<fitsHistoryType>() && card.comment() == "coadded target000001.fits" )
1249 {
1250 foundHistory = true;
1251 }
1252 }
1253 REQUIRE( foundHistory );
1254
1255 const std::filesystem::path rawImagePath = testDirectory.path() / "raw-image-with-header.fits";
1256 REQUIRE( fitsFile.write( rawImagePath.string(), image.data(), image.rows(), image.cols(), 1, header ) ==
1258 REQUIRE( fitsFile.read( readImage, readHeader, rawImagePath.string() ) == mx::error_t::noerror );
1259 REQUIRE( readImage.isApprox( image ) );
1260 REQUIRE( readHeader["TESTKEY"].value<int>() == 17 );
1261 REQUIRE( readHeader["PREPROC MASK"].String() == "1" );
1262
1263 mx::improc::eigenCube<float> cube( 2, 3, 2 );
1264 cube.image( 0 ) = image;
1265 cube.image( 1 ) = image + 10.0F;
1266
1267 const std::filesystem::path cubePath = testDirectory.path() / "cube.fits";
1268 REQUIRE( fitsFile.write( cubePath.string(), cube ) == mx::error_t::noerror );
1269
1271 REQUIRE( fitsFile.read( readCube, cubePath.string() ) == mx::error_t::noerror );
1272 REQUIRE( readCube.rows() == cube.rows() );
1273 REQUIRE( readCube.cols() == cube.cols() );
1274 REQUIRE( readCube.planes() == cube.planes() );
1275 REQUIRE( readCube.image( 0 ).isApprox( cube.image( 0 ) ) );
1276 REQUIRE( readCube.image( 1 ).isApprox( cube.image( 1 ) ) );
1277
1278 const std::filesystem::path headerCubePath = testDirectory.path() / "cube-with-header.fits";
1279 REQUIRE( fitsFile.write( headerCubePath.string(), cube, header ) == mx::error_t::noerror );
1280 const std::filesystem::path unavailableCubePath = testDirectory.path() / "missing" / "cube.fits";
1281 REQUIRE( fitsFile.write( unavailableCubePath.string(), cube, header ) != mx::error_t::noerror );
1282
1283 REQUIRE( fitsFile.read( readCube, readHeader, headerCubePath.string() ) == mx::error_t::noerror );
1284 REQUIRE( readCube.image( 0 ).isApprox( cube.image( 0 ) ) );
1285 REQUIRE( readCube.image( 1 ).isApprox( cube.image( 1 ) ) );
1286 REQUIRE( readHeader["TESTKEY"].value<int>() == 17 );
1287}
1288
1289/// Reading headers
1290/**
1291 *
1292 * \ingroup fitsFile_unit_tests
1293 */
1294TEST_CASE( "Reading headers", "[ioutils::fits::fitsFile]" )
1295{
1296// document what fitsFile_test is doing
1297#ifdef MXLIB_DOXYGEN_PROTECTED_REF
1298 fitsFile<float> ff;
1299 fitsHeader fh;
1300 fh.append( "dummy", 2, "dummy" );
1302 ff.write( "dummy", im, fh );
1303#endif
1304
1305 fitsFile_test<float> fft;
1306
1307 std::vector<std::string> fnames(
1308 { "/tmp/fitsFile_test1.fits", "/tmp/fitsFile_test2.fits", "/tmp/fitsFile_test3.fits" } );
1309 REQUIRE( fft.writeTestFrame( "/tmp/fitsFile_test", 1 ) == mx::error_t::noerror );
1310 REQUIRE( fft.writeTestFrame( "/tmp/fitsFile_test", 2 ) == mx::error_t::noerror );
1311 REQUIRE( fft.writeTestFrame( "/tmp/fitsFile_test", 3 ) == mx::error_t::noerror );
1312
1313 SECTION( "write three frames and read their headers as a vector, not allocated" )
1314 {
1315
1316 fitsFile<float> ff;
1317 std::vector<mx::fits::fitsHeader<mx::verbose::d>> fh;
1319
1320 REQUIRE( ff.readHeader( fh, fnames ) == mx::error_t::noerror );
1321 REQUIRE( fh.size() == fnames.size() );
1322
1323 REQUIRE_THAT( fh[0]["FLOAT1"].Float(), WithinAbs( 1.1, 1e-5 ) );
1324 REQUIRE( fh[0]["INT1"].Int() == 2 );
1325 REQUIRE_THAT( fh[1]["FLOAT2"].Float(), WithinAbs( 1.1, 1e-5 ) );
1326 REQUIRE( fh[1]["INT2"].Int() == 2 );
1327 REQUIRE_THAT( fh[2]["FLOAT3"].Float(), WithinAbs( 1.1, 1e-5 ) );
1328 REQUIRE( fh[2]["INT3"].Int() == 2 );
1329 }
1330
1331 SECTION( "write three frames and read their headers as a vector, allocated" )
1332 {
1333
1334 fitsFile<float> ff;
1335 std::vector<mx::fits::fitsHeader<mx::verbose::d>> fh;
1337
1338 fh.resize( fnames.size() );
1339 REQUIRE( ff.readHeader( fh, fnames ) == mx::error_t::noerror );
1340
1341 mx::error_t errc;
1342 errc = mx::error_t::error;
1343 REQUIRE_THAT( fh[0]["FLOAT1"].Float( &errc ), WithinAbs( 1.1, 1e-5 ) );
1344 REQUIRE( errc == mx::error_t::noerror );
1345 errc = mx::error_t::error;
1346 REQUIRE( fh[0]["INT1"].Int( &errc ) == 2 );
1347 REQUIRE( errc == mx::error_t::noerror );
1348 errc = mx::error_t::error;
1349 REQUIRE_THAT( fh[1]["FLOAT2"].Float( &errc ), WithinAbs( 1.1, 1e-5 ) );
1350 REQUIRE( errc == mx::error_t::noerror );
1351 errc = mx::error_t::error;
1352 REQUIRE( fh[1]["INT2"].Int( &errc ) == 2 );
1353 REQUIRE( errc == mx::error_t::noerror );
1354 errc = mx::error_t::error;
1355 REQUIRE_THAT( fh[2]["FLOAT3"].Float( &errc ), WithinAbs( 1.1, 1e-5 ) );
1356 REQUIRE( errc == mx::error_t::noerror );
1357 errc = mx::error_t::error;
1358 REQUIRE( fh[2]["INT3"].Int( &errc ) == 2 );
1359 REQUIRE( errc == mx::error_t::noerror );
1360 }
1361
1362 SECTION( "write three frames and read their headers as a vector, allocated to wrong size" )
1363 {
1364 fitsFile<float> ff;
1365 std::vector<mx::fits::fitsHeader<mx::verbose::d>> fh;
1367
1368 fh.resize( fnames.size() + 1 );
1369 REQUIRE( ff.readHeader( fh, fnames ) == mx::error_t::invalidarg );
1370 }
1371
1372 SECTION( "write three frames and read their headers as a vector, extracting only 1 keyword" )
1373 {
1374
1375 fitsFile<float> ff;
1376 std::vector<mx::fits::fitsHeader<mx::verbose::d>> fh;
1378
1379 fh.resize( fnames.size() );
1380 fh[0].append( "FLOAT1" );
1381 fh[1].append( "FLOAT2" );
1382 fh[2].append( "FLOAT3" );
1383
1384 REQUIRE( ff.readHeader( fh, fnames ) == mx::error_t::noerror );
1385
1386 mx::error_t errc;
1387
1388 REQUIRE_THAT( fh[0]["FLOAT1"].Float( &errc ), WithinAbs( 1.1, 1e-5 ) );
1389 REQUIRE( errc == mx::error_t::noerror );
1390 REQUIRE( fh[0]["FLOAT1"].valueGood() == true );
1391
1392 REQUIRE( fh[0]["INT1"].Int( &errc ) == 0 );
1393 REQUIRE( errc == mx::error_t::invalidarg );
1394 REQUIRE( fh[0]["INT1"].valueGood() == false );
1395
1396 REQUIRE_THAT( fh[1]["FLOAT2"].Float( &errc ), WithinAbs( 1.1, 1e-5 ) );
1397 REQUIRE( errc == mx::error_t::noerror );
1398 REQUIRE( fh[1]["FLOAT2"].valueGood() == true );
1399
1400 REQUIRE( fh[1]["INT2"].Int( &errc ) == 0 );
1401 REQUIRE( errc == mx::error_t::invalidarg );
1402 REQUIRE( fh[1]["INT2"].valueGood() == false );
1403
1404 REQUIRE_THAT( fh[2]["FLOAT3"].Float( &errc ), WithinAbs( 1.1, 1e-5 ) );
1405 REQUIRE( errc == mx::error_t::noerror );
1406 REQUIRE( fh[2]["FLOAT3"].valueGood() == true );
1407
1408 REQUIRE( fh[2]["INT3"].Int( &errc ) == 0 );
1409 REQUIRE( errc == mx::error_t::invalidarg );
1410 REQUIRE( fh[2]["INT3"].valueGood() == false );
1411 }
1412}
1413
1414} // namespace fitsFileTest
1415} // namespace fitsTest
1416} // namespace unitTest
1417} // namespace mx
Class to manage interactions with a FITS file.
Definition fitsFile.hpp:84
int naxis()
Get the current value of m_naxis.
Definition fitsFile.hpp:779
long getSize(error_t &errc)
Get the total size.
Definition fitsFile.hpp:885
void setCubeReadSize()
Set to read all frames from a cube.
long naxes(int dim)
Get the current value of m_naxes for the specified dimension.
Definition fitsFile.hpp:785
error_t open()
Open the file and gets its dimensions.
Definition fitsFile.hpp:796
std::string fileName()
Get the current value of m_fileName.
Definition fitsFile.hpp:755
int getDimensions(error_t &errc)
Get the number of dimensions (i.e. m_naxis).
Definition fitsFile.hpp:873
void setReadSize()
Set to read all the pixels in the file.
fitsFile()
Default constructor.
Definition fitsFile.hpp:712
error_t close()
Close the file.
Definition fitsFile.hpp:851
error_t readHeader(fitsHeader< verboseT > &head)
Read the header from the fits file.
error_t calcPixarrs(pixarrT &pixarr)
Fill in the read-size arrays for reading a subset (always used).
Definition fitsFile.hpp:956
error_t read(dataT *data)
Read the contents of the FITS file into an array.
error_t write(const dataT *im, int d1, int d2, int d3, fitsHeader< verboseT > *head)
Write the contents of a raw array to the FITS file.
Class to manage a FITS file metadata header and provide fast access to the cards by keyword.
error_t append(const fitsHeaderCard< verboseT > &card)
Append a fitsHeaderCard to the end of the header.
An image cube with an Eigen-like API.
Definition eigenCube.hpp:33
Eigen::Map< Eigen::Array< dataT, Eigen::Dynamic, Eigen::Dynamic > > image(Index n)
Returns a 2D Eigen::Eigen::Map pointed at the specified image.
An image cube with an Eigen API.
Tools for using the eigen library for image processing.
Declares and defines a class to work with a FITS file.
Eigen::Array< scalarT, -1, -1 > eigenImage
Definition of the eigenImage type, which is an alias for Eigen::Array.
error_t
The mxlib error codes.
Definition error_t.hpp:26
@ noerror
No error has occurred.
Definition error_t.hpp:27
@ fits_bad_naxis
FITS: illegal NAXIS keyword value.
Definition error_t.hpp:188
@ fits_file_not_created
FITS: could not create the named file.
Definition error_t.hpp:150
@ fits_read_error
FITS: error reading from FITS file.
Definition error_t.hpp:153
@ fits_bad_dimen
FITS: illegal number of dimensions in array.
Definition error_t.hpp:236
@ allocerr
An error occurred during memory allocation.
Definition error_t.hpp:36
@ paramnotset
A parameter was not set.
Definition error_t.hpp:32
@ fits_file_not_closed
FITS: could not close the file.
Definition error_t.hpp:154
@ invalidconfig
A config setting was invalid.
Definition error_t.hpp:30
@ filesystem
A general filesystem error occurred.
Definition error_t.hpp:39
@ invalidarg
An argument was invalid.
Definition error_t.hpp:29
@ fits_write_error
FITS: error writing to FITS file.
Definition error_t.hpp:151
@ error
A general error has occurred.
Definition error_t.hpp:28
@ fits_file_not_opened
FITS: could not open the named file.
Definition error_t.hpp:149
TEST_CASE("Calculating subimage sizes", "[ioutils::fits::fitsFile]")
Calculating subimage sizes.
constexpr int fitsType()
Return the cfitsio constant for a given data type.
@ allocationFailure
Result or workspace allocation failed.
The mxlib c++ namespace.
Definition mxlib.hpp:37
Verbosity level 2. Additional information is provided, including source file and line.
Definition error.hpp:173