import json import threading import urllib.request from http.server import ThreadingHTTPServer from gausse.optim.sweep import run_sweep from gausse.web.page import PAGE_HTML from gausse.web.stats import overview, run_detail def _make_db(tmp_path): db = tmp_path / "runs.sqlite3" run_sweep(db, n_runs=60, n_workers=2, seed=11) return db def test_overview_reports_counts_and_top(tmp_path): db = _make_db(tmp_path) o = overview(db, db.with_suffix(".sqlite3.log")) assert o["total"] == 60 assert o["feasible"] + o["infeasible"] == 60 assert len(o["efficiency_histogram"]) == 10 # лог процесса должен существовать и попасть в хвост assert len(o["log_tail"]) > 0 if o["top"]: # топ отсортирован по КПД по убыванию effs = [t["efficiency"] for t in o["top"]] assert effs == sorted(effs, reverse=True) def test_run_detail_has_full_physics_and_distances(tmp_path): db = _make_db(tmp_path) o = overview(db, None) assert o["top"], "нужен хотя бы один реализуемый прогон" d = run_detail(db, o["top"][0]["run_id"]) assert d is not None detail = d["detail"] # ключевое, что просил пользователь: расстояния между катушками и позиции assert "inter_stage_gaps_m" in detail assert "coil_center_positions_m" in detail stage0 = detail["stages"][0] assert "electrical" in stage0 and "wire_resistance_ac_ohm" in stage0["electrical"] # v2: скин/близость — AC-сопротивление обмотки выше DC assert stage0["electrical"]["wire_ac_resistance_factor"] >= 1.0 assert "winding" in stage0 and "total_turns" in stage0["winding"] assert "sensor_to_coil_distance_m" in stage0 def test_run_detail_missing_id_returns_none(tmp_path): db = _make_db(tmp_path) assert run_detail(db, "нет-такого-id") is None def test_http_server_serves_page_and_api(tmp_path): from functools import partial from gausse.web.server import _Handler db = _make_db(tmp_path) handler = partial(_Handler, db_path=db, log_path=db.with_suffix(".sqlite3.log")) httpd = ThreadingHTTPServer(("127.0.0.1", 0), handler) port = httpd.server_address[1] t = threading.Thread(target=httpd.serve_forever, daemon=True) t.start() try: base = f"http://127.0.0.1:{port}" page = urllib.request.urlopen(base + "/", timeout=5).read().decode("utf-8") assert "" in page.lower() api = json.loads(urllib.request.urlopen(base + "/api/overview", timeout=5).read()) assert api["total"] == 60 finally: httpd.shutdown() httpd.server_close() def test_page_is_self_contained(): # никаких внешних ресурсов (CSP-безопасно): ни http-ссылок в src/href, ни CDN assert "src=\"http" not in PAGE_HTML assert "href=\"http" not in PAGE_HTML def test_render_plot_png_returns_image_bytes(tmp_path): from gausse.web.render import render_plot_png from gausse.web.stats import overview db = _make_db(tmp_path) o = overview(db, None) assert o["top"], "нужен реализуемый прогон для графика" run_id = o["top"][0]["run_id"] for kind in ("velocity", "field", "current"): png = render_plot_png(db, run_id, kind) assert png is not None and png[:8] == b"\x89PNG\r\n\x1a\n", f"{kind} не PNG" def test_render_plot_unknown_kind_returns_none(tmp_path): from gausse.web.render import render_plot_png db = _make_db(tmp_path) o = _overview_top_id(db) assert render_plot_png(db, o, "nonsense") is None def _overview_top_id(db): from gausse.web.stats import overview return overview(db, None)["top"][0]["run_id"] def test_render_animation_gif_returns_gif_bytes(tmp_path): from gausse.web.render import render_animation_gif db = _make_db(tmp_path) run_id = _overview_top_id(db) gif = render_animation_gif(db, run_id) assert gif is not None and gif[:6] in (b"GIF87a", b"GIF89a")