mxlib
c++ tools for analyzing astronomical data and other tasks by Jared R. Males. [git repo]
Loading...
Searching...
No Matches
fitsHeader_test.cpp
Go to the documentation of this file.
1/** \file fitsHeader_test.cpp
2 * \brief Tests FITS-header value extraction.
3 */
4#include "../../../catch2/catch.hpp"
5
6using namespace Catch::Matchers;
7
9using namespace mx::fits;
10
12
13namespace mx
14{
15namespace unitTest
16{
17namespace fitsTest
18{
19namespace fitsHeaderTest
20{
21
22/// \cond
23template <class headerT>
24std::vector<std::string> keywords( headerT &header )
25{
26 std::vector<std::string> result;
27 for( auto &card : header )
28 {
29 result.push_back( card.keyword() );
30 }
31 return result;
32}
33
34enum class mutationFailureKind
35{
36 returnedError,
37 badAllocation,
38 standardException
39};
40
41fitsHeaderDetail::mutation failingMutation = fitsHeaderDetail::mutation::appendList;
42mutationFailureKind mutationFailure = mutationFailureKind::returnedError;
43
44mx::error_t failMutation( fitsHeaderDetail::mutation operation )
45{
46 if( operation != failingMutation )
47 {
49 }
50
51 if( mutationFailure == mutationFailureKind::badAllocation )
52 {
53 throw std::bad_alloc();
54 }
55 if( mutationFailure == mutationFailureKind::standardException )
56 {
57 throw std::runtime_error( "fitsHeader mutation failure" );
58 }
59 return mx::error_t::error;
60}
61
62class mutationHookGuard
63{
64 public:
65 mutationHookGuard() : m_saved( fitsHeaderDetail::mutationHook() )
66 {
67 fitsHeaderDetail::mutationHook() = failMutation;
68 }
69
70 ~mutationHookGuard()
71 {
72 fitsHeaderDetail::mutationHook() = m_saved;
73 }
74
75 private:
76 fitsHeaderDetail::mutationHookT m_saved;
77};
78/// \endcond
79
80/** \brief Verifies empty-state, append, lookup, copy, assignment, and clearing behavior.
81 *
82 * \ingroup fitsHeader_unit_tests
83 */
84TEST_CASE( "Managing the fitsHeader lifecycle", "[ioutils::fits::fitsHeader]" )
85{
86 fitsHeader<> header;
87 REQUIRE( header.empty() );
88 REQUIRE( header.size() == 0 );
89 REQUIRE( header.begin() == header.end() );
90 REQUIRE( header.iterator( "MISSING" ) == header.end() );
91 REQUIRE( header.count( "MISSING" ) == 0 );
92
93 REQUIRE( header.append( "UNKNOWN" ) == mx::error_t::noerror );
94 REQUIRE( header.append( "INTEGER", 42 ) == mx::error_t::noerror );
95 REQUIRE( header.append( "STRING", "value", "a comment" ) == mx::error_t::noerror );
96 REQUIRE( header.append( fitsHeaderCard<>( "FLOAT", 2.5F, "float comment" ) ) == mx::error_t::noerror );
97 REQUIRE_FALSE( header.empty() );
98 REQUIRE( header.size() == 4 );
99 REQUIRE( header.iterator( "INTEGER" ) != header.end() );
100 REQUIRE( header.iterator( "INTEGER" )->keyword() == "INTEGER" );
101 REQUIRE( header.iterator( "INTEGER" )->type() == fitsType<int>() );
102 REQUIRE( header.iterator( "INTEGER" )->valueGood() );
103 REQUIRE( header["INTEGER"].value<int>() == 42 );
104 REQUIRE( header["STRING"].String() == "value" );
105 REQUIRE( header["STRING"].comment() == "a comment" );
106 REQUIRE( header["FLOAT"].value<float>() == 2.5F );
107 REQUIRE( header.append( "INTEGER", 43 ) == mx::error_t::invalidarg );
108 REQUIRE( header.size() == 4 );
109
110 fitsHeader<> copied( header );
111 REQUIRE( copied.size() == 4 );
112 REQUIRE( copied["INTEGER"].value<int>() == 42 );
113 REQUIRE( copied["INTEGER"].value( 7 ) == mx::error_t::noerror );
114 REQUIRE( header["INTEGER"].value<int>() == 42 );
115
116 fitsHeader<> assigned;
117 REQUIRE( assigned.append( "OLD", 1 ) == mx::error_t::noerror );
118 assigned = header;
119 REQUIRE( assigned.size() == 4 );
120 REQUIRE( assigned.count( "OLD" ) == 0 );
121 REQUIRE( assigned["STRING"].String() == "value" );
122 assigned = assigned;
123 REQUIRE( assigned.size() == 4 );
124 REQUIRE( assigned["INTEGER"].value<int>() == 42 );
125
126 REQUIRE( assigned.clear() == mx::error_t::noerror );
127 REQUIRE( assigned.clear() == mx::error_t::noerror );
128 REQUIRE( assigned.empty() );
129 REQUIRE( assigned.iterator( "INTEGER" ) == assigned.end() );
130}
131
132/** \brief Verifies mutable and const fitsHeader keyword access, including missing-card sentinels.
133 *
134 * \ingroup fitsHeader_unit_tests
135 */
136TEST_CASE( "Accessing fitsHeader cards by keyword", "[ioutils::fits::fitsHeader]" )
137{
138 fitsHeader<> header;
139 REQUIRE( header.append( "EXISTING", 3, "existing card" ) == mx::error_t::noerror );
140 REQUIRE( header["EXISTING"].value<int>() == 3 );
141
142 fitsHeaderCard<> &created = header["CREATED"];
143 REQUIRE( created.keyword() == "CREATED" );
144 REQUIRE( created.type() == fitsType<fitsUnknownType>() );
145 REQUIRE( header.size() == 2 );
146 REQUIRE( header.count( "CREATED" ) == 1 );
147
148 const fitsHeader<> &constHeader = header;
149 REQUIRE( constHeader["EXISTING"].keyword() == "EXISTING" );
150 const fitsHeaderCard<> &missing = constHeader["MISSING"];
151 REQUIRE( missing.keyword().empty() );
152 const_cast<fitsHeaderCard<> &>( missing ).keyword( "changed sentinel" );
153 REQUIRE( constHeader["ALSO MISSING"].keyword().empty() );
154 REQUIRE( header.size() == 2 );
155}
156
157/** \brief Verifies fitsHeader erasure by keyword and exact list iterator.
158 *
159 * \ingroup fitsHeader_unit_tests
160 */
161TEST_CASE( "Erasing fitsHeader cards", "[ioutils::fits::fitsHeader]" )
162{
163 fitsHeader<> header;
164 REQUIRE( header.append( "FIRST", 1 ) == mx::error_t::noerror );
165 REQUIRE( header.append( "COMMENT", fitsCommentType(), "same" ) == mx::error_t::noerror );
166 REQUIRE( header.append( "COMMENT", fitsCommentType(), "same" ) == mx::error_t::noerror );
167 REQUIRE( header.append( "HISTORY", fitsHistoryType(), "event" ) == mx::error_t::noerror );
168 REQUIRE( header.append( "LAST", 2 ) == mx::error_t::noerror );
169
170 REQUIRE( header.count( "COMMENT" ) == 2 );
171 REQUIRE( header.erase( "FIRST" ) == mx::error_t::noerror );
172 REQUIRE( header.count( "FIRST" ) == 0 );
173 REQUIRE( header.erase( "FIRST" ) == mx::error_t::notfound );
174 REQUIRE( header.erase( "COMMENT" ) == mx::error_t::invalidarg );
175 REQUIRE( header.erase( "HISTORY" ) == mx::error_t::invalidarg );
176 REQUIRE( header.erase( header.end() ) == mx::error_t::invalidarg );
177
178 auto comment = header.begin();
179 REQUIRE( comment->keyword() == "COMMENT" );
180 REQUIRE( header.erase( comment ) == mx::error_t::noerror );
181 REQUIRE( header.count( "COMMENT" ) == 1 );
182 REQUIRE( header.size() == 3 );
183
184 auto history = header.iterator( "HISTORY" );
185 REQUIRE( history != header.end() );
186 REQUIRE( header.erase( history ) == mx::error_t::noerror );
187 REQUIRE( header.count( "HISTORY" ) == 0 );
188 REQUIRE( keywords( header ) == std::vector<std::string>{ "COMMENT", "LAST" } );
189}
190
191/** \brief Verifies ordered fitsHeader insertion and duplicate-key rejection.
192 *
193 * \ingroup fitsHeader_unit_tests
194 */
195TEST_CASE( "Inserting ordered fitsHeader cards", "[ioutils::fits::fitsHeader]" )
196{
197 fitsHeader<> header;
198 REQUIRE( header.append( "SECOND", 2 ) == mx::error_t::noerror );
199 REQUIRE( header.append( "FOURTH", 4 ) == mx::error_t::noerror );
200
201 REQUIRE( header.insert_before( header.begin(), "FIRST", 1, "first" ) == mx::error_t::noerror );
202 REQUIRE( header.insert_after( header.iterator( "SECOND" ), "THIRD", 3, "third" ) == mx::error_t::noerror );
203 REQUIRE( header.insert_before( header.end(), fitsHeaderCard<>( "FIFTH", 5, "" ) ) == mx::error_t::noerror );
204 REQUIRE( keywords( header ) == std::vector<std::string>{ "FIRST", "SECOND", "THIRD", "FOURTH", "FIFTH" } );
205 REQUIRE( header["FIRST"].comment() == "first" );
206 REQUIRE( header["THIRD"].comment() == "third" );
207
208 REQUIRE( header.insert_before( header.begin(), "FIRST", 10 ) == mx::error_t::invalidarg );
209 REQUIRE( header.insert_after( header.iterator( "SECOND" ), "THIRD", 30 ) == mx::error_t::invalidarg );
210 REQUIRE( header.insert_after( header.end(), "INVALID", 0 ) == mx::error_t::invalidarg );
211 REQUIRE( header.count( "INVALID" ) == 0 );
212
213 REQUIRE( header.insert_before( header.begin(), fitsHeaderCard<>( "COMMENT", fitsCommentType(), "before" ) ) ==
215 REQUIRE( header.insert_after( header.begin(), fitsHeaderCard<>( "COMMENT", fitsCommentType(), "after" ) ) ==
217 REQUIRE( header.count( "COMMENT" ) == 2 );
218}
219
220/** \brief Verifies removal of standard top-of-header FITS cards and boilerplate comments.
221 *
222 * \ingroup fitsHeader_unit_tests
223 */
224TEST_CASE( "Removing standard FITS header cards", "[ioutils::fits::fitsHeader]" )
225{
226 fitsHeader<> header;
227 for( const std::string &keyword :
228 { "SIMPLE", "BITPIX", "NAXIS", "NAXIS1", "NAXIS2", "NAXIS3", "EXTEND", "BZERO", "BSCALE", "LONGSTRN" } )
229 {
230 REQUIRE( header.append( keyword, 1 ) == mx::error_t::noerror );
231 }
232 REQUIRE( header.append( "COMMENT", fitsCommentType(), "FITS (Flexible Image Transport System)" ) ==
234 REQUIRE( header.append( "COMMENT", fitsCommentType(), "Astronomy and Astrophysics' convention" ) ==
236 REQUIRE( header.append( "COMMENT", fitsCommentType(), "preserve this comment" ) == mx::error_t::noerror );
237 REQUIRE( header.append( "HISTORY", fitsHistoryType(), "preserve this history" ) == mx::error_t::noerror );
238 REQUIRE( header.append( "OBJECT", "target" ) == mx::error_t::noerror );
239
240 REQUIRE( header.eraseStandardTop() == mx::error_t::noerror );
241 REQUIRE( keywords( header ) == std::vector<std::string>{ "COMMENT", "HISTORY", "OBJECT" } );
242 REQUIRE( header.begin()->comment() == "preserve this comment" );
243}
244
245/** \brief Verifies fitsHeader concatenation, duplicate propagation, self-append safety, and CONTINUE validation.
246 *
247 * \ingroup fitsHeader_unit_tests
248 */
249TEST_CASE( "Appending complete FITS headers", "[ioutils::fits::fitsHeader]" )
250{
251 fitsHeader<> header;
252 fitsHeader<> source;
253 REQUIRE( header.append( "FIRST", 1 ) == mx::error_t::noerror );
254 REQUIRE( source.append( "SECOND", 2 ) == mx::error_t::noerror );
255 REQUIRE( source.append( "THIRD", 3 ) == mx::error_t::noerror );
256 REQUIRE( header.append( source ) == mx::error_t::noerror );
257 REQUIRE( keywords( header ) == std::vector<std::string>{ "FIRST", "SECOND", "THIRD" } );
258
259 fitsHeader<> duplicate;
260 REQUIRE( duplicate.append( "THIRD", 30 ) == mx::error_t::noerror );
261 REQUIRE( duplicate.append( "FOURTH", 4 ) == mx::error_t::noerror );
262 REQUIRE( header.append( duplicate ) == mx::error_t::invalidarg );
263 REQUIRE( header.count( "FOURTH" ) == 0 );
264
265 REQUIRE( header.append( header ) == mx::error_t::invalidarg );
266 REQUIRE( header.size() == 3 );
267
268 fitsHeader<> empty;
269 fitsHeaderCard<> continuation( "CONTINUE", "'continued text'", fitsType<fitsContinueType>() );
270 REQUIRE( empty.append( continuation ) == mx::error_t::invalidarg );
271 REQUIRE( empty.empty() );
272
273 fitsHeader<> continued;
274 REQUIRE( continued.append( "LONG", "first&", "initial comment" ) == mx::error_t::noerror );
275 fitsHeaderCard<> finalContinuation( "CONTINUE", "", fitsType<fitsContinueType>(), "'second' / final comment" );
276 REQUIRE( continued.append( finalContinuation ) == mx::error_t::noerror );
277 REQUIRE( continued.size() == 1 );
278 REQUIRE( continued["LONG"].String() == "firstsecond" );
279 REQUIRE( continued["LONG"].comment() == "final comment" );
280}
281
282/** \brief Verifies fitsHeader propagates allocation failures and rolls back partially completed mutations.
283 *
284 * \ingroup fitsHeader_unit_tests
285 */
286TEST_CASE( "Propagating fitsHeader mutation failures", "[ioutils::fits::fitsHeader]" )
287{
288 mutationHookGuard guard;
289
290 failingMutation = fitsHeaderDetail::mutation::appendList;
291 mutationFailure = mutationFailureKind::returnedError;
292 fitsHeader<> lookupHeader;
293 fitsHeaderCard<> &missing = lookupHeader["FAILED"];
294 REQUIRE( missing.keyword().empty() );
295 REQUIRE( lookupHeader.empty() );
296
297 for( mutationFailureKind failure : { mutationFailureKind::returnedError,
298 mutationFailureKind::badAllocation,
299 mutationFailureKind::standardException } )
300 {
301 for( fitsHeaderDetail::mutation operation :
302 { fitsHeaderDetail::mutation::appendList, fitsHeaderDetail::mutation::appendMap } )
303 {
304 fitsHeader<> header;
305 failingMutation = operation;
306 mutationFailure = failure;
307 if( failure == mutationFailureKind::returnedError )
308 {
309 REQUIRE( header.append( "CARD", 1 ) == mx::error_t::error );
310 }
311 else
312 {
313 REQUIRE_THROWS( header.append( "CARD", 1 ) );
314 }
315 REQUIRE( header.empty() );
316 REQUIRE( header.count( "CARD" ) == 0 );
317 }
318 }
319
320 for( mutationFailureKind failure : { mutationFailureKind::returnedError,
321 mutationFailureKind::badAllocation,
322 mutationFailureKind::standardException } )
323 {
324 for( fitsHeaderDetail::mutation operation :
325 { fitsHeaderDetail::mutation::insertBeforeList, fitsHeaderDetail::mutation::insertBeforeMap } )
326 {
327 fitsHeaderDetail::resetMutationHook();
328 fitsHeader<> header;
329 REQUIRE( header.append( "ANCHOR", 1 ) == mx::error_t::noerror );
330 fitsHeaderDetail::mutationHook() = failMutation;
331 failingMutation = operation;
332 mutationFailure = failure;
333 if( failure == mutationFailureKind::returnedError )
334 {
335 REQUIRE( header.insert_before( header.begin(), "CARD", 2 ) == mx::error_t::error );
336 }
337 else
338 {
339 REQUIRE_THROWS( header.insert_before( header.begin(), "CARD", 2 ) );
340 }
341 REQUIRE( keywords( header ) == std::vector<std::string>{ "ANCHOR" } );
342 REQUIRE( header.count( "CARD" ) == 0 );
343 }
344 }
345
346 for( mutationFailureKind failure : { mutationFailureKind::returnedError,
347 mutationFailureKind::badAllocation,
348 mutationFailureKind::standardException } )
349 {
350 for( fitsHeaderDetail::mutation operation :
351 { fitsHeaderDetail::mutation::insertAfterList, fitsHeaderDetail::mutation::insertAfterMap } )
352 {
353 fitsHeaderDetail::resetMutationHook();
354 fitsHeader<> header;
355 REQUIRE( header.append( "ANCHOR", 1 ) == mx::error_t::noerror );
356 fitsHeaderDetail::mutationHook() = failMutation;
357 failingMutation = operation;
358 mutationFailure = failure;
359 if( failure == mutationFailureKind::returnedError )
360 {
361 REQUIRE( header.insert_after( header.begin(), "CARD", 2 ) == mx::error_t::error );
362 }
363 else
364 {
365 REQUIRE_THROWS( header.insert_after( header.begin(), "CARD", 2 ) );
366 }
367 REQUIRE( keywords( header ) == std::vector<std::string>{ "ANCHOR" } );
368 REQUIRE( header.count( "CARD" ) == 0 );
369 }
370 }
371}
372
373/// Test functionality of headersToValues
374/**
375 *
376 * \ingroup fitsHeader_unit_tests
377 */
378TEST_CASE( "Test functionality of headersToValues", "[ioutils::fits::fitsHeader]" )
379{
380// to force doxygen linking
381#ifdef MXLIB_DOXYGEN_PROTECTED_REF
382#endif
383
384 SECTION( "a vector of floating point values, no errors" )
385 {
386 std::vector<fitsHeader<mx::verbose::d>> fhs( 5 );
387
388 fhs[0].append<float>( "KEYTEST", 1, "test comment" );
389 fhs[1].append<float>( "KEYTEST", 2, "test comment" );
390 fhs[2].append<float>( "KEYTEST", 3, "test comment" );
391 fhs[3].append<float>( "KEYTEST", 4, "test comment" );
392 fhs[4].append<float>( "KEYTEST", 5, "test comment" );
393
394 std::vector<float> vals;
395 std::vector<size_t> bad;
396 mx::error_t errc = headersToValues( vals, bad, fhs, "KEYTEST" );
397
398 REQUIRE( !errc );
399
400 REQUIRE( vals[0] == 1 );
401 REQUIRE( vals[1] == 2 );
402 REQUIRE( vals[2] == 3 );
403 REQUIRE( vals[3] == 4 );
404 REQUIRE( vals[4] == 5 );
405 }
406
407 SECTION( "a vector of floating point values, no errors" )
408 {
409 std::vector<fitsHeader<mx::verbose::d>> fhs( 5 );
410
411 fhs[0].append<float>( "KEYTEST", 1, "test comment" );
412 fhs[1].append<float>( "KEYTEST", 2, "test comment" );
413 // fhs[2] is not set
414 fhs[3].append<float>( "KEYTEST", 4, "test comment" );
415 fhs[4].append<float>( "KEYTEST", 5, "test comment" );
416
417 std::vector<float> vals;
418 std::vector<size_t> bad;
419 mx::error_t errc = headersToValues( vals, bad, fhs, "KEYTEST" );
420
421 REQUIRE( !!errc );
422
423 REQUIRE( vals[0] == 1 );
424 REQUIRE( vals[1] == 2 );
425 REQUIRE( vals[2] == std::numeric_limits<float>::max() );
426 REQUIRE( vals[3] == 4 );
427 REQUIRE( vals[4] == 5 );
428 }
429}
430
431/** \brief Verifies metadata, repeated provenance cards, and copied headers used by HCI output writers.
432 *
433 * \ingroup fitsHeader_unit_tests
434 */
435TEST_CASE( "Appending HCI-style FITS provenance", "[ioutils::fits::fitsHeader][hciReduce]" )
436{
438 REQUIRE( header.append( "MJD", 60000.25, "coadd midpoint" ) == mx::error_t::noerror );
439 REQUIRE( header.append( "QFILE", "quality.dat", "quality file" ) == mx::error_t::noerror );
440 REQUIRE( header.append( "", fitsCommentType(), "HCI parameters" ) == mx::error_t::noerror );
441 REQUIRE( header.append( "", fitsCommentType(), "HCI parameters continued" ) == mx::error_t::noerror );
442 REQUIRE( header.append( "HISTORY", fitsHistoryType(), "coadded frame001.fits" ) == mx::error_t::noerror );
443 REQUIRE( header.append( "HISTORY", fitsHistoryType(), "coadded frame002.fits" ) == mx::error_t::noerror );
444
446 REQUIRE( source.append( "ORIGIN", "hciReduce", "producer" ) == mx::error_t::noerror );
447 REQUIRE( header.append( source ) == mx::error_t::noerror );
448
449 REQUIRE( header.size() == 7 );
450 REQUIRE( header["MJD"].value<double>() == 60000.25 );
451 REQUIRE( header["QFILE"].String() == "quality.dat" );
452 REQUIRE( header["ORIGIN"].String() == "hciReduce" );
453 REQUIRE( header.append( "MJD", 60001.0, "duplicate" ) == mx::error_t::invalidarg );
454 REQUIRE( header.size() == 7 );
455}
456
457/** \brief Verifies HCI coadd updates of midpoint, start, end, and delta metadata cards.
458 *
459 * \ingroup fitsHeader_unit_tests
460 */
461TEST_CASE( "Updating HCI coadd time and keyword cards", "[ioutils::fits::fitsHeader][hciReduce]" )
462{
464 REQUIRE( header.append( "MJD-OBS", "2000-01-01T00:00:00.000000000Z", "coadd midpoint" ) == mx::error_t::noerror );
465 REQUIRE( header.append( "ANGLE", 10.0, "derotation angle" ) == mx::error_t::noerror );
466
467 REQUIRE( header["MJD-OBS"].value( "2000-01-01T12:00:00.000000000Z" ) == mx::error_t::noerror );
468 REQUIRE( header["START MJD-OBS"].value( "2000-01-01T00:00:00.000000000Z" ) == mx::error_t::noerror );
469 REQUIRE( header["END MJD-OBS"].value( "2000-01-01T12:00:00.000000000Z" ) == mx::error_t::noerror );
470 REQUIRE( header["DELTA MJD-OBS"].value( 0.5 ) == mx::error_t::noerror );
471
472 REQUIRE( header["ANGLE"].value( 15.0 ) == mx::error_t::noerror );
473 REQUIRE( header["START ANGLE"].value( 10.0 ) == mx::error_t::noerror );
474 REQUIRE( header["END ANGLE"].value( 20.0 ) == mx::error_t::noerror );
475 REQUIRE( header["DELTA ANGLE"].value( 10.0 ) == mx::error_t::noerror );
476
477 REQUIRE( header["MJD-OBS"].String() == "2000-01-01T12:00:00.000000000Z" );
478 REQUIRE( header["START MJD-OBS"].String() == "2000-01-01T00:00:00.000000000Z" );
479 REQUIRE( header["END MJD-OBS"].String() == "2000-01-01T12:00:00.000000000Z" );
480 REQUIRE( header["DELTA MJD-OBS"].value<double>() == 0.5 );
481 REQUIRE( header["ANGLE"].value<double>() == 15.0 );
482 REQUIRE( header["START ANGLE"].value<double>() == 10.0 );
483 REQUIRE( header["END ANGLE"].value<double>() == 20.0 );
484 REQUIRE( header["DELTA ANGLE"].value<double>() == 10.0 );
485}
486
487/** \brief Verifies FITS HISTORY provenance for HCI final-image outputs.
488 *
489 * \ingroup fitsHeader_unit_tests
490 */
491TEST_CASE( "Appending HCI Git status provenance", "[ioutils::fits::fitsHeader][hciReduce]" )
492{
493 fitsHeader<mx::verbose::vv> cleanHeader;
494 fitsHeaderGitStatus( cleanHeader, "mxlib_uncomp", "abc123", 0 );
495
496 std::vector<std::string> cleanHistory;
497 for( auto &card : cleanHeader )
498 {
499 if( card.type() == fitsType<fitsHistoryType>() )
500 {
501 cleanHistory.push_back( card.comment() );
502 }
503 }
504
505 REQUIRE( cleanHistory == std::vector<std::string>{ "Git status for mxlib_uncomp:", " sha1=abc123" } );
506
507 fitsHeader<mx::verbose::vv> modifiedHeader;
508 fitsHeaderGitStatus( modifiedHeader, "mxlib_uncomp", "abc123", 1 );
509
510 std::vector<std::string> modifiedHistory;
511 for( auto &card : modifiedHeader )
512 {
513 if( card.type() == fitsType<fitsHistoryType>() )
514 {
515 modifiedHistory.push_back( card.comment() );
516 }
517 }
518
519 REQUIRE( modifiedHistory ==
520 std::vector<std::string>{ "Git status for mxlib_uncomp:", " sha1=abc123, modified" } );
521}
522
523/** \brief Verifies the scalar and string FITS metadata types emitted by HCI stdFitsHeader.
524 *
525 * \ingroup fitsHeader_unit_tests
526 */
527TEST_CASE( "Appending HCI reduction parameter metadata", "[ioutils::fits::fitsHeader][hciReduce]" )
528{
530 char modes[] = "1,5,10";
531
532 REQUIRE( header.append<int>( "NUMIMS", 5, "images processed" ) == mx::error_t::noerror );
533 REQUIRE( header.append<float>( "QTHRESH", 0.75F, "quality threshold" ) == mx::error_t::noerror );
534 REQUIRE( header.append<bool>( "PREPROC MASK", true, "apply preprocessing mask" ) == mx::error_t::noerror );
535 REQUIRE( header.append<std::string>( "COADMTHD", "mean", "coadd method" ) == mx::error_t::noerror );
536 REQUIRE( header.append<char *>( "NMODES", modes, "number of modes" ) == mx::error_t::noerror );
537
538 REQUIRE( header["NUMIMS"].value<int>() == 5 );
539 REQUIRE_THAT( header["QTHRESH"].value<float>(), WithinAbs( 0.75F, 1e-6F ) );
540 REQUIRE( header["PREPROC MASK"].type() == fitsType<bool>() );
541 REQUIRE( header["PREPROC MASK"].valueGood() );
542 REQUIRE( header["COADMTHD"].String() == "mean" );
543 REQUIRE( header["NMODES"].String() == "1,5,10" );
544}
545
546} // namespace fitsHeaderTest
547} // namespace fitsTest
548} // namespace unitTest
549} // namespace mx
Class to manage the three components of a FITS header card.
int type() const
Get the type.
const std::string & keyword() const
Get the keyword.
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.
size_t count(const std::string &keyword)
Get number of cards with a given keyword.
error_t insert_before(headerIteratorT it, fitsHeaderCard< verboseT > card)
Insert a card before another card.
headerIteratorT iterator(const std::string &keyword)
Get iterator pointing to a specific element.
error_t erase(const std::string &keyword)
Erase card by keyword.
size_t size()
Get number of cards currently stored in the header.
error_t eraseStandardTop()
Erase the standard entries at the top of the header.
bool empty()
Test whether the header is empty.
error_t clear()
Clear all cards from the header.
headerIteratorT end()
Get iterator to the end of the cards list.
headerIteratorT begin()
Get iterator to the beginning of the cards list.
error_t insert_after(headerIteratorT it, fitsHeaderCard< verboseT > card)
Insert a card after another card.
Tools for using the eigen library for image processing.
Declares and defines a class to work with a FITS header.
error_t
The mxlib error codes.
Definition error_t.hpp:26
@ noerror
No error has occurred.
Definition error_t.hpp:27
@ invalidarg
An argument was invalid.
Definition error_t.hpp:29
@ notfound
An item was not found.
Definition error_t.hpp:34
@ error
A general error has occurred.
Definition error_t.hpp:28
TEST_CASE("Managing the fitsHeader lifecycle", "[ioutils::fits::fitsHeader]")
Verifies empty-state, append, lookup, copy, assignment, and clearing behavior.
constexpr int fitsType()
Return the cfitsio constant for a given data type.
void fitsHeaderGitStatus(fitsHeaderT &head, const std::string &repoName, const char *sha1, int modified)
Write the status of a Git repository to HISTORY in a FITS header.
error_t headersToValues(std::vector< dataT > &v, std::vector< size_t > &bad, std::vector< fitsHeaderT > &heads, const std::string &keyw)
Convert the values in a std::vector of fits headers into a std::vector of values.
The mxlib c++ namespace.
Definition mxlib.hpp:37