fix(forward): pair BW ACH ASCII reports using the _ASCII.TXT convention (v1.5.4)
Blastware's official Auto Call Home server writes per-event ASCII
reports as <stem>_<ext>_ASCII.TXT (e.g. N844L20G_630H_ASCII.TXT),
not <binary>.TXT (e.g. N844L20G.630H.TXT). Versions v1.5.0–v1.5.3
only looked for the latter and silently shipped every binary alone,
so the SFM database lost the per-event Peak Acceleration / Peak
Displacement / ZC Freq / Time of Peak / Peak Vector Sum + time /
sensor self-check fields on every forwarded event.
Fix: pair-finding logic now tries the ACH-convention filename first
and falls back to <binary>.TXT for compatibility with operator-saved
manual exports and existing test fixtures.
ach_report_name("M529LK44.AB0") → "M529LK44_AB0_ASCII.TXT"
legacy_report_name("M529LK44.AB0") → "M529LK44.AB0.TXT"
When both files exist (operator manually saved + ACH auto-exported),
ACH wins because that's the canonical name on modern BW deployments.
Both candidates checked case-insensitively against the cached
directory listing — no extra stat() calls.
6 new unit tests cover the new pairing logic, helper-function
correctness, and the precedence rule. Total now 31 tests, all green.
Field-deploy note: re-running v1.5.4 on a folder where v1.5.0–v1.5.3
already ran will NOT re-forward historical events — the
sfm_forwarded.json state file remembers them by sha256. To re-forward
historical events to populate SFM with the now-correctly-paired
reports, delete the state file before starting v1.5.4.
This commit is contained in:
@@ -50,6 +50,22 @@ class TestIsEventBinary(unittest.TestCase):
|
||||
"something.h5", "noise.json"]:
|
||||
self.assertFalse(ef.is_event_binary(name), name)
|
||||
|
||||
def test_ach_report_name(self):
|
||||
"""BW ACH convention: <stem>.<ext> → <stem>_<ext>_ASCII.TXT"""
|
||||
cases = [
|
||||
("M529LK44.AB0", "M529LK44_AB0_ASCII.TXT"),
|
||||
("N844L20G.630H", "N844L20G_630H_ASCII.TXT"),
|
||||
("I145L64P.GD0W", "I145L64P_GD0W_ASCII.TXT"),
|
||||
("H907L1R7.PG0H", "H907L1R7_PG0H_ASCII.TXT"),
|
||||
]
|
||||
for binary, expected in cases:
|
||||
self.assertEqual(ef.ach_report_name(binary), expected, binary)
|
||||
|
||||
def test_legacy_report_name(self):
|
||||
"""Manual-export convention: <binary>.TXT"""
|
||||
self.assertEqual(ef.legacy_report_name("M529LK44.AB0"),
|
||||
"M529LK44.AB0.TXT")
|
||||
|
||||
def test_is_histogram_event(self):
|
||||
# 4-char extension ending in H = histogram
|
||||
for name in ["H907L1R7.PG0H", "S353L4H0.8S0H", "P036L318.C80H"]:
|
||||
@@ -139,6 +155,86 @@ class TestFindPendingEvents(unittest.TestCase):
|
||||
self.assertEqual(os.path.basename(pending[0][0]), "M529LK44.AB0")
|
||||
self.assertEqual(os.path.basename(pending[0][1]), "M529LK44.AB0.TXT")
|
||||
|
||||
def test_pairs_with_ach_underscore_ascii_naming(self):
|
||||
"""BW ACH writes M529LK44.AB0 + M529LK44_AB0_ASCII.TXT. The
|
||||
watcher must pair these even though the .TXT filename doesn't
|
||||
carry a literal copy of the binary's name."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp_p = Path(tmp)
|
||||
self._make(tmp_p, "N844L20G.630H", age_seconds=120, content=b"binary")
|
||||
self._make(tmp_p, "N844L20G_630H_ASCII.TXT", age_seconds=100, content=b"report")
|
||||
|
||||
state = ef.ForwardState(str(tmp_p / "fwd.json"))
|
||||
pending = ef.find_pending_events(
|
||||
str(tmp_p), state,
|
||||
max_age_days=30,
|
||||
quiescence_seconds=5,
|
||||
missing_report_grace_seconds=60,
|
||||
)
|
||||
self.assertEqual(len(pending), 1)
|
||||
self.assertEqual(os.path.basename(pending[0][0]), "N844L20G.630H")
|
||||
self.assertEqual(os.path.basename(pending[0][1]),
|
||||
"N844L20G_630H_ASCII.TXT")
|
||||
|
||||
def test_pairs_with_ach_underscore_ascii_naming_for_waveform(self):
|
||||
"""Same as above but for new-firmware waveform events
|
||||
(extension ends in W)."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp_p = Path(tmp)
|
||||
self._make(tmp_p, "I145L64P.GD0W", age_seconds=120, content=b"binary")
|
||||
self._make(tmp_p, "I145L64P_GD0W_ASCII.TXT", age_seconds=100, content=b"report")
|
||||
|
||||
state = ef.ForwardState(str(tmp_p / "fwd.json"))
|
||||
pending = ef.find_pending_events(
|
||||
str(tmp_p), state,
|
||||
max_age_days=30,
|
||||
quiescence_seconds=5,
|
||||
missing_report_grace_seconds=60,
|
||||
)
|
||||
self.assertEqual(len(pending), 1)
|
||||
self.assertEqual(os.path.basename(pending[0][1]),
|
||||
"I145L64P_GD0W_ASCII.TXT")
|
||||
|
||||
def test_pairing_prefers_ach_naming_when_both_exist(self):
|
||||
"""If a folder has BOTH conventions (operator manually exported
|
||||
AND ACH also auto-exported), ACH wins because that's the
|
||||
canonical name in modern BW deployments."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp_p = Path(tmp)
|
||||
self._make(tmp_p, "M529LK44.AB0", age_seconds=120, content=b"binary")
|
||||
# Both partner files present
|
||||
self._make(tmp_p, "M529LK44.AB0.TXT", age_seconds=100, content=b"manual")
|
||||
self._make(tmp_p, "M529LK44_AB0_ASCII.TXT", age_seconds=100, content=b"ach")
|
||||
|
||||
state = ef.ForwardState(str(tmp_p / "fwd.json"))
|
||||
pending = ef.find_pending_events(
|
||||
str(tmp_p), state,
|
||||
max_age_days=30,
|
||||
quiescence_seconds=5,
|
||||
missing_report_grace_seconds=60,
|
||||
)
|
||||
self.assertEqual(len(pending), 1)
|
||||
self.assertEqual(os.path.basename(pending[0][1]),
|
||||
"M529LK44_AB0_ASCII.TXT")
|
||||
|
||||
def test_pairing_falls_back_to_dot_txt_when_ach_absent(self):
|
||||
"""If only the manual-export filename exists, the legacy
|
||||
convention still works (preserves codec-agent test fixtures)."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp_p = Path(tmp)
|
||||
self._make(tmp_p, "M529LK44.AB0", age_seconds=120, content=b"binary")
|
||||
self._make(tmp_p, "M529LK44.AB0.TXT", age_seconds=100, content=b"manual")
|
||||
|
||||
state = ef.ForwardState(str(tmp_p / "fwd.json"))
|
||||
pending = ef.find_pending_events(
|
||||
str(tmp_p), state,
|
||||
max_age_days=30,
|
||||
quiescence_seconds=5,
|
||||
missing_report_grace_seconds=60,
|
||||
)
|
||||
self.assertEqual(len(pending), 1)
|
||||
self.assertEqual(os.path.basename(pending[0][1]), "M529LK44.AB0.TXT")
|
||||
|
||||
def test_skips_if_already_forwarded(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
tmp_p = Path(tmp)
|
||||
|
||||
Reference in New Issue
Block a user