From 13775116677cb6765160803e987e719f8caee0fa Mon Sep 17 00:00:00 2001 From: saesaemlee Date: Sun, 21 Jun 2026 15:58:20 +0900 Subject: [PATCH] fix: TEST_NUM_from_record reads full 4-byte U*4 (closes #76) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `TEST_NUM` is a U*4 field at byte offset 4..7 (inclusive) in PTR/MPR/FTR records, so the slice into `struct.unpack` must be `record[4:8]`, not `record[4:7]`. The 3-byte slice raised `struct.error: unpack requires a buffer of 4 bytes` on every call, making the function effectively dead on arrival. Also: - wrap the unpack result with `int(...)[0]` so the success path matches the sentinel return value (`-1`, an int) and callers don't have to branch on tuple vs int - fix the docstring offset table (4:7 → 4:8) - add three parametric pytest cases: - round-trip with TEST_NUM = 0x12345678 (> 2**24, exposes any MSB-byte truncation) under both little- and big-endian - sentinel path returns int(-1), not tuple, for non-test records - all three test record subtypes (PTR 15/10, MPR 15/15, FTR 15/20) read TEST_NUM correctly Full suite: 31 passed, 4 skipped. Co-Authored-By: Claude Opus 4.7 --- Semi_ATE/STDF/utils.py | 11 ++++++----- tests/test_utils.py | 42 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/Semi_ATE/STDF/utils.py b/Semi_ATE/STDF/utils.py index fcee5da..843ad25 100644 --- a/Semi_ATE/STDF/utils.py +++ b/Semi_ATE/STDF/utils.py @@ -441,19 +441,20 @@ def HEAD_NUM_and_SITE_NUM_from_record(record): def TEST_NUM_from_record(record, endian): ''' given a PTR, MPR or FTR record, extract the test Number. - Note: TEST_NUM is for these records always located on offset 4..7 + Note: TEST_NUM is a U*4 (unsigned 4-byte) field located at offset 4..7 + (inclusive), so the slice is record[4:8]. REC_TYP REC_SUB TEST_NUM (V4) - PTR 15 10 4:7 - MPR 15 15 4:7 - FTR 15 20 4:7 + PTR 15 10 4:8 + MPR 15 15 4:8 + FTR 15 20 4:8 Also note that endian is important here! ''' REC_TYP, REC_SUB = TS_from_record(record) TEST_NUM = -1 if REC_TYP == 15 and REC_SUB in [10, 15, 20]: - TEST_NUM = struct.unpack("%sI" % endian, record[4:7]) + TEST_NUM = int(struct.unpack("%sI" % endian, record[4:8])[0]) return TEST_NUM compression_extensions = ['.gz', '.7z', '.zip', '.xz', '.bz2'] diff --git a/tests/test_utils.py b/tests/test_utils.py index 82d1095..70c6b7f 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -6,13 +6,18 @@ @author: seimit """ import os +import struct import tempfile import gzip import bz2 import lzma import zipfile import time + +import pytest + from Semi_ATE import STDF +from Semi_ATE.STDF.utils import TEST_NUM_from_record def test_records_from_file(): @@ -203,4 +208,39 @@ def test_dict_to_rec(): assert rec.get_value('HEAD_NUM') == head_num assert rec.get_value('SITE_GRP') == site_grp assert rec.get_value('START_T') == start_t - assert rec.get_value('WAFER_ID') == waf_id \ No newline at end of file + assert rec.get_value('WAFER_ID') == waf_id + +def _make_ptr_header(endian: str, test_num: int) -> bytes: + """Build the first 8 bytes of a PTR record: REC_LEN, REC_TYP=15, REC_SUB=10, TEST_NUM.""" + return struct.pack(f"{endian}HBBI", 4, 15, 10, test_num) + + +@pytest.mark.parametrize("endian", ["<", ">"]) +def test_TEST_NUM_from_record_roundtrip(endian): + """A PTR with TEST_NUM > 2**24 round-trips correctly when all 4 bytes are read. + + Regression test for #76: previously the slice was record[4:7] (3 bytes), + which would raise struct.error and made the function unusable for any + PTR/MPR/FTR record. The fix is record[4:8] (4 bytes, matching U*4). + """ + expected = 0x12345678 # > 2**24, exposes any MSB-byte truncation + record = _make_ptr_header(endian, expected) + assert TEST_NUM_from_record(record, endian) == expected + + +def test_TEST_NUM_from_record_returns_int_for_non_test_record(): + """Non-PTR/MPR/FTR records get a sentinel int (-1), not a tuple — the + returned value must always be int for downstream comparisons to work.""" + # FAR record: REC_TYP=0, REC_SUB=10 + record = struct.pack("