/ proof-of-concept / cymsim
Sign in
proof-of-concept / cymsim
cymsim / tests / test_cache_run.py
173 lines · 7.0 KBBlameHistoryRaw
1"""Reading runs from a cache: a local HTTP server stands in for the artifact-cache app."""
2import copy
3import hashlib
4import logging
5import threading
6import urllib.error
7from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
9import pytest
10from test_resume import _assert_same_files
12from cymsim.cache import cache_key, cached_run, fetch
13from cymsim.cli import main
14from cymsim.cymsim_file import load_cymsim
15from cymsim.simulate import Simulator
17PREFIX = "/api/apps/cymsim/entries/"
20class FakeCache:
21 """GET <prefix><key> with ETag revalidation, as the artifact-cache worker serves it."""
23 def __init__(self):
24 self.entries, self.requests = {}, []
25 cache = self
27 class Handler(BaseHTTPRequestHandler):
28 def do_GET(self):
29 key = self.path[len(PREFIX):] if self.path.startswith(PREFIX) else None
30 data = cache.entries.get(key)
31 if data is None:
32 status = 404
33 self.send_response(404)
34 self.end_headers()
35 else:
36 etag = f'"{hashlib.md5(data).hexdigest()}"'
37 status = 304 if self.headers.get("If-None-Match") == etag else 200
38 self.send_response(status)
39 self.send_header("ETag", etag)
40 if status == 200:
41 self.send_header("Content-Length", str(len(data)))
42 self.end_headers()
43 if status == 200:
44 self.wfile.write(data)
45 cache.requests.append((key, status))
47 def log_message(self, *args):
48 pass
50 self.server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
51 self.url = f"http://127.0.0.1:{self.server.server_address[1]}{PREFIX[:-len('entries/')]}"
52 threading.Thread(target=self.server.serve_forever, daemon=True).start()
54 def put(self, cfg, path):
55 self.entries[cache_key(cfg)] = path.read_bytes()
58@pytest.fixture
59def cache():
60 c = FakeCache()
61 yield c
62 c.server.shutdown()
65def _config(base, duration):
66 cfg = copy.deepcopy(base)
67 cfg.numerics.duration = duration
68 cfg.output.displacement_rate = 200.0
69 cfg.output.probes = [(0.1, 0.05), (0.05, -0.12)]
70 return cfg
73@pytest.fixture(scope="module")
74def runs(coarse_config, tmp_path_factory):
75 """Uninterrupted runs of 0.02 and 0.05 s, saved."""
76 d = tmp_path_factory.mktemp("runs")
77 out = {}
78 for T in (0.02, 0.05):
79 out[T] = d / f"{T}.cymsim"
80 Simulator(_config(coarse_config, T)).run(save_path=str(out[T]))
81 return out
84def test_without_an_entry_the_run_starts_from_rest(coarse_config, runs, cache, tmp_path):
85 out = tmp_path / "out.cymsim"
86 cached_run(_config(coarse_config, 0.05), cache.url, save_path=str(out), directory=tmp_path / "c")
87 _assert_same_files(out, runs[0.05])
88 assert cache.requests == [(cache_key(_config(coarse_config, 0.05)), 404)]
91def test_a_covering_entry_is_cut_without_building_the_model(coarse_config, runs, cache, tmp_path, monkeypatch):
92 cfg = _config(coarse_config, 0.03)
93 ref = tmp_path / "ref.cymsim"
94 Simulator(cfg).run(save_path=str(ref), resume=load_cymsim(runs[0.05]))
95 cache.put(cfg, runs[0.05])
97 def no_model(*args, **kwargs):
98 raise AssertionError("the model was built")
100 monkeypatch.setattr(Simulator, "__init__", no_model)
101 out = tmp_path / "out.cymsim"
102 res = cached_run(cfg, cache.url, save_path=str(out), directory=tmp_path / "c")
103 assert res.complete and len(res.pressure_rayleigh) == round(44100 * 0.03)
104 _assert_same_files(out, ref) # including the state, whose overhang has grown
105 # the energy log stops where an uninterrupted 0.03 s run stops, as on the resume path
106 cached_log, ref_log = load_cymsim(runs[0.05]).energy_log, load_cymsim(ref).energy_log
107 assert res.energy_log == ref_log and len(ref_log) < len(cached_log)
108 assert res.energy_log[-1].time <= 0.03 + 0.02 and cached_log[-1].time > 0.05
111def test_a_shorter_entry_is_continued(coarse_config, runs, cache, tmp_path):
112 cfg = _config(coarse_config, 0.05)
113 cache.put(cfg, runs[0.02])
114 out = tmp_path / "out.cymsim"
115 cached_run(cfg, cache.url, save_path=str(out), directory=tmp_path / "c")
116 _assert_same_files(out, runs[0.05])
119def test_an_entry_of_another_configuration_is_ignored(coarse_config, runs, cache, tmp_path, caplog):
120 cfg = _config(coarse_config, 0.02)
121 other = copy.deepcopy(cfg)
122 other.hits[0].velocity *= 2
123 other_path = tmp_path / "other.cymsim"
124 Simulator(other).run(save_path=str(other_path))
125 cache.entries[cache_key(cfg)] = other_path.read_bytes()
126 out = tmp_path / "out.cymsim"
127 with caplog.at_level(logging.WARNING, logger="cymsim"):
128 cached_run(cfg, cache.url, save_path=str(out), directory=tmp_path / "c")
129 assert "different configuration" in caplog.text
130 _assert_same_files(out, runs[0.02])
133def test_local_copies(coarse_config, runs, cache, tmp_path):
134 cfg, d = _config(coarse_config, 0.02), tmp_path / "c"
135 key = cache_key(cfg)
136 cache.put(cfg, runs[0.02])
137 p = fetch(cache.url, key, d)
138 assert p == d / f"{key}.cymsim" and p.read_bytes() == runs[0.02].read_bytes()
139 assert fetch(cache.url, key, d) == p
140 assert [s for _, s in cache.requests] == [200, 304]
141 cache.entries[key] = runs[0.05].read_bytes() # replaced remotely: downloaded again
142 assert fetch(cache.url, key, d).read_bytes() == runs[0.05].read_bytes()
143 dead = "http://127.0.0.1:9/api/apps/cymsim" # unreachable: the local copy serves
144 assert fetch(dead, key, d) == p
145 with pytest.raises(urllib.error.URLError):
146 fetch(dead, "v1/missing", d)
147 del cache.entries[key] # deleted remotely: the local copy goes too
148 assert fetch(cache.url, key, d) is None and not p.exists()
151def test_the_acceleration_record_bypasses_the_cache(coarse_config, cache, tmp_path):
152 cfg = _config(coarse_config, 0.02)
153 cfg.output.record_acceleration = True
154 res = cached_run(cfg, cache.url, directory=tmp_path / "c")
155 assert res.acceleration is not None and cache.requests == []
158def test_cli_cache(coarse_config, runs, cache, tmp_path, monkeypatch):
159 cfg = _config(coarse_config, 0.03)
160 cfg.output.wav = cfg.output.npz = cfg.output.cascade_plot = False
161 cfg.output.prefix = str(tmp_path / "x")
162 ypath = tmp_path / "c.yaml"
163 cfg.to_yaml(ypath)
164 cache.put(cfg, runs[0.05])
165 monkeypatch.setenv("CYMSIM_CACHE_DIR", str(tmp_path / "c"))
166 out = tmp_path / "out.cymsim"
167 assert main(["run", str(ypath), "--cache", cache.url, "-o", str(out), "--quiet"]) == 0
168 assert len(load_cymsim(out).pressure_rayleigh) == round(44100 * 0.03)
169 monkeypatch.setenv("CYMSIM_CACHE_URL", cache.url)
170 assert main(["run", str(ypath), "-o", str(out), "--quiet"]) == 0
171 assert main(["run", str(ypath), "--no-cache", "-o", str(out), "--quiet"]) == 0
172 assert [s for _, s in cache.requests] == [200, 304]
173 assert main(["run", str(ypath), "--cache", cache.url, "--resume", str(out), "--quiet"]) == 2
↑↓ move↵openescclose