From 66360a7014a0bad45b9db017756f063337a43961 Mon Sep 17 00:00:00 2001 From: Nicholas Karlson Date: Mon, 19 Jan 2026 10:33:10 -0800 Subject: [PATCH 1/2] Test: make Track D template ZIP drift guard cross-platform --- tests/test_workbook_track_d_zip_is_current.py | 63 +++++++++++++++++-- 1 file changed, 58 insertions(+), 5 deletions(-) diff --git a/tests/test_workbook_track_d_zip_is_current.py b/tests/test_workbook_track_d_zip_is_current.py index a912fd9..4248aac 100644 --- a/tests/test_workbook_track_d_zip_is_current.py +++ b/tests/test_workbook_track_d_zip_is_current.py @@ -8,6 +8,39 @@ from zipfile import ZipFile +# NOTE: This guardrail is intended to prevent *template drift* (files added/removed/edited) +# rather than enforce an OS-specific newline convention. Some Git checkouts (and some tools) +# may produce CRLF vs LF differences for text files, which should not fail CI. +_TEXT_NAMES = {"Makefile", "makefile"} +_TEXT_EXTS = { + ".cfg", + ".csv", + ".ini", + ".json", + ".md", + ".py", + ".rst", + ".toml", + ".tsv", + ".txt", + ".yaml", + ".yml", +} + + +def _normalized_member_bytes(name: str, data: bytes) -> bytes: + """Normalize member bytes for cross-platform comparison. + + We only normalize *text-like* files by converting CRLF -> LF (and bare CR -> LF). + Binary files are hashed as-is. + """ + base = name.rsplit("/", 1)[-1] + ext = (base.rsplit(".", 1)[-1] if "." in base else "").lower() + if base in _TEXT_NAMES or (ext and f".{ext}" in _TEXT_EXTS): + return data.replace(b"\r\n", b"\n").replace(b"\r", b"\n") + return data + + def _zip_payload_hashes(zip_path: Path) -> dict[str, str]: """Return sha256 hashes of *decompressed* member bytes. @@ -16,7 +49,7 @@ def _zip_payload_hashes(zip_path: Path) -> dict[str, str]: out: dict[str, str] = {} with ZipFile(zip_path, "r") as zf: for name in sorted(n for n in zf.namelist() if not n.endswith("/")): - data = zf.read(name) + data = _normalized_member_bytes(name, zf.read(name)) out[name] = hashlib.sha256(data).hexdigest() return out @@ -52,7 +85,27 @@ def test_workbook_track_d_zip_is_current() -> None: built = _zip_payload_hashes(built_zip) committed = _zip_payload_hashes(committed_zip) - assert built == committed, ( - "Committed Track D workbook ZIP is stale or mismatched vs template source-of-truth.\n\n" - "Fix: run `python tools/build_workbook_zip.py` and commit the updated ZIP." - ) \ No newline at end of file + if built != committed: + built_names = set(built) + committed_names = set(committed) + only_in_built = sorted(built_names - committed_names) + only_in_committed = sorted(committed_names - built_names) + changed = sorted(n for n in built_names & committed_names if built[n] != committed[n]) + + lines: list[str] = [ + "Committed Track D workbook ZIP is stale or mismatched vs template source-of-truth.", + "", + ] + if only_in_built: + lines += ["Only in rebuilt ZIP:"] + [f" - {n}" for n in only_in_built] + [""] + if only_in_committed: + lines += ["Only in committed ZIP:"] + [f" - {n}" for n in only_in_committed] + [""] + if changed: + lines += [ + "Same path, different content (after newline-normalization for text files):", + *[f" - {n}" for n in changed], + "", + ] + lines += ["Fix: run `python tools/build_workbook_zip.py` and commit the updated ZIP."] + + raise AssertionError("\n".join(lines)) \ No newline at end of file From e3e2f5b5499655962dd8335b87b7aebe03ad3a93 Mon Sep 17 00:00:00 2001 From: Nicholas Karlson Date: Mon, 19 Jan 2026 10:55:05 -0800 Subject: [PATCH 2/2] Chore: rebuild Track D workbook ZIP from template source --- src/pystatsv1/assets/workbook_track_d.zip | Bin 167899 -> 166929 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/src/pystatsv1/assets/workbook_track_d.zip b/src/pystatsv1/assets/workbook_track_d.zip index 48cb9193bf50bfac0630f8854f2f5b73c9adf053..fc1a5ea3c49d159956c460e29ae814145ed13c6e 100644 GIT binary patch delta 3640 zcmZuz2~ZSQ5dG7#fXbn;z{<^rMMULV&Um6=sfgl*@h;?0JmMt=@UA7`U_55879!ucY~lpa%5z8hP*SeL z-!xS1c&0x^Gu40fr6|?^p^BpT;QdT`R~HC0<``-iKv7A8hciXHQs%h~wAdKmzN|T7 z03ms?ghU!|S9tMZ8^O0#KP!=G4B2y>sOGFeT(b&J|-=gw+&j-<1$(t*#0y58aRnb6Ul`Gn(u6poFT4+(my1=Pc#g zX#%sRoIC4HqYpmiuCnN<{0gJ3#?&01K_;J{#*&!9mEX$+nS zvF6o=LKcf|83Nf1J_&4Tt#EZX9Ar^lB(yMiB?=lCT%m_@276>m)(2xj^-06}mN`vMw3Wu(TM_hmM|2gJg7lFpk#1X;|Dh7YaPYuPwPw|9b zR1AUqkHz2|B9h{LK)~{Wmp}ayK26*P<7)dscpEqlNKfTJEzl9Zg5622lw80Gbf&#Ga6~Icq-1Tbr{g z=lz2G@PhRPGHw%cvFm^AjhSjS-?;($4;H!1gfV3uSbD#f5O0ZtQ*J2mmA4gMD&%YT z74j`A1($45zKhRKrftL}j>4{7CHX8B^0U_F84B_v7H6%@3pW(ndd-}A|9&ruU!mAj zBF+frJ4?Y&?EKa*6K;*bF|am<5FZJ-D1=v+fmX7?No9iTo;V{n%hTAD;3IFWky0Fz zEfl`9Oj=GdbtL6-W}qN?m~Ast9=5bOI1#d7Fd=ag!JQ>Sg_?~pSlm#@aW3f~PPn%5 zgd|D?qgD&9|1jv}u7k>j#wL^tlou7jG9wFJ|9pNGSJZOC#z^wMw^?n!gOp{QhXpf()%9)50Fw%<&)>3D?rWHSAkA);@T-;fj=YkRDm{B zbf<(APHx|eD|~b!A>me+gqS${C?=V*2?E8E?46F+3vm8CTSrKeMDlC`lkD3Bda=Yi zvo3T#t_I{1f`zP?iTfdkNs5+ou?qcd7W%6#W32kk5GU#H0wv7-gc06shG+?4WVMhX z{tzRqs)h)$wevH2vcJd8FP=-tD628c0-V3ZB&A(k*l^n$%WK@}(@$_bJe_WPpFHkn zy3cyJ5ScFH%L6yZ;ww#!hLB)MJ-r?9ohs1D&Sz`#qpyBa5t8ac2&$}?i977XKd6BK zNy7uT2CMswzG+Zvxq1N(LlJ9={%=Hyn9-36{#;NPA1ja0RR910 delta 4649 zcmZ{n3se->8OQHj7DSO{S(i61unG|bT%IB(rx00{aEO3;;UgX%t0F2JSW`e`Gekro zu~L%UGZ+2o|hCTPGc$q_ZDJ)SgL^JuD3Qmr=jSYs_|jX6Fr{qEeEWnRuv_?Y?s zzQ?`ad^6Mk>a?-nOfy!L1sE&{pj|gQ}M9B(cI6b#$bp4z2ps0pCgzN(~eAE!BdR&x`*FHI&$;+Ao&CGg?HwH3m z?T#D+v-bApMKbI38%Y*s-5K^oF{|^WWO3jA7{kupoj4n2h=J|&oekw)G%<67m3!OF zOiLDbEkcm`3`@Artjs*)V+(L@YbI?ltb5c-Aq@{W6iA9s7!ljb`SpYg|c` zuoS*x1MUoFXa6uA2Lv(mQ#0OYFm%Cc_*_x=5ssNhqVX!xw8rAqqUnjlF46p7JZ=+B zmldxN%~J`uQZ#2J;SHkMZ^J7^b9@#q7tP{izu$c+xK2Dfbq=l<&1X{a9MRmHhLc6} z_*}pFpLG1bc=oMKd_Xj-?Rd9newT%L(RAftxK)KRZ{^}1(cGSo*Rz)KjnBtdXRw7j zUxwdIOH(&y5HzOS*`m{yzV2u7RW2l8axRcHZ8+NFQ>R4d3p-3`qSyg{3e!eJYT;5aQZsbi zu!AxiBxZ8>$OEcx=qQPW-(LP#f&K9@WUv(nJ|Nbm4rcZ1X|T@&$yh5+G^rzyqpdjF z;N^`$^m@kqoJA^}Jr+df!6K)tol%iAe<#D!bbAFN*>j&<>EK}p&Gi%5ZfPXSL#QjH zBXN=%mr>Gyl+$!Q7gM>EA)S!Y;pG#d6{MgN4)fC8=ybZ&Zpca2Vow3n?0jZRL$h;} zbF;f8)7|QpYt;!W%dCVyrD-dtXriWHnA%?nK`7jYkTTCUi5j?K4kd-{I7HF>745=y zIxGVh&GDKIdc8cwJOpi51N#{#)egVC{6kg%ECc_JpQ>mPI@`%b@R{u8Q)f@+yQXeq zL#C@uoi?#<6~doRCWTw^OsSh_MHkL2zOWE>tT|U5`t}2)b1SwKG_B{kl*0yuN}%Kd z1$4Yi$b57gPE{oG{Wc+yuX_Zay*uVAc1_6j=}tis_JZJHPbbb-c=&={Ub;{4aQls! zB+;X)*6tBJZ2ABs5d+pkk($K+_2s3{(_k>2E=6deBJuG*Q;3XsaE2o6IP=+ZS@1dg zTkQ(o{QK9-+F??9VdEVNpPPRYpoDwbibUw+{CQL9qeakN7jh7SfBwVEmxYnv@;H)A zgmMWQ?8U_!t5=y3`T>%+`yqNie3QVi-BwO{8_{SfZI6t4BM|y&5UC||oO4l*b9VeHo$d+zt`$y03+7t_`y#`uE5q28Su)`kC zuEG9!e8b3dF$h({`(cp+JNr7tB1rFRT!sew@5*-$E=)wo)QQkT3e0ziVe5yvG;QC7 zZx<>lKg~qnJ~c^v3c$h_Xl%Wlx-( z&Cy{EHgO25QDgh3LHz8$tPOCg{o(@Z{?-0tnlpt5; z>nQCb*K<=LN{lAOq9i#xUx)2qbotD~z&@>EU$!aAE6{Fzl+QHE0FtX_}2a`8yh;K9Zx=>TcE5%#$WWB0 z*ES(t1si(-T&8KxnoN0My|y7ob_wN0+2u;T9$7i#@!_=)<*;TEPh`niy&fqWYY49f z_MnDcog*(VTd!?wyH7=}2Kd(+_)@McaJF9CkoS6p@*4ByO1&Pb9!%L=1W{IKqTHM> zXVpDIXK>YL%ibEC0nG`Ink4_|qtC|L1;CP*`mlvm_u+79yi({`XyuH?G=y#*MM(Jt zP*G0$`*8Bl?(+pezwp>WzW{#xITpY7G;f$6_X&@enXTk+eK^Hfib~NTcodu3p+xk5 D_Ydmt