|
2 | 2 | from datetime import datetime |
3 | 3 | from unittest.mock import mock_open, patch |
4 | 4 |
|
| 5 | +import numpy as np |
| 6 | + |
5 | 7 | from file_manager import FileManager |
6 | 8 |
|
7 | 9 | from hecdss import HecDss |
@@ -565,6 +567,101 @@ def test_read_write_with_missing(self): |
565 | 567 | self.assertIn("Type,Date/Time,INST-VAL", written_data) |
566 | 568 | self.assertIn("2,01Sep2021 1200,\r\n", written_data) |
567 | 569 |
|
| 570 | + def test_to_csv_writes_empty_cell_for_missing_value(self): |
| 571 | + """A missing (None) value is written as an empty cell, never the literal |
| 572 | + text 'None'. (Isolates the write path with an injected None, independent |
| 573 | + of what the reader produces.)""" |
| 574 | + rts = RegularTimeSeries.create( |
| 575 | + values=[1.0, 2.0], |
| 576 | + times=[datetime(2021, 9, 1, 6, 0), datetime(2021, 9, 1, 12, 0)], |
| 577 | + units="CFS", |
| 578 | + data_type="INST-VAL", |
| 579 | + path="/A/B/C//6Hour/F/", |
| 580 | + ) |
| 581 | + rts.values = np.array([1.0, None], dtype=object) # simulate a missing value |
| 582 | + mock_file = mock_open() |
| 583 | + with patch("builtins.open", mock_file): |
| 584 | + rts.to_csv("fake.csv", with_metadata=False) |
| 585 | + handle = mock_file() |
| 586 | + written = "".join(call.args[0] for call in handle.write.call_args_list) |
| 587 | + self.assertIn("2,01Sep2021 1200,\r\n", written) |
| 588 | + self.assertNotIn("None", written) |
| 589 | + |
| 590 | + def test_to_csv_quality_shorter_than_values_truncates(self): |
| 591 | + """FOOTGUN: to_csv takes the with-quality branch whenever quality is |
| 592 | + non-empty, then zips (times, values, quality). A quality list shorter than |
| 593 | + values makes zip stop at the shortest input, so trailing data points are |
| 594 | + silently dropped from the CSV.""" |
| 595 | + rts = RegularTimeSeries.create( |
| 596 | + values=[1.0, 2.0, 3.0], |
| 597 | + times=[ |
| 598 | + datetime(2021, 9, 1, 6, 0), |
| 599 | + datetime(2021, 9, 1, 12, 0), |
| 600 | + datetime(2021, 9, 1, 18, 0), |
| 601 | + ], |
| 602 | + quality=[0], # only one flag for three values |
| 603 | + units="CFS", |
| 604 | + data_type="INST-VAL", |
| 605 | + path="/A/B/C//6Hour/F/", |
| 606 | + ) |
| 607 | + mock_file = mock_open() |
| 608 | + with patch("builtins.open", mock_file): |
| 609 | + rts.to_csv("fake.csv", with_metadata=False) |
| 610 | + handle = mock_file() |
| 611 | + written = "".join(call.args[0] for call in handle.write.call_args_list) |
| 612 | + self.assertIn("1,01Sep2021 0600,1.0,0", written) |
| 613 | + self.assertNotIn("2,01Sep2021 1200", written) # silently dropped |
| 614 | + self.assertNotIn("3,01Sep2021 1800", written) # silently dropped |
| 615 | + |
| 616 | + def test_read_csv_skips_short_data_row(self): |
| 617 | + """A data row with fewer than 3 columns is malformed and skipped without |
| 618 | + raising (parity with the paired-data reader's short-row handling).""" |
| 619 | + content = ( |
| 620 | + "Type,Date/Time,INST-VAL\n" |
| 621 | + "1,01Sep2021 0600\n" # only 2 columns -> skipped |
| 622 | + "2,01Sep2021 1200,20.0\n" |
| 623 | + ) |
| 624 | + rts = self.read_rts_from_string(content) |
| 625 | + self.assertEqual(rts.values.tolist(), [20.0]) |
| 626 | + self.assertEqual(rts.times, [datetime(2021, 9, 1, 12, 0)]) |
| 627 | + |
| 628 | + def test_read_csv_metadata_only_no_data_rows(self): |
| 629 | + """Metadata rows with zero data rows yield an empty series (no crash); |
| 630 | + units and the E interval are still captured.""" |
| 631 | + content = ( |
| 632 | + "A,,,A\n" |
| 633 | + "E,,,6Hour\n" |
| 634 | + "Units,,,CFS\n" |
| 635 | + "Type,Date/Time,INST-VAL\n" |
| 636 | + ) |
| 637 | + rts = self.read_rts_from_string(content) |
| 638 | + self.assertEqual(rts.values.tolist(), []) |
| 639 | + self.assertEqual(rts.times, []) |
| 640 | + self.assertEqual(rts.units, "CFS") |
| 641 | + self.assertEqual(rts.interval, 21600) |
| 642 | + |
| 643 | + def test_round_trip_irregular(self): |
| 644 | + """Full write->read round trip for IrregularTimeSeries on a real temp file: |
| 645 | + irregular gaps, units, data_type and id all survive.""" |
| 646 | + path = self.test_files.create_test_file(".csv") |
| 647 | + its = IrregularTimeSeries.create( |
| 648 | + values=[10.5, 20.0, 42.0], |
| 649 | + times=[datetime(2021, 9, 1), datetime(2021, 9, 5), datetime(2021, 9, 20)], |
| 650 | + units="CFS", |
| 651 | + data_type="INST-VAL", |
| 652 | + path="/A/B/C//IR-Year/F/", |
| 653 | + ) |
| 654 | + its.to_csv(path, with_metadata=True) |
| 655 | + result = IrregularTimeSeries.read_csv(path) |
| 656 | + self.assertEqual(result.values.tolist(), [10.5, 20.0, 42.0]) |
| 657 | + self.assertEqual( |
| 658 | + result.times, |
| 659 | + [datetime(2021, 9, 1), datetime(2021, 9, 5), datetime(2021, 9, 20)], |
| 660 | + ) |
| 661 | + self.assertEqual(result.units, "CFS") |
| 662 | + self.assertEqual(result.data_type, "INST-VAL") |
| 663 | + self.assertEqual(result.id, "/A/B/C//IR-Year/F/") |
| 664 | + |
568 | 665 |
|
569 | 666 | if __name__ == "__main__": |
570 | 667 | unittest.main() |
0 commit comments