feat: golden corpus — DadosEmissao JSON -> canonical pre-signature XML
Adds golden_helpers (dados_from_json/serialize_infnfe, package modules so the auto's parity test and the future service can import them directly) and 6 golden cases under src/sowai_fiscal/goldens/ as package data: - caso_padrao_intra: preset Padrao, PR->PR, CSOSN 102/CFOP 5102, 2 itens - caso_padrao_inter: PR->SP, CFOP 6102, idDest=2 - caso_st_intra: Oleos, CSOSN 500/CFOP 5405, grupo ICMSSN500 (vBCSTRet/pST/vICMSSTRet) - caso_devolucao_intra: tipo_operacao devolucao, CFOP 1202 - caso_com_ub: FiscalResult com linhas ibs/cbs (grupo UB presente -- builder suporta estruturalmente mesmo com o guard de emissao do auto bloqueando hoje) - caso_fracao_centavo: qty 1.5 x 0.01, 2 linhas -- prova o arredondamento por-item (soma_itens_quantizados) contra a soma bruta Every input pins dh_emi/cnf/chave_acesso for determinism. serialize_infnfe replicates the exact SerializerConfig(xml_declaration=False, indent=None) + ns_map used by the auto's emissao.py::_serialize_nfe before signing. scripts/gen_goldens.py regenerates the .expected.xml files; test_goldens.py compares byte-for-byte (parametrized per case, readable first-divergent-byte diff on failure). Verified the comparison has real detection power by corrupting one expected file and confirming the test fails, then restored. 62 tests pass locally via `uv run pytest`.
This commit is contained in:
@@ -0,0 +1,56 @@
|
||||
"""Corpus golden: para cada `caso_*.input.json` em `sowai_fiscal.goldens`,
|
||||
carrega o `DadosEmissao`, monta+serializa o XML pré-assinatura e compara
|
||||
BYTE A BYTE com o `caso_*.expected.xml` irmão. Estes dados são PACOTE
|
||||
(`importlib.resources`), não fixtures de `tests/` -- o produto consumidor
|
||||
(auto) e o futuro serviço leem os MESMOS arquivos da lib instalada.
|
||||
|
||||
Regenerar os `.expected.xml` (nunca à mão) com `scripts/gen_goldens.py` --
|
||||
só quando uma NT mudar o leiaute DE PROPÓSITO."""
|
||||
import importlib.resources
|
||||
|
||||
import pytest
|
||||
|
||||
from sowai_fiscal.golden_helpers import dados_from_json, serialize_infnfe
|
||||
|
||||
_GOLDENS = importlib.resources.files("sowai_fiscal") / "goldens"
|
||||
|
||||
|
||||
def _casos() -> list[str]:
|
||||
with importlib.resources.as_file(_GOLDENS) as goldens_dir:
|
||||
return sorted(
|
||||
p.name.removesuffix(".input.json")
|
||||
for p in goldens_dir.glob("caso_*.input.json")
|
||||
)
|
||||
|
||||
|
||||
_CASOS = _casos()
|
||||
|
||||
|
||||
def test_at_least_six_golden_cases_exist():
|
||||
assert len(_CASOS) >= 6, f"esperado >=6 casos golden, achei {len(_CASOS)}: {_CASOS}"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("caso", _CASOS)
|
||||
def test_golden_case_matches_expected_xml_byte_a_byte(caso):
|
||||
with importlib.resources.as_file(_GOLDENS) as goldens_dir:
|
||||
input_path = goldens_dir / f"{caso}.input.json"
|
||||
expected_path = goldens_dir / f"{caso}.expected.xml"
|
||||
|
||||
dados = dados_from_json(input_path)
|
||||
actual = serialize_infnfe(dados)
|
||||
expected = expected_path.read_bytes()
|
||||
|
||||
if actual != expected:
|
||||
# Diff legível: aponta o primeiro byte divergente em vez de despejar
|
||||
# os dois XMLs inteiros no assert.
|
||||
first_diff = next(
|
||||
(i for i, (a, e) in enumerate(zip(actual, expected)) if a != e),
|
||||
min(len(actual), len(expected)),
|
||||
)
|
||||
window = 60
|
||||
start = max(0, first_diff - window)
|
||||
assert actual == expected, (
|
||||
f"caso {caso!r} diverge no byte {first_diff}:\n"
|
||||
f" actual: ...{actual[start:first_diff + window]!r}...\n"
|
||||
f" expected: ...{expected[start:first_diff + window]!r}..."
|
||||
)
|
||||
Reference in New Issue
Block a user