Files
ecw-charge-validation/tests/test_spreadsheet_write.py
ManikandanandClaude Opus 5.5 ba15a574ab Initial import: eCW Charge Validation bot and results Portal
Snapshot of the working project as of 2026-10-23 (local 2d4d4fc), one
commit with no earlier history: the tracked files were de-identified
first (synthetic claim ids and patient data only; see Context.md
section 7). Credentials, run logs, screenshots, claim reports and client
workbooks are gitignored and not included.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 18:38:17 +05:30

340 lines
12 KiB
Python

"""
write_xlsx(): the write side of core/spreadsheet.py.
python tests\\test_spreadsheet_write.py
Round-trips through the existing `read_rows()` reader rather than asserting
on raw XML — what matters is that a file this writes is a file this project's
own reader (and real Excel) can open, not the exact bytes.
Also write_xlsx_keep_old(), the one writer both dated outputs go through
(review item D10): a fresh path, an existing file kept aside, and an existing
file that is OPEN — held here by a plain `open(path, "rb")`, which on Windows
refuses the rename with WinError 32 exactly as Excel does — whose run must
land under a `__HHMMSS` name instead of being lost. Every case works in its
own temp folder.
"""
import io
import re
import sys
import pathlib
import tempfile
import contextlib
from datetime import datetime
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parent.parent / "src"))
from ecw_bot.core import spreadsheet # noqa: E402
TMP = pathlib.Path(tempfile.mkdtemp())
def case_write_then_read_round_trips():
rows = [
["CLAIMS#", "PAYER", "Group Name"],
["9107000", "Medicaid", "Medicaid"],
["9106000", "DentaQuest", "Dentaquest"],
]
path = TMP / "roundtrip.xlsx"
spreadsheet.write_xlsx(path, rows)
back = spreadsheet.read_rows(path)
assert back == rows, back
def case_special_characters_are_escaped():
rows = [["PATIENT", "NOTE"],
["Smith & Sons", "A < B > C"],
["O'Brien", '"quoted"']]
path = TMP / "escaped.xlsx"
spreadsheet.write_xlsx(path, rows)
back = spreadsheet.read_rows(path)
assert back == rows, back
def case_wide_row_uses_correct_column_letters():
"""27 columns exercises the single-letter -> AA rollover."""
header = [f"col{i}" for i in range(27)]
data = [str(i) for i in range(27)]
path = TMP / "wide.xlsx"
spreadsheet.write_xlsx(path, [header, data])
back = spreadsheet.read_rows(path)
assert back[1][26] == "26", back[1] # column AA (index 26)
assert back[1] == data, back[1]
def case_control_characters_are_stripped_not_written():
rows = [["A"], ["value\x00with\x01control\x1fchars"]]
path = TMP / "control_chars.xlsx"
spreadsheet.write_xlsx(path, rows)
back = spreadsheet.read_rows(path)
assert back == [["A"], ["valuewithcontrolchars"]], back
def case_creates_parent_directory():
path = TMP / "nested" / "new_dir" / "file.xlsx"
assert not path.parent.exists()
spreadsheet.write_xlsx(path, [["A"], ["1"]])
assert path.exists()
def case_non_string_values_are_stringified():
rows = [["N"], [5], [3.5]]
path = TMP / "numbers.xlsx"
spreadsheet.write_xlsx(path, rows)
back = spreadsheet.read_rows(path)
assert back == [["N"], ["5"], ["3.5"]], back
# ── write_xlsx_keep_old ──────────────────────────────────────────────────────
OLD = [["CLAIMS#", "Group Name"], ["9107000", "hand-filled by the operator"]]
NEW = [["CLAIMS#", "Group Name"], ["9107000", "Medicaid"], ["9106000", ""]]
def _folder_with_old_file(name="Medcor Ready_10-05-2026.xlsx"):
"""A fresh temp folder holding a same-day file with OLD rows in it."""
folder = pathlib.Path(tempfile.mkdtemp(dir=TMP))
out = folder / name
spreadsheet.write_xlsx(out, OLD)
return folder, out
def _keep_old(out, rows=NEW):
"""Run the helper, returning (path it returned, what it printed)."""
printed = io.StringIO()
with contextlib.redirect_stdout(printed):
got = spreadsheet.write_xlsx_keep_old(out, rows, "[TAG]")
return got, printed.getvalue()
@contextlib.contextmanager
def _held_open(path):
"""
Hold `path` open the way Excel does for the duration. A Python handle
blocks the RENAME (WinError 32) but not a write over it — so the cases
below also prove the old file was never written over. The rename is
probed first: a platform that does not lock an open file cannot
exercise this path, and should fail loudly rather than pass vacuously.
"""
with open(path, "rb") as handle:
probe = path.with_name("probe-rename.xlsx")
try:
path.rename(probe)
except OSError:
pass
else:
probe.rename(path)
raise AssertionError("an open file is not locked against rename "
"here — cannot exercise the locked path")
yield handle
class _FixedClock:
"""Stands in for spreadsheet.datetime: now() is always 12:34:56."""
@staticmethod
def now():
return datetime(2026, 10, 5, 12, 34, 56)
def case_keep_old_fresh_path_is_written_as_is():
folder = pathlib.Path(tempfile.mkdtemp(dir=TMP))
out = folder / "Charge Summary_10-05-2026.xlsx"
got, printed = _keep_old(out)
assert got == out, got
assert spreadsheet.read_rows(out) == NEW
assert [p.name for p in folder.iterdir()] == [out.name], \
list(folder.iterdir())
assert printed == "", printed
def case_keep_old_existing_file_is_kept_aside_and_new_rows_take_its_name():
folder, out = _folder_with_old_file()
old_bytes = out.read_bytes()
got, printed = _keep_old(out)
assert got == out, got
assert spreadsheet.read_rows(out) == NEW
kept = list(folder.glob("Medcor Ready_10-05-2026__*.xlsx"))
assert len(kept) == 1, kept
assert re.fullmatch(r"Medcor Ready_10-05-2026__\d{6}\.xlsx",
kept[0].name), kept[0].name
assert kept[0].read_bytes() == old_bytes, "the kept file was changed"
assert printed.strip() == (f"[TAG] {out.name} already existed — kept as "
f"{kept[0].name}"), printed
def case_keep_old_open_file_is_left_alone_and_new_rows_get_a_free_name():
folder, out = _folder_with_old_file()
old_bytes = out.read_bytes()
with _held_open(out):
got, printed = _keep_old(out) # must not raise
assert out.exists(), "the open file was moved"
assert got != out, got
assert got.parent == folder, got
assert re.fullmatch(r"Medcor Ready_10-05-2026__\d{6}\.xlsx",
got.name), got.name
assert spreadsheet.read_rows(got) == NEW, "this run's rows were lost"
assert out.read_bytes() == old_bytes, "the open file was changed"
assert sorted(p.name for p in folder.iterdir()) == \
sorted([out.name, got.name]), list(folder.iterdir())
assert printed.startswith(f"[TAG] {out.name} is open or locked ("), \
printed
assert "WinError 32" in printed, printed
assert printed.rstrip().endswith(f"— saved this run as {got.name} "
f"instead"), printed
def case_keep_old_names_skip_ones_already_taken_in_the_same_second():
folder, out = _folder_with_old_file()
taken = folder / "Medcor Ready_10-05-2026__123456.xlsx"
taken.write_bytes(b"an earlier run, same second")
orig = spreadsheet.datetime
spreadsheet.datetime = _FixedClock
try:
got, printed = _keep_old(out)
assert got == out, got
assert "kept as Medcor Ready_10-05-2026__123456_2.xlsx" in printed, \
printed
with _held_open(out):
got, printed = _keep_old(out)
assert got.name == "Medcor Ready_10-05-2026__123456_3.xlsx", got
finally:
spreadsheet.datetime = orig
assert taken.read_bytes() == b"an earlier run, same second"
def case_keep_old_failed_write_after_the_rename_puts_the_old_file_back():
"""
The rename succeeds but writing the new file in its place fails (disk
full, a sync client grabbing the name): the old file goes back under its
own name — the failed write's half-file removed first — and the rows go
to a free `__HHMMSS` name instead.
"""
folder, out = _folder_with_old_file()
old_bytes = out.read_bytes()
real_write = spreadsheet.write_xlsx
def _fails_on_the_usual_name(path, rows):
if pathlib.Path(path) == out:
pathlib.Path(path).write_bytes(b"PK half a zip")
raise OSError(28, "No space left on device")
real_write(path, rows)
spreadsheet.write_xlsx = _fails_on_the_usual_name
try:
got, printed = _keep_old(out)
finally:
spreadsheet.write_xlsx = real_write
assert out.read_bytes() == old_bytes, "the old file was not put back"
assert got != out and spreadsheet.read_rows(got) == NEW, got
assert sorted(p.name for p in folder.iterdir()) == \
sorted([out.name, got.name]), list(folder.iterdir())
assert "No space left on device" in printed, printed
def case_keep_old_failed_move_back_says_the_usual_name_may_be_incomplete():
"""
Fix round 1 (2026-10-05): the write to the usual name fails AND its
half-file cannot be removed (a virus scanner holding it — here a real
open handle, so the unlink is refused with WinError 32). The old file
cannot go back, so the log must say the usual name may be incomplete
and where the operator's earlier sheet really is.
"""
folder, out = _folder_with_old_file()
old_bytes = out.read_bytes()
real_write = spreadsheet.write_xlsx
handles = []
def _fails_and_stays_held(path, rows):
if pathlib.Path(path) == out:
pathlib.Path(path).write_bytes(b"PK half a zip")
handles.append(open(path, "rb")) # the scanner's grip
raise OSError(28, "No space left on device")
real_write(path, rows)
spreadsheet.write_xlsx = _fails_and_stays_held
try:
got, printed = _keep_old(out)
finally:
spreadsheet.write_xlsx = real_write
for h in handles:
h.close()
backups = [p for p in folder.iterdir()
if p not in (out, got)]
assert len(backups) == 1, list(folder.iterdir())
backup = backups[0]
assert backup.read_bytes() == old_bytes, "the earlier sheet changed"
assert got != out and spreadsheet.read_rows(got) == NEW, got
want = (f"[TAG] {out.name} may be an incomplete file — your earlier "
f"sheet is {backup.name} (could not move it back: ")
assert want in printed, printed
assert "already existed — kept as" not in printed, printed
def case_keep_old_a_bug_while_writing_puts_the_old_file_back_and_raises():
folder, out = _folder_with_old_file()
old_bytes = out.read_bytes()
real_write = spreadsheet.write_xlsx
def _bug(path, rows):
raise ValueError("not a file-system problem")
spreadsheet.write_xlsx = _bug
try:
_keep_old(out)
except ValueError:
pass
else:
raise AssertionError("a non-OSError must propagate, not be retried")
finally:
spreadsheet.write_xlsx = real_write
assert out.read_bytes() == old_bytes
assert [p.name for p in folder.iterdir()] == [out.name], \
list(folder.iterdir())
def case_keep_old_fallback_write_failing_too_propagates():
"""Nowhere left to put the rows: the caller's own except reports it."""
folder, out = _folder_with_old_file()
old_bytes = out.read_bytes()
real_write = spreadsheet.write_xlsx
def _disk_full(path, rows):
raise OSError(28, "No space left on device")
spreadsheet.write_xlsx = _disk_full
try:
with _held_open(out):
_keep_old(out)
except OSError as exc:
assert "No space left" in str(exc), exc
else:
raise AssertionError("the fallback's own failure must propagate")
finally:
spreadsheet.write_xlsx = real_write
assert out.read_bytes() == old_bytes
CASES = [v for k, v in sorted(globals().items()) if k.startswith("case_")]
def main() -> int:
failures = 0
for fn in CASES:
name = fn.__name__[5:].replace("_", " ")
try:
fn()
print(f" ok {name}")
except AssertionError as exc:
print(f" FAIL {name}: {exc}")
failures += 1
except Exception as exc:
print(f" ERROR {name}: {type(exc).__name__}: {exc}")
failures += 1
print(f"\n{len(CASES) - failures}/{len(CASES)} passed")
return 1 if failures else 0
if __name__ == "__main__":
sys.exit(main())