Skip to content

Commit 90c7302

Browse files
fix: add regression test for incremental chunked hashing in Preset.get_hash()
Add test_get_hash_incremental_chunking that: - Creates a 200 KiB+ YAML manifest to force multiple 64 KiB chunks - Verifies the digest matches hashlib.sha256(content).hexdigest() - Rejects any unbounded f.read() call by patching builtins.open - Asserts at least 3 reads of <= 65536 bytes occurred
1 parent 793986a commit 90c7302

1 file changed

Lines changed: 67 additions & 0 deletions

File tree

tests/test_presets.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,73 @@ def test_get_hash(self, pack_dir):
339339
assert hash_val.startswith("sha256:")
340340
assert len(hash_val) > 10
341341

342+
def test_get_hash_incremental_chunking(self, temp_dir):
343+
"""Regression: get_hash() must read in bounded chunks, not f.read().
344+
345+
Creates a file larger than 64 KiB to exercise multiple chunk
346+
iterations, verifies the digest matches hashlib.sha256(content),
347+
and asserts that raw read() is never called with no size limit.
348+
"""
349+
import hashlib
350+
from unittest.mock import patch
351+
352+
# Build valid YAML content larger than 64 KiB. The padding field
353+
# is a long string that inflates the file without breaking YAML.
354+
padding = "P" * (200 * 1024) # 200 KiB
355+
yaml_content = (
356+
"schema_version: '1.0'\n"
357+
"preset:\n"
358+
" id: large-test\n"
359+
" name: Large Test\n"
360+
" version: '1.0.0'\n"
361+
" description: chunking test\n"
362+
" author: test\n"
363+
"requires:\n"
364+
" speckit_version: '>=0.1.0'\n"
365+
"provides:\n"
366+
" templates:\n"
367+
" - type: template\n"
368+
" name: test\n"
369+
" file: test.md\n"
370+
f" padding: \"{padding}\"\n"
371+
)
372+
content = yaml_content.encode("utf-8")
373+
assert len(content) > 65536 # Ensure multi-chunk coverage.
374+
375+
manifest_path = temp_dir / "large_preset.yml"
376+
manifest_path.write_bytes(content)
377+
378+
manifest = PresetManifest(manifest_path)
379+
380+
# Spy on read() to reject unbounded calls.
381+
original_open = open
382+
383+
read_sizes: list[int] = []
384+
385+
def tracking_open(path, *args, **kwargs):
386+
fh = original_open(path, *args, **kwargs)
387+
if args and args[0] == "rb" or kwargs.get("mode") == "rb":
388+
orig_read = fh.read
389+
def tracking_read(n=-1):
390+
if n == -1 or n is None:
391+
raise RuntimeError(
392+
"f.read() called without size limit — "
393+
"use bounded chunked reads instead"
394+
)
395+
read_sizes.append(n)
396+
return orig_read(n)
397+
fh.read = tracking_read
398+
return fh
399+
400+
with patch("builtins.open", side_effect=tracking_open):
401+
result = manifest.get_hash()
402+
403+
expected = f"sha256:{hashlib.sha256(content).hexdigest()}"
404+
assert result == expected
405+
# Verify bounded reads happened (at least 3 chunks of <= 65536 bytes).
406+
assert len(read_sizes) >= 3
407+
assert all(s <= 65536 for s in read_sizes)
408+
342409
def test_multiple_templates(self, temp_dir, valid_pack_data):
343410
"""Test pack with multiple templates of different types."""
344411
valid_pack_data["provides"]["templates"] = [

0 commit comments

Comments
 (0)