"""Phase 9: 横断系测试(轻量版 ~20 测试)。 覆盖四大领域: - VL: 可变长 / ODO 逻辑 - LP: 循环 / PERFORM VARYING / UNTIL 逻辑 - NP: 数值精度 / COMP-3 / ROUNDED 逻辑 - D: 日期 / 闰年 / 月末 / 和历逻辑 """ from __future__ import annotations import math from datetime import date from typing import Any # ════════════════════════════════════════════════════════════ # VL: 可变长 / ODO 逻辑 # ════════════════════════════════════════════════════════════ def _odo_offset(depending_on: int, base_size: int, item_size: int) -> int: """模拟 COBOL OCCURS DEPENDING ON: 总长 = 固定部 + 可变项数 * 每项大小 """ if depending_on < 0: depending_on = 0 if depending_on > 999: depending_on = 999 return base_size + depending_on * item_size def _odo_read(table: list, start: int, count: int) -> list: """模拟 ODO 读取指定数量的可变元素。""" return table[start:start + count] class TestODO: """可变长 / ODO 逻辑 (5 tests)""" def test_odo_basic_length(self): length = _odo_offset(5, 10, 4) assert length == 10 + 5 * 4 def test_odo_zero_items(self): assert _odo_offset(0, 10, 4) == 10 def test_odo_negative_depending(self): assert _odo_offset(-1, 10, 4) == 10 def test_odo_read_partial(self): table = [10, 20, 30, 40, 50] assert _odo_read(table, 1, 3) == [20, 30, 40] def test_odo_read_beyond_end(self): table = [10, 20, 30] assert _odo_read(table, 1, 10) == [20, 30] # ════════════════════════════════════════════════════════════ # LP: 循环 / PERFORM VARYING / UNTIL 逻辑 # ════════════════════════════════════════════════════════════ def _perform_varying(start: int, end: int, step: int = 1) -> list[int]: """模拟 COBOL PERFORM VARYING: 返回每次循环的索引值。""" results: list[int] = [] i = start if step > 0: while i <= end: results.append(i) i += step elif step < 0: while i >= end: results.append(i) i += step return results def _perform_until(initial: int, condition_func, body_func, max_iter: int = 1000) -> list: """模拟 COBOL PERFORM UNTIL condition。""" results: list = [] i = initial count = 0 while not condition_func(i) and count < max_iter: val = body_func(i) results.append(val) i = val count += 1 return results class TestPerformVarying: """PERFORM VARYING 逻辑 (3 tests)""" def test_varying_ascending(self): assert _perform_varying(1, 5) == [1, 2, 3, 4, 5] def test_varying_step_2(self): assert _perform_varying(1, 10, 2) == [1, 3, 5, 7, 9] def test_varying_descending(self): assert _perform_varying(5, 1, -1) == [5, 4, 3, 2, 1] class TestPerformUntil: """PERFORM UNTIL 逻辑 (2 tests)""" def test_until_reaches_target(self): result = _perform_until(1, lambda x: x >= 10, lambda x: x + 1) assert result == [2, 3, 4, 5, 6, 7, 8, 9, 10] def test_until_condition_immediately_true(self): result = _perform_until(10, lambda x: x >= 10, lambda x: x + 1) assert result == [] # ════════════════════════════════════════════════════════════ # NP: 数值精度 / COMP-3 / ROUNDED 逻辑 # ════════════════════════════════════════════════════════════ def _comp3_to_value(bytes_data: bytes) -> int: """模拟 COMP-3 (BCD) 到整数的转换。""" if not bytes_data: return 0 last = bytes_data[-1] sign_nibble = last & 0x0F value_nibbles: list[int] = [] for b in bytes_data[:-1]: value_nibbles.append((b >> 4) & 0x0F) value_nibbles.append(b & 0x0F) value_nibbles.append((last >> 4) & 0x0F) value = 0 for nib in value_nibbles: value = value * 10 + nib if sign_nibble in (0x0D,): value = -value return value def _rounded(value: float, decimals: int) -> float: """模拟 COBOL ROUNDED 子句。""" factor = 10 ** decimals return math.floor(value * factor + 0.5) / factor class TestComp3: """COMP-3 数值精度 (3 tests)""" def test_comp3_positive(self): # BCD: 0x12 0x3C -> 123 assert _comp3_to_value(bytes([0x12, 0x3C])) == 123 def test_comp3_negative(self): # BCD: 0x45 0x6D -> -456 assert _comp3_to_value(bytes([0x45, 0x6D])) == -456 def test_comp3_zero(self): assert _comp3_to_value(bytes([0x0C])) == 0 class TestRounded: """ROUNDED 子句 (2 tests)""" def test_rounded_up(self): assert _rounded(1.235, 2) == 1.24 def test_rounded_down(self): assert _rounded(1.234, 2) == 1.23 # ════════════════════════════════════════════════════════════ # D: 日期 / 闰年 / 月末 / 和历逻辑 # ════════════════════════════════════════════════════════════ def _is_leap_year(year: int) -> bool: return year % 400 == 0 or (year % 100 != 0 and year % 4 == 0) def _days_in_month(year: int, month: int) -> int: if month == 2: return 29 if _is_leap_year(year) else 28 long_months = {1, 3, 5, 7, 8, 10, 12} return 31 if month in long_months else 30 def _month_end_date(year: int, month: int) -> date: return date(year, month, _days_in_month(year, month)) def _wareki_to_year(wareki_prefix: str, wareki_year: int) -> int: era_map = { "R": (2019, "令和"), "H": (1989, "平成"), "S": (1926, "昭和"), "T": (1912, "大正"), "M": (1868, "明治"), } if wareki_prefix not in era_map: raise ValueError(f"未知和历: {wareki_prefix!r}") return era_map[wareki_prefix][0] + wareki_year - 1 class TestLeapYear: """闰年判断 (2 tests)""" def test_leap_year_divisible_by_400(self): assert _is_leap_year(2000) is True assert _is_leap_year(2400) is True def test_leap_year_divisible_by_4_not_100(self): assert _is_leap_year(2024) is True assert _is_leap_year(2028) is True class TestMonthEnd: """月末日期 (2 tests)""" def test_february_leap_year(self): assert _days_in_month(2024, 2) == 29 assert _month_end_date(2024, 2) == date(2024, 2, 29) def test_february_non_leap(self): assert _days_in_month(2023, 2) == 28 assert _month_end_date(2023, 2) == date(2023, 2, 28) class TestWareki: """和历逻辑 (1 test)""" def test_wareki_reiwa(self): assert _wareki_to_year("R", 5) == 2023 def test_wareki_invalid_prefix(self): try: _wareki_to_year("X", 1) assert False, "应抛出异常" except ValueError: pass