Option C Tools
Loading...
Searching...
No Matches
test_lint_cache.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# -*- coding: utf-8 -*-
3# tests/tests_linter/test_lint_cache.py
4
5"""
6Purpose
7-------
8FS-510 regression coverage for the incremental lint cache.
9
10Responsibilities
11----------------
12- Validate content_hash and rules_hash are deterministic.
13- Validate cache miss on first run and hit on second.
14- Validate invalidation on content, rule, or version change.
15- Validate corrupt/missing file handling.
16- Validate --no-cache bypasses reads.
17- Validate thread safety.
18
19Diagnostics
20-----------
21Domain: LINTER-TESTS
22"""
23
24from __future__ import annotations
25
26import json
27import threading
28from pathlib import Path
29
30import pytest
31
32from oct.linter.lint_cache import (
33 LintCache,
34 LintCacheEntry,
35 content_hash,
36 rules_hash,
37)
38
39
40# -------------------------------------------------------------------
41# Hash determinism
42# -------------------------------------------------------------------
43
45
47 assert content_hash("hello") == content_hash("hello")
48
50 assert content_hash("hello") != content_hash("world")
51
53 rules = {"header": True, "docstring": False}
54 assert rules_hash(rules) == rules_hash(rules)
55
57 r1 = {"a": True, "b": False}
58 r2 = {"b": False, "a": True}
59 assert rules_hash(r1) == rules_hash(r2)
60
62 r1 = {"header": True}
63 r2 = {"header": False}
64 assert rules_hash(r1) != rules_hash(r2)
65
66
67# -------------------------------------------------------------------
68# Cache operations
69# -------------------------------------------------------------------
70
71def _make_entry(file_hash: str = "abc123") -> LintCacheEntry:
72 return LintCacheEntry(
73 content_hash=file_hash,
74 rule_results={"header": True, "docstring": False},
75 json_detail={"path": "test.py", "checks": {}},
76 oct_version="0.22.0",
77 timestamp="2026-04-17T00:00:00",
78 )
79
80
82
83 def test_miss_on_empty_cache(self, tmp_path: Path):
84 cache = LintCache(tmp_path / "cache.json", "0.22.0")
85 assert cache.get("abc", "def") is None
86
87 def test_put_and_get(self, tmp_path: Path):
88 cache = LintCache(tmp_path / "cache.json", "0.22.0")
89 entry = _make_entry("abc123")
90 cache.put("abc123", "rules1", entry)
91 result = cache.get("abc123", "rules1")
92 assert result is not None
93 assert result.content_hash == "abc123"
94 assert result.rule_results == {"header": True, "docstring": False}
95
96 def test_save_and_reload(self, tmp_path: Path):
97 cache_path = tmp_path / "cache.json"
98 cache = LintCache(cache_path, "0.22.0")
99 cache.put("abc123", "rules1", _make_entry("abc123"))
100 cache.save()
101
102 cache2 = LintCache(cache_path, "0.22.0")
103 result = cache2.get("abc123", "rules1")
104 assert result is not None
105 assert result.content_hash == "abc123"
106
107 def test_invalidated_on_version_change(self, tmp_path: Path):
108 cache_path = tmp_path / "cache.json"
109 cache = LintCache(cache_path, "0.22.0")
110 cache.put("abc123", "rules1", _make_entry("abc123"))
111 cache.save()
112
113 cache2 = LintCache(cache_path, "0.23.0")
114 assert cache2.get("abc123", "rules1") is None
115
116 def test_invalidated_on_content_change(self, tmp_path: Path):
117 cache = LintCache(tmp_path / "cache.json", "0.22.0")
118 cache.put("abc123", "rules1", _make_entry("abc123"))
119 assert cache.get("different_hash", "rules1") is None
120
121 def test_invalidated_on_rules_change(self, tmp_path: Path):
122 cache = LintCache(tmp_path / "cache.json", "0.22.0")
123 cache.put("abc123", "rules1", _make_entry("abc123"))
124 assert cache.get("abc123", "different_rules") is None
125
126 def test_corrupt_file_starts_fresh(self, tmp_path: Path):
127 cache_path = tmp_path / "cache.json"
128 cache_path.write_text("not json!!!", encoding="utf-8")
129 cache = LintCache(cache_path, "0.22.0")
130 assert cache.entry_count == 0
131
132 def test_missing_file_starts_fresh(self, tmp_path: Path):
133 cache = LintCache(tmp_path / "nonexistent.json", "0.22.0")
134 assert cache.entry_count == 0
135
136 def test_save_creates_parent_dirs(self, tmp_path: Path):
137 cache_path = tmp_path / "deep" / "nested" / "cache.json"
138 cache = LintCache(cache_path, "0.22.0")
139 cache.put("abc", "rules", _make_entry("abc"))
140 cache.save()
141 assert cache_path.is_file()
142
143 def test_entry_count(self, tmp_path: Path):
144 cache = LintCache(tmp_path / "cache.json", "0.22.0")
145 assert cache.entry_count == 0
146 cache.put("a", "r1", _make_entry("a"))
147 assert cache.entry_count == 1
148 cache.put("b", "r1", _make_entry("b"))
149 assert cache.entry_count == 2
150
151 def test_thread_safety(self, tmp_path: Path):
152 cache = LintCache(tmp_path / "cache.json", "0.22.0")
153 errors: list[str] = []
154
155 def worker(i: int) -> None:
156 try:
157 h = f"hash_{i}"
158 cache.put(h, "rules", _make_entry(h))
159 result = cache.get(h, "rules")
160 if result is None:
161 errors.append(f"Worker {i}: put then get returned None")
162 except Exception as e:
163 errors.append(f"Worker {i}: {e}")
164
165 threads = [threading.Thread(target=worker, args=(i,)) for i in range(20)]
166 for t in threads:
167 t.start()
168 for t in threads:
169 t.join()
170
171 assert not errors, f"Thread safety errors: {errors}"
172 assert cache.entry_count == 20
Definition lint_cache.py:57
LintCacheEntry _make_entry(str file_hash="abc123")