Skip to content

Commit 944fa3f

Browse files
Subramanian-K812PiotrKorkus
authored andcommitted
Add hash verification for all test scenarios of modified json
1 parent 32a4de0 commit 944fa3f

6 files changed

Lines changed: 152 additions & 11 deletions

File tree

feature_integration_tests/test_cases/persistency_scenario.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,35 @@ def read_kvs_snapshot(dir_path: Path, instance_id: int, snapshot_id: int = 0) ->
9191
return data
9292

9393

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+
94123
class PersistencyScenario(FitScenario):
95124
"""
96125
Base class for persistency feature integration tests.

feature_integration_tests/test_cases/tests/persistency/test_combined_requirements.py

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,12 @@
2323

2424
import pytest
2525
from fit_scenario import ResultCode
26-
from persistency_scenario import PersistencyScenario, create_kvs_defaults_file, read_kvs_snapshot
26+
from persistency_scenario import (
27+
PersistencyScenario,
28+
create_kvs_defaults_file,
29+
read_kvs_snapshot,
30+
verify_kvs_snapshot_hash,
31+
)
2732
from test_properties import add_test_properties
2833
from testing_utils import ScenarioResult
2934

@@ -92,6 +97,11 @@ def test_value_types_persisted(self, results: ScenarioResult, temp_dir: Path) ->
9297
assert snapshot["ascii_null"]["t"] == "null"
9398
assert snapshot["ascii_null"]["v"] is None
9499

100+
def test_snapshot_hash_matches_content(self, results: ScenarioResult, temp_dir: Path) -> None:
101+
"""Verify the hash file matches the Adler-32 of the snapshot JSON after normalization."""
102+
assert results.return_code == ResultCode.SUCCESS
103+
verify_kvs_snapshot_hash(temp_dir, instance_id=1, snapshot_id=0)
104+
95105

96106
# ---------------------------------------------------------------------------
97107
# Scenario 2: Partial override — only explicitly written keys enter snapshot
@@ -164,6 +174,11 @@ def test_only_overridden_key_in_snapshot(self, results: ScenarioResult, temp_dir
164174
assert "partial_key_0" not in snapshot, "Default-only key partial_key_0 must be absent from snapshot"
165175
assert "partial_key_2" not in snapshot, "Default-only key partial_key_2 must be absent from snapshot"
166176

177+
def test_snapshot_hash_matches_content(self, results: ScenarioResult, temp_dir: Path) -> None:
178+
"""Verify the hash file matches the Adler-32 of the snapshot JSON after normalization."""
179+
assert results.return_code == ResultCode.SUCCESS
180+
verify_kvs_snapshot_hash(temp_dir, instance_id=1, snapshot_id=0)
181+
167182
def test_default_values_accessible(self, results: ScenarioResult, logs_info_level: Any) -> None:
168183
"""
169184
Verify that the default values for partial_key_0 and partial_key_2 are
@@ -276,6 +291,11 @@ def test_default_only_utf8_keys_absent(self, results: ScenarioResult, temp_dir:
276291
f"Default-only Greek key '{self._KEY_GREEK}' must be absent from snapshot"
277292
)
278293

294+
def test_snapshot_hash_matches_content(self, results: ScenarioResult, temp_dir: Path) -> None:
295+
"""Verify the hash file matches the Adler-32 of the snapshot JSON after normalization."""
296+
assert results.return_code == ResultCode.SUCCESS
297+
verify_kvs_snapshot_hash(temp_dir, instance_id=1, snapshot_id=0)
298+
279299
def test_utf8_default_values_accessible(self, results: ScenarioResult, logs_info_level: Any) -> None:
280300
"""
281301
Verify that default values behind UTF-8 ASCII and Greek keys are accessible
@@ -353,3 +373,8 @@ def test_utf8_default_value_readable(self, results: ScenarioResult, temp_dir: Pa
353373
assert isclose(snapshot["result_key"]["v"], self._GET_DEFAULT_VALUE, abs_tol=1e-4), (
354374
f"Expected probe key value ≈ {self._GET_DEFAULT_VALUE}, got {snapshot['result_key']['v']}"
355375
)
376+
377+
def test_snapshot_hash_matches_content(self, results: ScenarioResult, temp_dir: Path) -> None:
378+
"""Verify the hash file matches the Adler-32 of the snapshot JSON after normalization."""
379+
assert results.return_code == ResultCode.SUCCESS
380+
verify_kvs_snapshot_hash(temp_dir, instance_id=1, snapshot_id=0)

feature_integration_tests/test_cases/tests/persistency/test_datatype_support.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
import pytest
1919
from fit_scenario import ResultCode
20-
from persistency_scenario import PersistencyScenario, read_kvs_snapshot
20+
from persistency_scenario import PersistencyScenario, read_kvs_snapshot, verify_kvs_snapshot_hash
2121
from test_properties import add_test_properties
2222
from testing_utils import ScenarioResult
2323

@@ -124,3 +124,8 @@ def test_all_types_in_snapshot(self, results: ScenarioResult, temp_dir: Path) ->
124124
for key, expected_tagged in self._EXPECTED_ALL_TYPES.items():
125125
assert key in snapshot, f"Expected key '{key}' in snapshot"
126126
self._assert_tagged_value(snapshot[key], expected_tagged)
127+
128+
def test_snapshot_hash_matches_content(self, results: ScenarioResult, temp_dir: Path) -> None:
129+
"""Verify the hash file matches the Adler-32 of the snapshot JSON after normalization."""
130+
assert results.return_code == ResultCode.SUCCESS
131+
verify_kvs_snapshot_hash(temp_dir, instance_id=1, snapshot_id=0)

feature_integration_tests/test_cases/tests/persistency/test_default_values.py

Lines changed: 33 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,12 @@
2121

2222
import pytest
2323
from fit_scenario import FitScenario, ResultCode, temp_dir_common
24-
from persistency_scenario import PersistencyScenario, create_kvs_defaults_file, read_kvs_snapshot
24+
from persistency_scenario import (
25+
PersistencyScenario,
26+
create_kvs_defaults_file,
27+
read_kvs_snapshot,
28+
verify_kvs_snapshot_hash,
29+
)
2530
from test_properties import add_test_properties
2631
from testing_utils import ScenarioResult
2732

@@ -94,6 +99,11 @@ def test_explicit_set_persisted(self, results: ScenarioResult, temp_dir: Path) -
9499
assert self._DEFAULT_KEY in snapshot, f"Expected key '{self._DEFAULT_KEY}' in snapshot"
95100
assert isclose(snapshot[self._DEFAULT_KEY]["v"], self._OVERRIDE_VALUE, abs_tol=1e-5)
96101

102+
def test_snapshot_hash_matches_content(self, results: ScenarioResult, temp_dir: Path) -> None:
103+
"""Verify the hash file matches the Adler-32 of the snapshot JSON after normalization."""
104+
assert results.return_code == ResultCode.SUCCESS
105+
verify_kvs_snapshot_hash(temp_dir, instance_id=1, snapshot_id=0)
106+
97107

98108
class DefaultValuesParityScenario(FitScenario):
99109
"""
@@ -191,12 +201,7 @@ def test_checksum(self, results: ScenarioResult, temp_dir: Path) -> None:
191201
Both files are at the conventional paths derived from instance_id.
192202
"""
193203
assert results.return_code == ResultCode.SUCCESS
194-
kvs_path = temp_dir / "kvs_1_0.json"
195-
hash_path = temp_dir / "kvs_1_0.hash"
196-
assert kvs_path.exists(), "KVS snapshot file must exist"
197-
assert hash_path.exists(), "KVS hash file must exist"
198-
expected = adler32(kvs_path.read_bytes()).to_bytes(length=4, byteorder="big")
199-
assert hash_path.read_bytes() == expected
204+
verify_kvs_snapshot_hash(temp_dir, instance_id=1, snapshot_id=0)
200205

201206

202207
@add_test_properties(
@@ -340,6 +345,11 @@ def test_default_value_readable(self, results: ScenarioResult, temp_dir: Path) -
340345
f"Expected probe key value ≈ {self._GET_DEFAULT_EXPECTED}, got {snapshot['result_key']['v']}"
341346
)
342347

348+
def test_snapshot_hash_matches_content(self, results: ScenarioResult, temp_dir: Path) -> None:
349+
"""Verify the hash file matches the Adler-32 of the snapshot JSON after normalization."""
350+
assert results.return_code == ResultCode.SUCCESS
351+
verify_kvs_snapshot_hash(temp_dir, instance_id=1, snapshot_id=0)
352+
343353

344354
@add_test_properties(
345355
partially_verifies=[
@@ -415,6 +425,11 @@ def test_selective_reset_state(self, results: ScenarioResult, temp_dir: Path) ->
415425
f"Expected {key}{self._override_value(i)}, got {snapshot[key]['v']}"
416426
)
417427

428+
def test_snapshot_hash_matches_content(self, results: ScenarioResult, temp_dir: Path) -> None:
429+
"""Verify the hash file matches the Adler-32 of the snapshot JSON after normalization."""
430+
assert results.return_code == ResultCode.SUCCESS
431+
verify_kvs_snapshot_hash(temp_dir, instance_id=1, snapshot_id=0)
432+
418433
def test_reset_key_returns_default(self, results: ScenarioResult, logs_info_level: Any) -> None:
419434
"""
420435
Verify that after reset_key on sel_key_0, KVS still reports its default value
@@ -513,6 +528,11 @@ def test_full_reset_new_keys_present(self, results: ScenarioResult, temp_dir: Pa
513528
f"Expected {key}{expected}, got {snapshot[key]['v']}"
514529
)
515530

531+
def test_snapshot_hash_matches_content(self, results: ScenarioResult, temp_dir: Path) -> None:
532+
"""Verify the hash file matches the Adler-32 of the snapshot JSON after normalization."""
533+
assert results.return_code == ResultCode.SUCCESS
534+
verify_kvs_snapshot_hash(temp_dir, instance_id=1, snapshot_id=0)
535+
516536
def test_full_reset_key_returns_default(self, results: ScenarioResult, logs_info_level: Any) -> None:
517537
"""
518538
Verify that after reset(), fr_key_0 still returns its default value via
@@ -641,6 +661,12 @@ def test_instance_2_snapshot_isolation(self, results: ScenarioResult, temp_dir:
641661
assert "key_b" in snapshot2, "key_b must be present in instance 2 snapshot"
642662
assert "key_a" not in snapshot2, "key_a must not leak from instance 1 defaults into instance 2 snapshot"
643663

664+
def test_snapshot_hash_matches_content(self, results: ScenarioResult, temp_dir: Path) -> None:
665+
"""Verify hash files match the Adler-32 of each instance snapshot after normalization."""
666+
assert results.return_code == ResultCode.SUCCESS
667+
verify_kvs_snapshot_hash(temp_dir, instance_id=1, snapshot_id=0)
668+
verify_kvs_snapshot_hash(temp_dir, instance_id=2, snapshot_id=0)
669+
644670
def test_default_isolation_via_logs(self, results: ScenarioResult, logs_info_level: Any) -> None:
645671
"""
646672
Verify that each instance's own default value is accessible and the

feature_integration_tests/test_cases/tests/persistency/test_reset_to_default.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,12 @@
1717

1818
import pytest
1919
from fit_scenario import ResultCode
20-
from persistency_scenario import PersistencyScenario, create_kvs_defaults_file, read_kvs_snapshot
20+
from persistency_scenario import (
21+
PersistencyScenario,
22+
create_kvs_defaults_file,
23+
read_kvs_snapshot,
24+
verify_kvs_snapshot_hash,
25+
)
2126
from test_properties import add_test_properties
2227
from testing_utils import ScenarioResult
2328

@@ -111,3 +116,8 @@ def test_default_value_reported_after_reset(self, results: ScenarioResult, logs_
111116
assert isclose(float(log.value), expected_default, abs_tol=1e-4), (
112117
f"Expected key2 default ≈ {expected_default}, got {log.value}"
113118
)
119+
120+
def test_snapshot_hash_matches_content(self, results: ScenarioResult, temp_dir: Path) -> None:
121+
"""Verify the hash file matches the Adler-32 of the snapshot JSON after normalization."""
122+
assert results.return_code == ResultCode.SUCCESS
123+
verify_kvs_snapshot_hash(temp_dir, instance_id=1, snapshot_id=0)

feature_integration_tests/test_scenarios/cpp/src/internals/persistency/kvs_instance.cpp

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515

1616
#include <kvsbuilder.hpp>
1717

18+
#include <array>
1819
#include <cctype>
1920
#include <cmath>
2021
#include <cstdint>
@@ -121,6 +122,25 @@ std::string canonicalize_f64_literals(const std::string& json) {
121122
return result;
122123
}
123124

125+
/**
126+
* @brief Compute the Adler-32 checksum of a byte string.
127+
*
128+
* Matches Python's ``zlib.adler32(data).to_bytes(4, byteorder='big')``.
129+
*
130+
* @param data Input byte string.
131+
* @return 32-bit Adler-32 checksum.
132+
*/
133+
uint32_t adler32_hash(const std::string& data) {
134+
static constexpr uint32_t kMod = 65521U;
135+
uint32_t a = 1U;
136+
uint32_t b = 0U;
137+
for (const unsigned char byte : data) {
138+
a = (a + static_cast<uint32_t>(byte)) % kMod;
139+
b = (b + a) % kMod;
140+
}
141+
return (b << 16U) | a;
142+
}
143+
124144
std::optional<std::string> snapshot_path(const KvsParameters& params) {
125145
if (!params.dir.has_value()) {
126146
return std::nullopt;
@@ -231,7 +251,33 @@ bool KvsInstance::normalize_snapshot_file_to_rust_envelope(const KvsParameters&
231251
}
232252

233253
out << final_content;
234-
return static_cast<bool>(out);
254+
if (!out) {
255+
return false;
256+
}
257+
out.close();
258+
259+
// Recalculate and rewrite the companion hash file so the snapshot stays
260+
// valid after JSON modification. Hash = Adler-32 of the written JSON
261+
// bytes stored as 4 big-endian bytes, matching Python's
262+
// zlib.adler32(data).to_bytes(4, byteorder='big').
263+
const std::string hash_path_str =
264+
(*path_opt).substr(0, (*path_opt).size() - 5U) + ".hash";
265+
const uint32_t checksum = adler32_hash(final_content);
266+
const std::array<uint8_t, 4> hash_bytes = {
267+
static_cast<uint8_t>((checksum >> 24U) & 0xFFU),
268+
static_cast<uint8_t>((checksum >> 16U) & 0xFFU),
269+
static_cast<uint8_t>((checksum >> 8U) & 0xFFU),
270+
static_cast<uint8_t>( checksum & 0xFFU),
271+
};
272+
std::ofstream hash_out(hash_path_str, std::ios::binary | std::ios::trunc);
273+
if (!hash_out.is_open()) {
274+
std::cerr << "Cannot normalize snapshot: failed to write hash "
275+
<< hash_path_str << std::endl;
276+
return false;
277+
}
278+
hash_out.write(reinterpret_cast<const char*>(hash_bytes.data()),
279+
static_cast<std::streamsize>(hash_bytes.size()));
280+
return static_cast<bool>(hash_out);
235281
}
236282

237283
bool KvsInstance::set_value(const std::string& key, double value) {

0 commit comments

Comments
 (0)