mxlib
c++ tools for analyzing astronomical data and other tasks by Jared R. Males. [git repo]
Loading...
Searching...
No Matches
readColumns_test.cpp
Go to the documentation of this file.
1/** \file readColumns_test.cpp
2 * \brief Tests delimited-column input.
3 */
4#include "../../catch2/catch.hpp"
5
6#define MX_NO_ERROR_REPORTS
7
9
10#undef __readColumns_hpp__
11#define MXLIBTEST_NAMESPACE MXLIBTEST_CATCH_ALL_EXCEPTIONS_ns
12#define MXLIB_CATCH_ALL_EXCEPTIONS
14#undef MXLIBTEST_NAMESPACE
15#undef MXLIB_CATCH_ALL_EXCEPTIONS
16
17using namespace Catch::Matchers;
18
19/** \cond */
20namespace
21{
22
23enum class injectedReadColumnsFailure
24{
25 none,
26 invalidArgument,
27 outOfRange,
28 badAlloc,
29 standard,
30 unknown,
31 streamErrno,
32 streamFallback
33};
34
35mx::ioutils::readColumnsDetail::operation failingOperation = mx::ioutils::readColumnsDetail::operation::afterReadLine;
36injectedReadColumnsFailure injectedFailure = injectedReadColumnsFailure::none;
37
38void injectReadColumnsFailure( mx::ioutils::readColumnsDetail::operation operation, std::ifstream &stream )
39{
40 if( operation != failingOperation )
41 {
42 return;
43 }
44
45 switch( injectedFailure )
46 {
47 case injectedReadColumnsFailure::badAlloc:
48 throw std::bad_alloc();
49 case injectedReadColumnsFailure::standard:
50 throw std::runtime_error( "injected readColumns failure" );
51 case injectedReadColumnsFailure::unknown:
52 throw 1;
53 case injectedReadColumnsFailure::streamErrno:
54 errno = EIO;
55 stream.setstate( operation == mx::ioutils::readColumnsDetail::operation::afterReadLine
56 ? std::ios::badbit
57 : std::ios::failbit );
58 return;
59 case injectedReadColumnsFailure::streamFallback:
60 errno = 0;
61 stream.setstate( operation == mx::ioutils::readColumnsDetail::operation::afterReadLine
62 ? std::ios::badbit
63 : std::ios::failbit );
64 return;
65 default:
66 return;
67 }
68}
69
70class readColumnsHookGuard
71{
72 public:
73 readColumnsHookGuard() : m_saved( mx::ioutils::readColumnsDetail::operationHook() )
74 {
75 injectedFailure = injectedReadColumnsFailure::none;
76 mx::ioutils::readColumnsDetail::operationHook() = injectReadColumnsFailure;
77 }
78
79 ~readColumnsHookGuard()
80 {
81 mx::ioutils::readColumnsDetail::operationHook() = m_saved;
82 }
83
84 private:
85 mx::ioutils::readColumnsDetail::operationHookT m_saved;
86};
87
88struct throwingColumn
89{
90 using value_type = std::string;
91
92 void push_back( const std::string & )
93 {
94 switch( injectedFailure )
95 {
96 case injectedReadColumnsFailure::invalidArgument:
97 throw std::invalid_argument( "injected readcol failure" );
98 case injectedReadColumnsFailure::outOfRange:
99 throw std::out_of_range( "injected readcol failure" );
100 case injectedReadColumnsFailure::badAlloc:
101 throw std::bad_alloc();
102 case injectedReadColumnsFailure::standard:
103 throw std::runtime_error( "injected readcol failure" );
104 case injectedReadColumnsFailure::unknown:
105 throw 1;
106 default:
107 return;
108 }
109 }
110};
111
112} // namespace
113/** \endcond */
114
115namespace unitTest
116{
117namespace ioutilsTest
118{
119namespace readColumnsTest
120{
121
122/// Reading space delimited numeric data
123/**
124 * \ingroup readColumns_unit_tests
125 */
126TEST_CASE( "Reading space delimited numeric data", "[ioutils::readColumns]" )
127{
128 SECTION( "a single column of floating point numbers" )
129 {
130 std::string fname = "/tmp/readcol_test_single_col_float.dat";
131 std::ofstream fout;
132 fout.open( fname );
133 fout << "#a commment\n";
134 fout << "1.23\n";
135 fout << "2.15\n";
136 fout << "4.96 #with comment\n";
137 fout << "5.23\n";
138 fout << "1e-9"; // no newline on last line
139 fout.close();
140
141 SECTION( "reading as int" )
142 {
143 std::vector<int> data0;
144 mx::error_t errc = mx::ioutils::readColumns( fname, data0 );
145 REQUIRE( errc == mx::error_t::noerror );
146 REQUIRE( data0.size() == 5 );
147 REQUIRE( data0[0] == 1 );
148 REQUIRE( data0[1] == 2 );
149 REQUIRE( data0[2] == 4 );
150 REQUIRE( data0[3] == 5 );
151 REQUIRE( data0[4] == 1 ); // this is a consequence of stoi
152 }
153
154 SECTION( "reading as float" )
155 {
156 std::vector<float> data0;
157 mx::error_t errc = mx::ioutils::readColumns( fname, data0 );
158 REQUIRE( errc == mx::error_t::noerror );
159 REQUIRE( data0.size() == 5 );
160 REQUIRE_THAT( data0[0], WithinRel( 1.23, 1e-5 ) );
161 REQUIRE_THAT( data0[1], WithinRel( 2.15, 1e-5 ) );
162 REQUIRE_THAT( data0[2], WithinRel( 4.96, 1e-5 ) );
163 REQUIRE_THAT( data0[3], WithinRel( 5.23, 1e-5 ) );
164 REQUIRE_THAT( data0[4], WithinRel( 1e-9, 1e-5 ) );
165 }
166 }
167
168 SECTION( "a two columns of floating point numbers" )
169 {
170 std::string fname = "/tmp/readcol_test_two_cols_float.dat";
171 std::ofstream fout;
172 fout.open( fname );
173 fout << "#a commment\n";
174 fout << "1.23 6.78\n";
175 fout << "2.15 8.88\n";
176 fout << "4.96 -2.33#with comment\n";
177 fout << "\n"; // empty line
178 fout << " \n"; // blank line
179 fout << "5.23 9.9e5\n";
180 fout << "1e-9 -5.6e2\n";
181 fout << " #comment at end not on first char and no newline";
182 fout.close();
183
184 SECTION( "reading both as int" )
185 {
186 std::vector<int> data0, data1;
187 mx::error_t errc = mx::ioutils::readColumns( fname, data0, data1 );
188 REQUIRE( errc == mx::error_t::noerror );
189 REQUIRE( data0.size() == 5 );
190 REQUIRE( data0[0] == 1 );
191 REQUIRE( data0[1] == 2 );
192 REQUIRE( data0[2] == 4 );
193 REQUIRE( data0[3] == 5 );
194 REQUIRE( data0[4] == 1 ); // this is a consequence of stoi
195
196 REQUIRE( data1.size() == 5 );
197 REQUIRE( data1[0] == 6 );
198 REQUIRE( data1[1] == 8 );
199 REQUIRE( data1[2] == -2 );
200 REQUIRE( data1[3] == 9 ); // this is a consequence of stoi
201 REQUIRE( data1[4] == -5 ); // this is a consequence of stoi
202 }
203
204 SECTION( "reading both as float" )
205 {
206 std::vector<float> data0, data1;
207 mx::error_t errc = mx::ioutils::readColumns( fname, data0, data1 );
208 REQUIRE( errc == mx::error_t::noerror );
209 REQUIRE( data0.size() == 5 );
210 REQUIRE_THAT( data0[0], WithinRel( 1.23, 1e-5 ) );
211 REQUIRE_THAT( data0[1], WithinRel( 2.15, 1e-5 ) );
212 REQUIRE_THAT( data0[2], WithinRel( 4.96, 1e-5 ) );
213 REQUIRE_THAT( data0[3], WithinRel( 5.23, 1e-5 ) );
214 REQUIRE_THAT( data0[4], WithinRel( 1e-9, 1e-5 ) );
215
216 REQUIRE( data1.size() == 5 );
217 REQUIRE_THAT( data1[0], WithinRel( 6.78, 1e-5 ) );
218 REQUIRE_THAT( data1[1], WithinRel( 8.88, 1e-5 ) );
219 REQUIRE_THAT( data1[2], WithinRel( -2.33, 1e-5 ) );
220 REQUIRE_THAT( data1[3], WithinRel( 9.9e5, 1e-5 ) );
221 REQUIRE_THAT( data1[4], WithinRel( -5.6e2, 1e-5 ) );
222 }
223 }
224}
225
226/// Reading space delimited numeric data with errors
227/**
228 * \ingroup readColumns_unit_tests
229 */
230TEST_CASE( "Reading space delimited numeric data with errors", "[ioutils::readColumns]" )
231{
232 SECTION( "a missing file" )
233 {
234 std::vector<std::string> filenames;
235 std::vector<double> values;
237 "/tmp/readcol_test_missing_file.dat",
238 filenames,
239 values ) == mx::error_t::enoent );
240 REQUIRE( filenames.empty() );
241 REQUIRE( values.empty() );
242 }
243
244 SECTION( "two columns of floating point numbers" )
245 {
246 std::string fname = "/tmp/readcol_test_two_col_float_intoverflow.dat";
247 std::ofstream fout;
248 fout.open( fname );
249 fout << "1.23 6.7\n";
250 fout << "2.15 8.8\n";
251 fout << "4.96 9.9\n";
252 fout << "5.23 10.11\n";
253 fout << "1e-9 55555555555555555555555555555555555\n";
254 fout.close();
255
256 SECTION( "reading as int" )
257 {
258 std::vector<int> data0, data1;
259 mx::error_t errc =
261 REQUIRE( errc == mx::error_t::erange );
262 }
263
264 /*SECTION( "reading as int64_t" )
265 {
266 std::vector<int64_t> data0, data1;
267 mx::error_t errc = mx::ioutils::readColumns(fname, data0, data1);
268 REQUIRE( errc == mx::error_t::std_out_of_range );
269 }*/
270 }
271
272#if 0 // for doxygen
273 std::string fname;
274 std::vector<int> data0, data1;
275 mx::ioutils::readColumns( fname, data0, data1 );
276#endif
277}
278
279/// Reading combined type data
280/**
281 * \ingroup readColumns_unit_tests
282 */
283TEST_CASE( "Reading combined type data", "[ioutils::readColumns]" )
284{
285 SECTION( "a single column of floating point numbers" )
286 {
287 std::string fname = "/tmp/readcol_test_combos.dat";
288 std::ofstream fout;
289 fout.open( fname );
290 fout << "test1 1.23 2.56\n";
291 fout << "test2 2.15 8.93\n";
292 fout << "test3 0 0\n";
293 fout << "test4 6.7 1\n";
294 fout << "test5 0 22.2\n";
295 fout << "test6 1e-4 0\n";
296 fout.close();
297
298 std::vector<std::string> str0;
299 std::vector<float> data1, data2;
300 mx::error_t errc =
302
303 REQUIRE( errc == mx::error_t::noerror );
304
305 REQUIRE( str0[0] == "test1" );
306 REQUIRE( str0[1] == "test2" );
307 REQUIRE( str0[2] == "test3" );
308 REQUIRE( str0[3] == "test4" );
309 REQUIRE( str0[4] == "test5" );
310 REQUIRE( str0[5] == "test6" );
311
312 REQUIRE_THAT( data1[0], WithinRel( 1.23, 1e-7 ) );
313 REQUIRE_THAT( data1[1], WithinRel( 2.15, 1e-7 ) );
314 REQUIRE( data1[2] == 0 );
315 REQUIRE_THAT( data1[3], WithinRel( 6.7, 1e-7 ) );
316 REQUIRE( data1[4] == 0 );
317 REQUIRE_THAT( data1[5], WithinRel( 1e-4, 1e-7 ) );
318
319 REQUIRE_THAT( data2[0], WithinRel( 2.56, 1e-7 ) );
320 REQUIRE_THAT( data2[1], WithinRel( 8.93, 1e-7 ) );
321 REQUIRE( data2[2] == 0 );
322 REQUIRE_THAT( data2[3], WithinRel( 1, 1e-7 ) );
323 REQUIRE_THAT( data2[4], WithinRel( 22.2, 1e-7 ) );
324 REQUIRE( data2[5] == 0 );
325 }
326}
327
328/** \brief Verifies filename and double-valued metadata columns used by HCI quality and weight lists.
329 *
330 * \ingroup readColumns_unit_tests
331 */
332TEST_CASE( "Reading filename and double metadata columns", "[ioutils::readColumns]" )
333{
334 const std::string filename{ "/tmp/readcol_test_hci_metadata.dat" };
335 std::ofstream fout{ filename };
336 fout << "# image quality or weight\n";
337 fout << "target/frame001.fits 0.25\n";
338 fout << "reference/frame002.fits 1.5 # retained inline comment\n";
339 fout.close();
340
341 std::vector<std::string> filenames{ "existing.fits" };
342 std::vector<double> values{ -1.0 };
345
346 REQUIRE( filenames ==
347 std::vector<std::string>{ "existing.fits", "target/frame001.fits", "reference/frame002.fits" } );
348 REQUIRE( values == std::vector<double>{ -1.0, 0.25, 1.5 } );
349}
350
351/** \brief Verifies direct column parsing handles empty, missing, terminated, and skipped fields.
352 *
353 * \ingroup readColumns_unit_tests
354 */
355TEST_CASE( "Parsing column edge cases", "[ioutils::readColumns]" )
356{
357 SECTION( "empty input" )
358 {
359 std::vector<std::string> values;
360 int column = 0;
361 REQUIRE( mx::ioutils::readcol<mx::ioutils::readColSpaceDelim, mx::verbose::vv>( "", 0, column, values ) ==
363 REQUIRE( values.empty() );
364 }
365
366 SECTION( "whitespace-only final field" )
367 {
368 std::vector<std::string> values;
369 int column = 0;
370 REQUIRE( mx::ioutils::readcol<mx::ioutils::readColSpaceDelim, mx::verbose::vv>( " ", 3, column, values ) ==
372 REQUIRE( values == std::vector<std::string>{ "" } );
373 }
374
375 SECTION( "invalid whitespace-only numeric final field" )
376 {
377 std::vector<double> values;
378 int column = 0;
379 REQUIRE( mx::ioutils::readcol<mx::ioutils::readColSpaceDelim, mx::verbose::vv>( " ", 3, column, values ) ==
381 }
382
383 SECTION( "missing comma-delimited field" )
384 {
385 std::vector<int> values;
386 int column = 0;
387 REQUIRE( mx::ioutils::readcol<mx::ioutils::readColCommaDelim, mx::verbose::vv>( ",value", 6, column, values ) ==
389 REQUIRE( values == std::vector<int>{ -99 } );
390 }
391
392 SECTION( "terminated direct field" )
393 {
394 std::vector<std::string> values;
395 int column = 0;
396 REQUIRE(
397 mx::ioutils::readcol<mx::ioutils::readColSpaceDelim, mx::verbose::vv>( "value\n", 6, column, values ) ==
399 REQUIRE( values == std::vector<std::string>{ "value" } );
400 }
401
402 SECTION( "skipped field" )
403 {
404 const std::string filename{ "/tmp/readcol_test_skip.dat" };
405 std::ofstream fout{ filename };
406 fout << "first ignored 2.5\n";
407 fout.close();
408
409 std::vector<std::string> names;
410 mx::ioutils::skipCol skipped;
411 std::vector<double> values;
413 names,
414 skipped,
415 values ) ==
417 REQUIRE( names == std::vector<std::string>{ "first" } );
418 REQUIRE( values == std::vector<double>{ 2.5 } );
419 }
420}
421
422/** \brief Verifies per-column conversion exceptions follow the configured propagation policy.
423 *
424 * \ingroup readColumns_unit_tests
425 */
426TEST_CASE( "Handling column conversion exceptions", "[ioutils::readColumns]" )
427{
428 throwingColumn values;
429 int column = 3;
430
431 SECTION( "always-translated conversion exceptions" )
432 {
433 injectedFailure = injectedReadColumnsFailure::invalidArgument;
434 REQUIRE( mx::ioutils::readcol<mx::ioutils::readColSpaceDelim, mx::verbose::vv>( "value", 5, column, values ) ==
436
437 injectedFailure = injectedReadColumnsFailure::outOfRange;
438 REQUIRE( mx::ioutils::readcol<mx::ioutils::readColSpaceDelim, mx::verbose::vv>( "value", 5, column, values ) ==
440 }
441
442 SECTION( "allocation failure" )
443 {
444 injectedFailure = injectedReadColumnsFailure::badAlloc;
445 REQUIRE_THROWS_AS(
446 ( mx::ioutils::readcol<mx::ioutils::readColSpaceDelim, mx::verbose::vv>( "value", 5, column, values ) ),
448 REQUIRE( mx::ioutils::MXLIBTEST_CATCH_ALL_EXCEPTIONS_ns::readcol<
449 mx::ioutils::MXLIBTEST_CATCH_ALL_EXCEPTIONS_ns::readColSpaceDelim,
450 mx::verbose::vv>( "value", 5, column, values ) == mx::error_t::std_bad_alloc );
451 }
452
453 SECTION( "standard failure" )
454 {
455 injectedFailure = injectedReadColumnsFailure::standard;
456 REQUIRE_THROWS_AS(
457 ( mx::ioutils::readcol<mx::ioutils::readColSpaceDelim, mx::verbose::vv>( "value", 5, column, values ) ),
459 REQUIRE( mx::ioutils::MXLIBTEST_CATCH_ALL_EXCEPTIONS_ns::readcol<
460 mx::ioutils::MXLIBTEST_CATCH_ALL_EXCEPTIONS_ns::readColSpaceDelim,
461 mx::verbose::vv>( "value", 5, column, values ) == mx::error_t::std_exception );
462 }
463
464 SECTION( "unknown failure" )
465 {
466 injectedFailure = injectedReadColumnsFailure::unknown;
467 REQUIRE_THROWS_AS(
468 ( mx::ioutils::readcol<mx::ioutils::readColSpaceDelim, mx::verbose::vv>( "value", 5, column, values ) ),
470 REQUIRE( mx::ioutils::MXLIBTEST_CATCH_ALL_EXCEPTIONS_ns::readcol<
471 mx::ioutils::MXLIBTEST_CATCH_ALL_EXCEPTIONS_ns::readColSpaceDelim,
472 mx::verbose::vv>( "value", 5, column, values ) == mx::error_t::exception );
473 }
474}
475
476/** \brief Verifies line-read exceptions follow the configured propagation policy.
477 *
478 * \ingroup readColumns_unit_tests
479 */
480TEST_CASE( "Handling line-read exceptions", "[ioutils::readColumns]" )
481{
482 const std::string filename{ "/tmp/readcol_test_line_exceptions.dat" };
483 std::ofstream fout{ filename };
484 fout << "value 1.5\n";
485 fout.close();
486
487 readColumnsHookGuard guard;
488 failingOperation = mx::ioutils::readColumnsDetail::operation::afterReadLine;
489 std::vector<std::string> names;
490 std::vector<double> values;
491
492 SECTION( "allocation failure" )
493 {
494 injectedFailure = injectedReadColumnsFailure::badAlloc;
495 REQUIRE_THROWS_AS(
498 REQUIRE( mx::ioutils::MXLIBTEST_CATCH_ALL_EXCEPTIONS_ns::readColumns<
499 mx::ioutils::MXLIBTEST_CATCH_ALL_EXCEPTIONS_ns::readColSpaceDelim,
500 mx::verbose::vv>( filename, names, values ) == mx::error_t::std_bad_alloc );
501 }
502
503 SECTION( "standard failure" )
504 {
505 injectedFailure = injectedReadColumnsFailure::standard;
506 REQUIRE_THROWS_AS(
509 REQUIRE( mx::ioutils::MXLIBTEST_CATCH_ALL_EXCEPTIONS_ns::readColumns<
510 mx::ioutils::MXLIBTEST_CATCH_ALL_EXCEPTIONS_ns::readColSpaceDelim,
511 mx::verbose::vv>( filename, names, values ) == mx::error_t::std_exception );
512 }
513
514 SECTION( "unknown failure" )
515 {
516 injectedFailure = injectedReadColumnsFailure::unknown;
517 REQUIRE_THROWS_AS(
520 REQUIRE( mx::ioutils::MXLIBTEST_CATCH_ALL_EXCEPTIONS_ns::readColumns<
521 mx::ioutils::MXLIBTEST_CATCH_ALL_EXCEPTIONS_ns::readColSpaceDelim,
522 mx::verbose::vv>( filename, names, values ) == mx::error_t::exception );
523 }
524}
525
526/** \brief Verifies open, read, and close stream errors retain errno or use the documented fallback code.
527 *
528 * \ingroup readColumns_unit_tests
529 */
530TEST_CASE( "Handling column file stream errors", "[ioutils::readColumns]" )
531{
532 const std::string filename{ "/tmp/readcol_test_stream_errors.dat" };
533 std::ofstream fout{ filename };
534 fout << "value 1.5\n";
535 fout.close();
536
537 readColumnsHookGuard guard;
538 std::vector<std::string> names;
539 std::vector<double> values;
540
541 for( auto operation : { mx::ioutils::readColumnsDetail::operation::afterOpen,
542 mx::ioutils::readColumnsDetail::operation::afterReadLine,
543 mx::ioutils::readColumnsDetail::operation::afterClose } )
544 {
545 failingOperation = operation;
546
547 injectedFailure = injectedReadColumnsFailure::streamErrno;
550
551 injectedFailure = injectedReadColumnsFailure::streamFallback;
552 const mx::error_t expected =
553 operation == mx::ioutils::readColumnsDetail::operation::afterOpen ? mx::error_t::fileoerr
554 : operation == mx::ioutils::readColumnsDetail::operation::afterReadLine ? mx::error_t::filererr
557 expected );
558 }
559}
560
561} // namespace readColumnsTest
562} // namespace ioutilsTest
563} // namespace unitTest
Augments an exception with the source file and line.
Definition exception.hpp:42
error_t readColumns(const std::string &fname, arrTs &...arrays)
Read in columns from a text file.
error_t
The mxlib error codes.
Definition error_t.hpp:26
@ noerror
No error has occurred.
Definition error_t.hpp:27
@ std_exception
An exception was thrown.
Definition error_t.hpp:52
@ filererr
An error occurred while reading from a file.
Definition error_t.hpp:42
@ exception
An exception was thrown.
Definition error_t.hpp:51
@ std_bad_alloc
A bad allocation exception was thrown.
Definition error_t.hpp:53
@ erange
Numerical result out of range (ERANGE).
Definition error_t.hpp:138
@ enoent
No such file or directory (ENOENT).
Definition error_t.hpp:111
@ filecerr
An error occurred while closing a file.
Definition error_t.hpp:43
@ eio
Input/output error (EIO).
Definition error_t.hpp:96
@ fileoerr
An error occurred while opening a file.
Definition error_t.hpp:40
@ std_out_of_range
An out of range exception was thrown.
Definition error_t.hpp:58
@ invalidarg
An argument was invalid.
Definition error_t.hpp:29
@ std_invalid_argument
An invalid argument exception was thrown.
Definition error_t.hpp:55
TEST_CASE("Reading space delimited numeric data", "[ioutils::readColumns]")
Reading space delimited numeric data.
A utility to read in columns from a text file.
A dummy class to allow mx::readColumns to skip a column(s) in a file without requiring memory allocat...
Verbosity level 2. Additional information is provided, including source file and line.
Definition error.hpp:173