|
| 1 | +# ******************************************************************************* |
| 2 | +# Copyright (c) 2026 Contributors to the Eclipse Foundation |
| 3 | +# |
| 4 | +# See the NOTICE file(s) distributed with this work for additional |
| 5 | +# information regarding copyright ownership. |
| 6 | +# |
| 7 | +# This program and the accompanying materials are made available under the |
| 8 | +# terms of the Apache License Version 2.0 which is available at |
| 9 | +# https://www.apache.org/licenses/LICENSE-2.0 |
| 10 | +# |
| 11 | +# SPDX-License-Identifier: Apache-2.0 |
| 12 | +# ******************************************************************************* |
| 13 | +""" |
| 14 | +Helpers and base scenario class for persistency feature integration tests. |
| 15 | +
|
| 16 | +``create_kvs_defaults_file`` and ``read_kvs_snapshot`` provide the file-system |
| 17 | +operations that test methods use to set up and inspect KVS state. |
| 18 | +``PersistencyScenario`` is a :class:`FitScenario` subclass that supplies the |
| 19 | +shared ``temp_dir`` fixture so individual test classes do not have to duplicate it. |
| 20 | +""" |
| 21 | + |
| 22 | +import json |
| 23 | +from collections.abc import Generator |
| 24 | +from pathlib import Path |
| 25 | +from zlib import adler32 |
| 26 | + |
| 27 | +import pytest |
| 28 | +from fit_scenario import FitScenario, temp_dir_common |
| 29 | + |
| 30 | + |
| 31 | +def create_kvs_defaults_file(dir_path: Path, instance_id: int, values: dict) -> Path: |
| 32 | + """ |
| 33 | + Create a KVS defaults JSON file and matching hash file at conventional paths. |
| 34 | +
|
| 35 | + KVS expects defaults at: {dir}/kvs_{instance_id}_default.json |
| 36 | + and the hash at: {dir}/kvs_{instance_id}_default.hash |
| 37 | +
|
| 38 | + The JSON format is: {"key": {"t": "type_tag", "v": value}, ...} |
| 39 | + The hash is adler32 of the JSON string, written as 4 big-endian bytes. |
| 40 | +
|
| 41 | + Parameters |
| 42 | + ---------- |
| 43 | + dir_path : Path |
| 44 | + Working directory for the KVS instance. |
| 45 | + instance_id : int |
| 46 | + KVS instance identifier. |
| 47 | + values : dict |
| 48 | + Mapping of key -> (type_tag, value), e.g. {"my_key": ("f64", 1.0)}. |
| 49 | +
|
| 50 | + Returns |
| 51 | + ------- |
| 52 | + Path |
| 53 | + Path to the created JSON defaults file. |
| 54 | + """ |
| 55 | + json_path = dir_path / f"kvs_{instance_id}_default.json" |
| 56 | + hash_path = dir_path / f"kvs_{instance_id}_default.hash" |
| 57 | + |
| 58 | + data = {key: {"t": type_tag, "v": val} for key, (type_tag, val) in values.items()} |
| 59 | + json_str = json.dumps(data) |
| 60 | + |
| 61 | + json_path.write_text(json_str) |
| 62 | + hash_path.write_bytes(adler32(json_str.encode()).to_bytes(length=4, byteorder="big")) |
| 63 | + return json_path |
| 64 | + |
| 65 | + |
| 66 | +def read_kvs_snapshot(dir_path: Path, instance_id: int, snapshot_id: int = 0) -> dict: |
| 67 | + """ |
| 68 | + Read and parse the KVS snapshot JSON for a given instance. |
| 69 | +
|
| 70 | + Supports both the Rust/normalized envelope format {"t":"obj","v":{...}} |
| 71 | + and the raw C++ format {key: {...}}. Returns the inner key -> tagged-value mapping. |
| 72 | +
|
| 73 | + Parameters |
| 74 | + ---------- |
| 75 | + dir_path : Path |
| 76 | + Working directory containing the KVS snapshot files. |
| 77 | + instance_id : int |
| 78 | + KVS instance identifier used in the filename convention. |
| 79 | + snapshot_id : int, optional |
| 80 | + Snapshot sequence number (default 0). |
| 81 | +
|
| 82 | + Returns |
| 83 | + ------- |
| 84 | + dict |
| 85 | + Mapping of key -> tagged-value dict, e.g. {"mykey": {"t": "f64", "v": 1.0}}. |
| 86 | + """ |
| 87 | + path = dir_path / f"kvs_{instance_id}_{snapshot_id}.json" |
| 88 | + data = json.loads(path.read_text()) |
| 89 | + if isinstance(data, dict) and data.get("t") == "obj" and "v" in data: |
| 90 | + return data["v"] |
| 91 | + return data |
| 92 | + |
| 93 | + |
| 94 | +def verify_kvs_snapshot_hash(dir_path: Path, instance_id: int, snapshot_id: int = 0) -> None: |
| 95 | + """ |
| 96 | + Assert that the snapshot hash file content matches the Adler-32 of the JSON file. |
| 97 | +
|
| 98 | + After ``normalize_snapshot_file_to_rust_envelope`` rewrites the JSON, the |
| 99 | + companion ``.hash`` file must also be rewritten. This helper detects any |
| 100 | + mismatch between the two, catching stale hashes introduced by manual or |
| 101 | + tool-driven JSON modifications. |
| 102 | +
|
| 103 | + Parameters |
| 104 | + ---------- |
| 105 | + dir_path : Path |
| 106 | + Working directory containing the KVS snapshot files. |
| 107 | + instance_id : int |
| 108 | + KVS instance identifier used in the filename convention. |
| 109 | + snapshot_id : int, optional |
| 110 | + Snapshot sequence number (default 0). |
| 111 | + """ |
| 112 | + json_path = dir_path / f"kvs_{instance_id}_{snapshot_id}.json" |
| 113 | + hash_path = dir_path / f"kvs_{instance_id}_{snapshot_id}.hash" |
| 114 | + json_bytes = json_path.read_bytes() |
| 115 | + expected = adler32(json_bytes).to_bytes(4, byteorder="big") |
| 116 | + actual = hash_path.read_bytes() |
| 117 | + assert actual == expected, ( |
| 118 | + f"Hash mismatch for kvs_{instance_id}_{snapshot_id}: " |
| 119 | + f"hash file contains {actual.hex()} but Adler-32 of the JSON is {expected.hex()}" |
| 120 | + ) |
| 121 | + |
| 122 | + |
| 123 | +class PersistencyScenario(FitScenario): |
| 124 | + """ |
| 125 | + Base class for persistency feature integration tests. |
| 126 | +
|
| 127 | + Provides the ``temp_dir`` fixture shared by all persistency test classes, |
| 128 | + avoiding fixture duplication across subclasses. |
| 129 | + """ |
| 130 | + |
| 131 | + @pytest.fixture(scope="class") |
| 132 | + def temp_dir( |
| 133 | + self, |
| 134 | + tmp_path_factory: pytest.TempPathFactory, |
| 135 | + version: str, |
| 136 | + ) -> Generator[Path, None, None]: |
| 137 | + """ |
| 138 | + Provide a temporary working directory for the KVS instance. |
| 139 | +
|
| 140 | + The directory is named after the test class and parametrized version, |
| 141 | + and is automatically removed after the test class completes. |
| 142 | +
|
| 143 | + Parameters |
| 144 | + ---------- |
| 145 | + tmp_path_factory : pytest.TempPathFactory |
| 146 | + Built-in pytest factory for temporary directories. |
| 147 | + version : str |
| 148 | + Parametrized scenario version (``"rust"`` or ``"cpp"``). |
| 149 | + """ |
| 150 | + yield from temp_dir_common(tmp_path_factory, self.__class__.__name__, version) |
0 commit comments