Download backend/app/export.py from SevdanurGenc/LLM-Automated-Test-Generation-Agent: direct link, hf CLI and curl.
- Browser
- Download file 42.6 kB
-
https://huggingface.co/SevdanurGenc/LLM-Automated-Test-Generation-Agent/resolve/main/backend/app/export.py
- Command line
-
hf download hf://SevdanurGenc/LLM-Automated-Test-Generation-Agent/backend/app/export.py
-
curl -L -o export.py https://huggingface.co/SevdanurGenc/LLM-Automated-Test-Generation-Agent/resolve/main/backend/app/export.py
42.6 kB
| """Tek bir işin ayrıntılı, kendi başına açılabilen HTML raporu (Türkçe / İngilizce). | |
| Rapor hiçbir dış kaynağa bağlı değildir: grafikler satır içi SVG, stiller gömülüdür. Tarayıcıda açılır; | |
| "PDF olarak kaydet" düğmesi (ya da `?print=1`) yazdırma penceresini açar ve renkleri koruyarak A4 PDF üretir. | |
| """ | |
| from __future__ import annotations | |
| import datetime as dt | |
| import html | |
| import math | |
| import re | |
| from collections import defaultdict | |
| from typing import Any, Optional | |
| from .config import settings | |
| from .models import Analysis, JobState | |
| T: dict[str, dict[str, str]] = { | |
| "tr": { | |
| "brand": "TestAjanı", "doc": "Test Üretim Raporu", "print": "PDF olarak kaydet", "generated": "Oluşturulma", | |
| "lang": "Dil", "kind": "Test türü", "model": "Model", "duration": "Süre", "sec": "sn", "min": "dk", | |
| "stop": "Durma nedeni", "status": "Durum", | |
| "st.done": "Tamamlandı", "st.failed": "Hata", "st.cancelled": "İptal edildi", "st.running": "Sürüyor", "st.queued": "Sırada", | |
| "glance": "Bir bakışta", "passed": "Geçen testler", "coverage": "Satır kapsama", "mutation": "Mutasyon skoru", | |
| "rounds": "Doğrulama turu", | |
| "passed.d": "{p} / {t} test özgün kodda geçti", "coverage.d": "Kod satırlarının ne kadarı testlerle çalıştırıldı", | |
| "mutation.d": "Koda bilerek eklenen küçük hataların ne kadarını testler yakaladı", "rounds.d": "Yaz → çalıştır → düzelt döngüsü", | |
| "v.strong": "Güçlü test paketi", "v.good": "İyi test paketi", "v.weak": "Geliştirilmesi gereken test paketi", | |
| "v.none": "Doğrulanmış test paketi yok", | |
| "v.strong.d": "Testlerin hepsi geçiyor; kodun büyük bölümü çalıştırılıyor ve eklenen hataların çoğu yakalanıyor.", | |
| "v.good.d": "Testler geçiyor ve kodun çoğunu kapsıyor; bazı hatalar gözden kaçabilir — kaçan mutantlara bakın.", | |
| "v.weak.d": "Testler kodun önemli kısımlarını sınamıyor ya da hataları yakalamakta zayıf; aşağıdaki öneriler yardımcı olur.", | |
| "v.none.d": "Bu koşuda derlenip çalışan bir test dosyası elde edilemedi.", | |
| "glossary": "Bu sayılar ne anlama geliyor?", | |
| "g.cov": "Satır kapsama: testler çalışırken kodun yüzde kaçının en az bir kez yürütüldüğü. Yüksek kapsama, testin her şeyi doğru kontrol ettiği anlamına gelmez.", | |
| "g.mut": "Mutasyon skoru: koda küçük hatalar (ör. < yerine <=) tek tek eklenir; testlerden en az biri başarısız olursa hata “yakalandı” sayılır. Testlerin gerçekten hata bulma gücünü ölçer.", | |
| "g.bug": "Olası hata: bir test başarısız olduğunda ajan bunun kodun hatası olduğunu iddia edebilir; bağımsız hakem onaylarsa burada listelenir.", | |
| "toc": "İçindekiler", | |
| "explain": "Kod yorumu", "units": "Birimler", "sig": "İmza", "purpose": "Amaç", "branches": "Dallanma", | |
| "edge": "Uç durumlar", "risks": "Analistin şüphelendiği noktalar", "plan": "Test planı", | |
| "findings": "Bulgular", "bugs": "Testlerin ortaya çıkardığı olası hatalar", "rejected": "Hakemin reddettiği hata bildirimleri", | |
| "nofind": "Kodda hata bildirilmedi. Tüm testler, kodun belgelenmiş davranışıyla uyumlu.", | |
| "process": "Ajan süreci", "round": "Tur", "pass_l": "geçti", "fail_l": "başarısız", "cov_l": "kapsama", | |
| "compile_err": "derlenemedi", "mutfix": "Hedefli mutasyon turu", "mutfix.d": "Kaçan mutantlar ajana gösterildi; {n} yeni test eklendi.", | |
| "before": "önce", "after": "sonra", | |
| "tests": "Üretilen testler", "results": "Test sonuçları", "covmap": "Kapsama haritası", | |
| "covna": "Bu koşucu için satır kapsaması ölçülmüyor (Web UI veya JaCoCo’suz Java).", "hit": "çalıştırıldı", | |
| "miss": "hiç çalışmadı", "mutants": "Mutasyon testi", "line": "Satır", "op": "Operatör", "change": "Değişiklik", | |
| "result": "Sonuç", "byop": "Operatöre göre", "total": "Toplam", | |
| "killed": "yakalandı", "survived": "kaçtı", "invalid": "geçersiz", "timeout": "zaman aşımı", "pending": "…", | |
| "uncovered": "kapsanmayan satır", "tips": "Öneriler", | |
| "tip.cov": "Kapsanmayan satırlar (kırmızı) için test ekleyin; özellikle hata dalları ve sınır değerleri.", | |
| "tip.mut": "Kaçan mutantların satırlarına bakın: o satırdaki karşılaştırmayı veya hesabı tam değeriyle doğrulayan bir test ekleyin.", | |
| "tip.ok": "Test paketi iyi durumda. Kod değiştikçe testleri yeniden üretmek için aynı ayarlarla tekrar çalıştırabilirsiniz.", | |
| "run": "Çalışma bilgisi", "hardware": "Donanım", "gpu": "GPU payı", "speed": "Üretim hızı", "energy": "Enerji", | |
| "llm": "LLM çağrısı", "tools": "Araç çağrısı", "tokens": "token", "files": "Yardımcı dosyalar", | |
| "translated": "Kod yorumu model tarafından Türkçeye çevrildi.", | |
| "kinds": "unit=Birim,interface=Arayüz (sözleşme),ui=Web UI", "rule": "kural", "judge": "hakem ajan", | |
| "cfg": "Ayarlar", "cfg.rounds": "En çok tur", "cfg.target": "Kapsama hedefi", "cfg.mut": "Mutasyon testi", | |
| "cfg.mutround": "Mutant turu", "cfg.device": "Hesaplama cihazı", "cfg.provider": "Sağlayıcı", "cfg.temp": "Sıcaklık", | |
| "cfg.abl": "Kapatılan mekanizmalar", "cfg.prune": "Başarısız testleri ayıkla", "on": "açık", "off": "kapalı", | |
| "cfg.mutants": "en çok {n} mutant", "cfg.cloud": "bulut (sağlayıcının sunucuları)", "dev.auto": "Otomatik", | |
| "dev.gpu": "GPU", "dev.cpu": "CPU", "tok.in": "Girdi token", "tok.out": "Çıktı token", | |
| "cfg.mutround.na": "çalışmadı (kaçan mutant yok ya da paket yeşil değil)", "cfg.mutround.done": "çalıştı", | |
| "footer": "TestAjanı ile üretildi. Testler gerçek derleyici ve test koşucusunda, yalıtılmış ortamda doğrulanmıştır.", | |
| }, | |
| "en": { | |
| "brand": "TestAjanı", "doc": "Test Generation Report", "print": "Save as PDF", "generated": "Generated", | |
| "lang": "Language", "kind": "Test type", "model": "Model", "duration": "Duration", "sec": "s", "min": "min", | |
| "stop": "Stop reason", "status": "Status", | |
| "st.done": "Completed", "st.failed": "Error", "st.cancelled": "Cancelled", "st.running": "Running", "st.queued": "Queued", | |
| "glance": "At a glance", "passed": "Passing tests", "coverage": "Line coverage", "mutation": "Mutation score", | |
| "rounds": "Verification rounds", | |
| "passed.d": "{p} of {t} tests pass on the original code", "coverage.d": "Share of code lines executed by the tests", | |
| "mutation.d": "Share of deliberately injected small faults caught by the tests", "rounds.d": "Write → run → repair loop", | |
| "v.strong": "Strong test suite", "v.good": "Good test suite", "v.weak": "Test suite needs improvement", | |
| "v.none": "No verified test suite", | |
| "v.strong.d": "All tests pass, most of the code is executed and most injected faults are caught.", | |
| "v.good.d": "Tests pass and cover most of the code; some faults may slip through — check the surviving mutants.", | |
| "v.weak.d": "The tests miss important parts of the code or are weak at catching faults; the suggestions below help.", | |
| "v.none.d": "No test file that compiles and runs was obtained in this run.", | |
| "glossary": "What do these numbers mean?", | |
| "g.cov": "Line coverage: the percentage of code lines executed at least once while the tests run. High coverage does not mean everything is checked correctly.", | |
| "g.mut": "Mutation score: small faults (e.g. <= instead of <) are injected one at a time; a fault counts as “killed” if at least one test fails. It measures the real fault-finding power of the tests.", | |
| "g.bug": "Possible bug: when a test fails, the agent may claim the code is wrong; if the independent judge agrees, it is listed here.", | |
| "toc": "Contents", | |
| "explain": "Code explanation", "units": "Units", "sig": "Signature", "purpose": "Purpose", "branches": "Branches", | |
| "edge": "Edge cases", "risks": "Points the analyst found suspicious", "plan": "Test plan", | |
| "findings": "Findings", "bugs": "Possible bugs revealed by the tests", "rejected": "Bug reports rejected by the judge", | |
| "nofind": "No bug was reported in the code. All tests agree with the code’s documented behaviour.", | |
| "process": "Agent process", "round": "Round", "pass_l": "passed", "fail_l": "failed", "cov_l": "coverage", | |
| "compile_err": "did not compile", "mutfix": "Targeted mutation round", "mutfix.d": "Surviving mutants were shown to the agent; {n} new tests were added.", | |
| "before": "before", "after": "after", | |
| "tests": "Generated tests", "results": "Test results", "covmap": "Coverage map", | |
| "covna": "Line coverage is not measured for this runner (Web UI or Java without JaCoCo).", "hit": "executed", | |
| "miss": "never executed", "mutants": "Mutation testing", "line": "Line", "op": "Operator", "change": "Change", | |
| "result": "Result", "byop": "By operator", "total": "Total", | |
| "killed": "killed", "survived": "survived", "invalid": "invalid", "timeout": "timeout", "pending": "…", | |
| "uncovered": "uncovered line", "tips": "Suggestions", | |
| "tip.cov": "Add tests for the uncovered (red) lines, especially error branches and boundary values.", | |
| "tip.mut": "Look at the lines of surviving mutants: add a test that checks the comparison or computation on that line with its exact value.", | |
| "tip.ok": "The test suite is in good shape. Re-run with the same settings to regenerate tests as the code changes.", | |
| "run": "Run details", "hardware": "Hardware", "gpu": "GPU share", "speed": "Generation speed", "energy": "Energy", | |
| "llm": "LLM calls", "tools": "Tool calls", "tokens": "tokens", "files": "Helper files", | |
| "translated": "The code explanation was translated into English by the model.", | |
| "kinds": "unit=Unit,interface=Interface (contract),ui=Web UI", "rule": "rule", "judge": "judge agent", | |
| "cfg": "Settings", "cfg.rounds": "Max rounds", "cfg.target": "Coverage target", "cfg.mut": "Mutation testing", | |
| "cfg.mutround": "Mutant round", "cfg.device": "Compute device", "cfg.provider": "Provider", "cfg.temp": "Temperature", | |
| "cfg.abl": "Disabled mechanisms", "cfg.prune": "Prune failing tests", "on": "on", "off": "off", | |
| "cfg.mutants": "up to {n} mutants", "cfg.cloud": "cloud (provider's servers)", "dev.auto": "Auto", | |
| "dev.gpu": "GPU", "dev.cpu": "CPU", "tok.in": "Input tokens", "tok.out": "Output tokens", | |
| "cfg.mutround.na": "did not run (no surviving mutants or suite not green)", "cfg.mutround.done": "ran", | |
| "footer": "Generated with TestAjanı. Tests were verified with a real compiler and test runner in an isolated sandbox.", | |
| }, | |
| } | |
| LANG_NAMES = {"python": "Python", "javascript": "JavaScript", "typescript": "TypeScript", "java": "Java"} | |
| CSS = """ | |
| :root{--ink:#172033;--ink2:#4f5b72;--ink3:#8590a6;--line:#dfe4ee;--paper:#f3f5fa;--card:#fff;--sunk:#f6f8fc; | |
| --sig:#3046c5;--sig2:#5b6cf0;--sigbg:#e7eafc;--hit:#16855f;--hitbg:#dcf3e9;--miss:#cf3f31;--missbg:#fde5e2; | |
| --warn:#a8650c;--warnbg:#fcf0d9;--grey:#c3cad8} | |
| *{box-sizing:border-box}html{-webkit-print-color-adjust:exact;print-color-adjust:exact} | |
| body{margin:0;background:var(--paper);color:var(--ink);font:14.5px/1.6 "Segoe UI",system-ui,-apple-system,Roboto,sans-serif} | |
| code,pre,.mono{font-family:"JetBrains Mono",ui-monospace,Consolas,Menlo,monospace;font-size:12.3px} | |
| .hero{background:linear-gradient(120deg,#1d2a7a 0%,#3046c5 55%,#5b6cf0 100%);color:#fff;padding:30px 0 70px} | |
| .wrap{max-width:1000px;margin:0 auto;padding:0 24px} | |
| .brandrow{display:flex;align-items:center;gap:10px;font-weight:700;letter-spacing:.02em;opacity:.95} | |
| .logo{width:26px;height:26px;border-radius:7px;background:rgba(255,255,255,.18);display:grid;place-items:center} | |
| .doc{font-size:12px;text-transform:uppercase;letter-spacing:.14em;opacity:.8;margin-left:auto} | |
| .hero h1{font-size:28px;line-height:1.25;margin:18px 0 10px;font-weight:700} | |
| .chips{display:flex;flex-wrap:wrap;gap:8px}.chip{background:rgba(255,255,255,.14);border:1px solid rgba(255,255,255,.22); | |
| border-radius:999px;padding:3px 11px;font-size:12.5px}.chip b{font-weight:600} | |
| .actions{margin-top:16px;display:flex;gap:8px}.btn{border:0;border-radius:8px;padding:8px 14px;font:inherit;font-size:13px; | |
| font-weight:600;cursor:pointer;background:#fff;color:#1d2a7a} | |
| main.wrap{margin-top:-46px} | |
| .card{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:22px 24px;margin-bottom:18px; | |
| box-shadow:0 1px 2px rgba(20,30,60,.04),0 6px 22px rgba(20,30,60,.05)} | |
| .verdict{display:flex;gap:16px;align-items:center;border-left:6px solid var(--hit)} | |
| .verdict.good{border-left-color:var(--sig)}.verdict.weak{border-left-color:var(--warn)}.verdict.none{border-left-color:var(--miss)} | |
| .verdict .vi{width:46px;height:46px;border-radius:50%;display:grid;place-items:center;font-size:22px;font-weight:700;flex:none; | |
| background:var(--hitbg);color:var(--hit)}.verdict.good .vi{background:var(--sigbg);color:var(--sig)} | |
| .verdict.weak .vi{background:var(--warnbg);color:var(--warn)}.verdict.none .vi{background:var(--missbg);color:var(--miss)} | |
| .verdict h2{margin:0;font-size:19px;border:0;padding:0}.verdict p{margin:2px 0 0;color:var(--ink2)} | |
| .kpis{display:grid;grid-template-columns:repeat(4,1fr);gap:14px} | |
| .kpi{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:16px;display:flex;gap:12px;align-items:center} | |
| .kpi .v{font-size:22px;font-weight:700;line-height:1.1}.kpi .k{font-weight:600;font-size:13px}.kpi .d{color:var(--ink3);font-size:12px;line-height:1.35} | |
| .big{font-size:30px;font-weight:700;color:var(--sig);width:64px;text-align:center} | |
| h2{font-size:18px;margin:0 0 14px;padding-bottom:8px;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:10px} | |
| h2 .n{width:26px;height:26px;border-radius:7px;background:var(--sigbg);color:var(--sig);display:grid;place-items:center;font-size:13px} | |
| h3{font-size:14.5px;margin:18px 0 8px} | |
| .glossary{display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-top:14px} | |
| .glossary div{background:var(--sunk);border-radius:10px;padding:10px 12px;font-size:12.5px;color:var(--ink2)} | |
| .toc{columns:2;font-size:13.5px;margin:0;padding-left:18px}.toc a{color:var(--sig);text-decoration:none} | |
| table{border-collapse:collapse;width:100%;font-size:13px}th,td{border-bottom:1px solid var(--line);padding:7px 9px;text-align:left;vertical-align:top} | |
| th{color:var(--ink2);font-weight:600;font-size:12px;text-transform:uppercase;letter-spacing:.04em;background:var(--sunk)} | |
| td.num,th.num{text-align:right;font-variant-numeric:tabular-nums} | |
| .pill{display:inline-block;border-radius:999px;padding:1px 9px;font-size:11.5px;font-weight:600} | |
| .p-ok{background:var(--hitbg);color:var(--hit)}.p-bad{background:var(--missbg);color:var(--miss)} | |
| .p-warn{background:var(--warnbg);color:var(--warn)}.p-grey{background:var(--sunk);color:var(--ink3)} | |
| .box{border-left:4px solid var(--miss);background:var(--missbg);padding:9px 13px;border-radius:8px;margin:7px 0} | |
| .box.w{border-color:var(--warn);background:var(--warnbg)}.box.ok{border-color:var(--hit);background:var(--hitbg)} | |
| .box.i{border-color:var(--sig);background:var(--sigbg)} | |
| pre{background:#0f172a;color:#e2e8f0;border-radius:10px;padding:14px 16px;overflow-x:auto;white-space:pre-wrap;word-break:break-word;line-height:1.5} | |
| .src{border:1px solid var(--line);border-radius:10px;overflow:hidden;font-family:"JetBrains Mono",ui-monospace,Consolas,monospace;font-size:11.8px} | |
| .src div{display:grid;grid-template-columns:44px 20px 1fr;white-space:pre-wrap;word-break:break-word} | |
| .src .ln{color:var(--ink3);text-align:right;padding-right:8px;background:var(--sunk)}.src .h{background:var(--hitbg)}.src .m{background:var(--missbg)} | |
| .legend{font-size:12px;color:var(--ink2);margin:0 0 8px;display:flex;flex-wrap:wrap;gap:14px;align-items:center} | |
| .legend i{display:inline-block;width:11px;height:11px;border-radius:3px;margin-right:5px;vertical-align:-1px} | |
| .grid2{display:grid;grid-template-columns:1fr 1fr;gap:18px;align-items:start} | |
| .bar{height:8px;border-radius:4px;background:var(--sunk);overflow:hidden;min-width:60px}.bar span{display:block;height:100%} | |
| .case{display:grid;grid-template-columns:22px 1fr;gap:6px;padding:4px 0;border-bottom:1px dashed var(--line)} | |
| .ok{color:var(--hit)}.bad{color:var(--miss)}.warn{color:var(--warn)}.muted{color:var(--ink3)} | |
| .facts{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}.facts div{background:var(--sunk);border-radius:10px;padding:10px 12px} | |
| .facts b{display:block;font-size:16px}.facts span{color:var(--ink3);font-size:12px} | |
| footer{color:var(--ink3);font-size:12px;text-align:center;padding:10px 0 40px} | |
| svg text{font-family:"Segoe UI",system-ui,sans-serif} | |
| @media (max-width:760px){.kpis,.glossary,.facts{grid-template-columns:1fr 1fr}.grid2{grid-template-columns:1fr}.toc{columns:1}} | |
| @page{size:A4;margin:12mm 11mm 14mm} | |
| @media print{ | |
| html,body{background:#fff}body{font-size:11.5px;line-height:1.5} | |
| .actions{display:none}.wrap{max-width:none;padding:0 2mm} | |
| .hero{padding:14px 0 46px}.hero h1{font-size:22px;margin:10px 0 8px}.chip{font-size:11px} | |
| main.wrap{margin-top:-36px} | |
| .kpis{grid-template-columns:repeat(4,1fr);gap:8px}.kpi{padding:10px;gap:8px}.kpi .v{font-size:17px}.kpi .k{font-size:11px}.kpi .d{font-size:9.5px}.kpi svg{width:44px;height:44px;flex:none} | |
| .glossary{grid-template-columns:1fr 1fr 1fr}.facts{grid-template-columns:repeat(3,1fr)} | |
| .card{box-shadow:none;padding:14px 16px;margin-bottom:10px;break-inside:avoid-page}.card.long{break-inside:auto} | |
| pre,.src{break-inside:auto}h2{break-after:avoid;font-size:15px}h3{break-after:avoid} | |
| /* Yatay kaydırma kağıtta yok: kaydırmalı kapsayıcılar açılır, tablolar sayfa genişliğine sığar */ | |
| [style*="overflow-x"],[style*="overflow:auto"],.scroll{overflow:visible!important;max-height:none!important} | |
| ::-webkit-scrollbar{display:none} | |
| table{font-size:9.6px;width:100%}th,td{padding:4px 5px;overflow-wrap:break-word} | |
| th{font-size:8.6px;letter-spacing:.02em;white-space:normal}td.num{white-space:nowrap} | |
| code,.mono{font-size:9.4px;overflow-wrap:anywhere;white-space:normal} | |
| tr,.case,.box,.kpi,.facts div,.glossary div{break-inside:avoid} | |
| thead{display:table-header-group} | |
| .bar{min-width:36px!important}svg{max-width:100%;height:auto} | |
| .src{font-size:9.6px}.src div{grid-template-columns:34px 14px 1fr} | |
| pre{font-size:9.6px;padding:10px 12px} | |
| } | |
| """ | |
| def _e(s: Any) -> str: | |
| return html.escape(str(s if s is not None else "")) | |
| def _inline(s: str) -> str: | |
| s = _e(s) | |
| s = re.sub(r"\*\*([^*]+)\*\*", r"<strong>\1</strong>", s) | |
| return re.sub(r"`([^`]+)`", r"<code>\1</code>", s) | |
| def _markdown(text: str) -> str: | |
| out, items = [], [] | |
| for raw in (text or "").split("\n"): | |
| line = raw.strip() | |
| m = re.match(r"^([-*]|\d+[.)])\s+(.*)$", line) | |
| if m: | |
| items.append(f"<li>{_inline(m.group(2))}</li>") | |
| continue | |
| if items: | |
| out.append("<ul>" + "".join(items) + "</ul>") | |
| items = [] | |
| if line: | |
| h = re.match(r"^#{1,4}\s+(.*)$", line) | |
| out.append(f"<h3>{_inline(h.group(1))}</h3>" if h else f"<p>{_inline(line)}</p>") | |
| if items: | |
| out.append("<ul>" + "".join(items) + "</ul>") | |
| return "\n".join(out) | |
| OP_EN = {"NOT kaldır": "NOT removal"} | |
| def op_name(op: str, lang: str) -> str: | |
| """Mutasyon işleci adı (yalnızca "NOT kaldır" dile bağlıdır).""" | |
| return OP_EN.get(op, op) if lang == "en" else op | |
| def short_title(text: str, n: int = 90) -> str: | |
| """Özetten başlık: ilk cümle kısaysa o, değilse sözcük sınırında kesilip "…" eklenir.""" | |
| t = " ".join((text or "").split()) | |
| first = re.split(r"(?<=[.!?])\s", t, maxsplit=1)[0] | |
| if len(first) <= n + 20: | |
| return first.rstrip(".") | |
| cut = t[:n] | |
| if " " in cut: | |
| cut = cut[:cut.rfind(" ")] | |
| return cut.rstrip(" ,;:-—") + "…" | |
| def _pct(v: Optional[float], lang: str, d: int = 1) -> str: | |
| if v is None: | |
| return "—" | |
| s = f"{v:.{d}f}" | |
| return f"%{s.replace('.', ',')}" if lang == "tr" else f"{s}%" | |
| def _num(v: float, lang: str, d: int = 0) -> str: | |
| s = f"{v:,.{d}f}" | |
| return s.replace(",", "X").replace(".", ",").replace("X", ".") if lang == "tr" else s | |
| def _ring(v: Optional[float], color: str, size: int = 58) -> str: | |
| """Yüzde halkası (satır içi SVG).""" | |
| r = size / 2 - 6 | |
| c = 2 * math.pi * r | |
| frac = max(0.0, min(1.0, (v or 0) / 100)) | |
| return (f'<svg width="{size}" height="{size}" viewBox="0 0 {size} {size}" aria-hidden="true">' | |
| f'<circle cx="{size/2}" cy="{size/2}" r="{r:.1f}" fill="none" stroke="#e8ecf4" stroke-width="7"/>' | |
| f'<circle cx="{size/2}" cy="{size/2}" r="{r:.1f}" fill="none" stroke="{color}" stroke-width="7" ' | |
| f'stroke-linecap="round" stroke-dasharray="{c * frac:.1f} {c:.1f}" transform="rotate(-90 {size/2} {size/2})"/></svg>') | |
| def _donut(parts: list[tuple[str, int, str]], size: int = 150) -> str: | |
| total = sum(n for _, n, _ in parts) or 1 | |
| r, cx = size / 2 - 14, size / 2 | |
| c = 2 * math.pi * r | |
| off, segs = 0.0, [] | |
| for _, n, col in parts: | |
| if not n: | |
| continue | |
| ln = c * n / total | |
| segs.append(f'<circle cx="{cx}" cy="{cx}" r="{r:.1f}" fill="none" stroke="{col}" stroke-width="22" ' | |
| f'stroke-dasharray="{ln:.2f} {c:.2f}" stroke-dashoffset="{-off:.2f}" transform="rotate(-90 {cx} {cx})"/>') | |
| off += ln | |
| return (f'<svg width="{size}" height="{size}" viewBox="0 0 {size} {size}" role="img">' | |
| f'<circle cx="{cx}" cy="{cx}" r="{r:.1f}" fill="none" stroke="#eef1f7" stroke-width="22"/>' + "".join(segs) | |
| + "</svg>") | |
| def _rounds_chart(iters: list, L: dict[str, str], lang: str) -> str: | |
| """Tur başına geçen/başarısız test (yığılmış çubuk) ve kapsama (çizgi).""" | |
| if not iters: | |
| return "" | |
| W, H, left, top, bottom = 640, 210, 40, 16, 38 | |
| n = len(iters) | |
| slot = (W - left - 10) / n | |
| bw = min(46, slot * 0.55) | |
| mx = max(1, max(i.total for i in iters)) | |
| ih = H - top - bottom | |
| g = [] | |
| for k in range(5): | |
| y = top + ih * k / 4 | |
| g.append(f'<line x1="{left}" x2="{W-6}" y1="{y:.1f}" y2="{y:.1f}" stroke="#eef1f7"/>' | |
| f'<text x="{left-6}" y="{y+4:.1f}" font-size="10" fill="#8590a6" text-anchor="end">{100 - 25*k}</text>') | |
| pts = [] | |
| for idx, it in enumerate(iters): | |
| x = left + slot * idx + (slot - bw) / 2 | |
| if it.compiled and it.total: | |
| hp = ih * it.passed / mx | |
| hf = ih * it.failed / mx | |
| g.append(f'<rect x="{x:.1f}" y="{top+ih-hp:.1f}" width="{bw:.1f}" height="{hp:.1f}" rx="3" fill="#16855f"><title>{it.passed} {L["pass_l"]}</title></rect>') | |
| if hf: | |
| g.append(f'<rect x="{x:.1f}" y="{top+ih-hp-hf:.1f}" width="{bw:.1f}" height="{hf:.1f}" rx="3" fill="#cf3f31"><title>{it.failed} {L["fail_l"]}</title></rect>') | |
| g.append(f'<text x="{x+bw/2:.1f}" y="{top+ih-hp-hf-4:.1f}" font-size="10.5" fill="#4f5b72" text-anchor="middle">{it.passed}/{it.total}</text>') | |
| else: | |
| g.append(f'<rect x="{x:.1f}" y="{top+ih-14:.1f}" width="{bw:.1f}" height="14" rx="3" fill="#c3cad8"/>' | |
| f'<text x="{x+bw/2:.1f}" y="{top+ih-18:.1f}" font-size="9.5" fill="#cf3f31" text-anchor="middle">{_e(L["compile_err"])}</text>') | |
| label = f'{L["round"]} {idx+1}' + (" ★" if it.action == "mutfix" else "") | |
| g.append(f'<text x="{x+bw/2:.1f}" y="{H-bottom+16:.1f}" font-size="11" fill="#4f5b72" text-anchor="middle">{_e(label)}</text>') | |
| if it.coverage is not None and it.compiled: | |
| pts.append((x + bw / 2, top + ih * (1 - it.coverage / 100))) | |
| if len(pts) > 1: | |
| g.append('<polyline fill="none" stroke="#3046c5" stroke-width="2.4" stroke-linejoin="round" points="' | |
| + " ".join(f"{a:.1f},{b:.1f}" for a, b in pts) + '"/>') | |
| for a, b in pts: | |
| g.append(f'<circle cx="{a:.1f}" cy="{b:.1f}" r="4" fill="#fff" stroke="#3046c5" stroke-width="2.4"/>') | |
| legend = (f'<div class="legend"><span><i style="background:#16855f"></i>{_e(L["pass_l"])}</span>' | |
| f'<span><i style="background:#cf3f31"></i>{_e(L["fail_l"])}</span>' | |
| f'<span><i style="background:#3046c5;height:3px;vertical-align:3px"></i>{_e(L["cov_l"])} (%)</span>' | |
| + (f'<span>★ {_e(L["mutfix"])}</span>' if any(i.action == "mutfix" for i in iters) else "") + "</div>") | |
| return legend + f'<svg viewBox="0 0 {W} {H}" width="100%" role="img">' + "".join(g) + "</svg>" | |
| def _verdict(run, mut: Optional[float]) -> str: | |
| if not run or not run.compiled or not run.total: | |
| return "none" | |
| green = run.failed + run.errors == 0 | |
| cov = run.coverage.percent | |
| if green and (cov is None or cov >= 85) and (mut is None or mut >= 70): | |
| return "strong" | |
| if green and (cov is None or cov >= 70) and (mut is None or mut >= 50): | |
| return "good" | |
| return "weak" | |
| def render_job(job: JobState, lang: str = "tr", auto_print: bool = False) -> str: | |
| lang = lang if lang in T else "tr" | |
| L = T[lang] | |
| kinds = dict(x.split("=") for x in L["kinds"].split(",")) | |
| r = job.result | |
| req = job.request | |
| run = r.final_run if r else None | |
| tr_ = r.translations.get(lang) if r and lang != req.lang else None | |
| a = Analysis(**tr_["analysis"]) if tr_ else (r.analysis if r else Analysis()) | |
| bug_reasons = tr_.get("bug_reasons", []) if tr_ else [] | |
| verdicts = tr_.get("rejected_verdicts", []) if tr_ else [] | |
| title = req.title or (short_title(a.summary) if a.summary else L["doc"]) | |
| title = re.sub(r"^\[(?:bench|oto|auto)\]\s*", "", title) | |
| model = req.model or settings.default_model | |
| when = dt.datetime.fromtimestamp(job.created_at).strftime("%Y-%m-%d %H:%M") | |
| c = (r.compute if r else {}) or {} | |
| mut = r.mutation.score if r and r.mutation else None | |
| p: list[str] = [f'<!doctype html><html lang="{lang}"><head><meta charset="utf-8">' | |
| f'<meta name="viewport" content="width=device-width,initial-scale=1">' | |
| f"<title>{_e(L['doc'])} — {_e(title)}</title><style>{CSS}</style></head><body>"] | |
| # ------------------------------------------------------------ başlık bandı | |
| chips = [(L["lang"], LANG_NAMES.get(req.language, req.language)), (L["kind"], kinds.get(req.test_kind, req.test_kind)), | |
| (L["model"], model), (L["generated"], when)] | |
| if r: | |
| chips.append((L["duration"], f"{r.duration_s/60:.1f} {L['min']}" if r.duration_s >= 120 else f"{r.duration_s:.0f} {L['sec']}")) | |
| chips.append((L["status"], L.get(f"st.{job.status}", job.status))) | |
| p.append('<header class="hero"><div class="wrap"><div class="brandrow"><span class="logo">' | |
| '<svg width="15" height="15" viewBox="0 0 32 32"><path d="M7 17l6 6 12-14" stroke="#fff" stroke-width="4" ' | |
| 'fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg></span>' | |
| f'{_e(L["brand"])}<span class="doc">{_e(L["doc"])}</span></div><h1>{_e(title)}</h1><div class="chips">' | |
| + "".join(f'<span class="chip">{_e(k)}: <b>{_e(v)}</b></span>' for k, v in chips) | |
| + f'</div><div class="actions"><button class="btn" onclick="window.print()">⤓ {_e(L["print"])}</button></div>' | |
| '</div></header><main class="wrap">') | |
| # ------------------------------------------------------------ bir bakışta | |
| vd = _verdict(run, mut) | |
| icon = {"strong": "✓", "good": "✓", "weak": "!", "none": "✕"}[vd] | |
| p.append(f'<section class="card verdict {vd}"><div class="vi">{icon}</div><div><h2>{_e(L["v." + vd])}</h2>' | |
| f'<p>{_e(L["v." + vd + ".d"])}</p></div></section>') | |
| if r: | |
| pass_rate = 100 * run.passed / run.total if run and run.total else None | |
| n_iter = len([i for i in r.iterations if i.action != "mutfix"]) | |
| cov = run.coverage.percent if run else None | |
| col = lambda v, good, ok: "#16855f" if (v or 0) >= good else ("#3046c5" if (v or 0) >= ok else "#a8650c") # noqa: E731 | |
| kp = [ | |
| (_ring(pass_rate, col(pass_rate, 100, 90)), f"{run.passed}/{run.total}" if run and run.compiled else "—", | |
| L["passed"], L["passed.d"].format(p=run.passed if run else 0, t=run.total if run else 0)), | |
| (_ring(cov, col(cov, 85, 70)), _pct(cov, lang), L["coverage"], L["coverage.d"]), | |
| (_ring(mut, col(mut, 70, 50)), _pct(mut, lang), L["mutation"], L["mutation.d"]), | |
| (f'<div class="big">{n_iter}</div>', "", L["rounds"], L["rounds.d"]), | |
| ] | |
| p.append('<section class="kpis" style="margin-bottom:18px">' + "".join( | |
| f'<div class="kpi">{g}<div>{f"<div class=v>{_e(v)}</div>" if v else ""}<div class="k">{_e(k)}</div>' | |
| f'<div class="d">{_e(d)}</div></div></div>' for g, v, k, d in kp) + "</section>") | |
| p.append(f'<section class="card"><h3 style="margin-top:0">{_e(L["glossary"])}</h3><div class="glossary">' | |
| f'<div>{_e(L["g.cov"])}</div><div>{_e(L["g.mut"])}</div><div>{_e(L["g.bug"])}</div></div></section>') | |
| secs = [("s1", L["explain"]), ("s2", L["findings"]), ("s3", L["process"]), ("s4", L["tests"]), | |
| ("s5", L["covmap"]), ("s6", L["mutants"]), ("s7", L["run"])] | |
| p.append(f'<section class="card"><h3 style="margin-top:0">{_e(L["toc"])}</h3><ol class="toc">' | |
| + "".join(f'<li><a href="#{i}">{_e(t)}</a></li>' for i, t in secs) + "</ol></section>") | |
| # ---------------------------------------------------------- 1 kod yorumu | |
| p.append(f'<section class="card long" id="s1"><h2><span class="n">1</span>{_e(L["explain"])}</h2>') | |
| if tr_: | |
| p.append(f'<div class="box i" style="font-size:12.5px">{_e(L["translated"])}</div>') | |
| if a.summary: | |
| p.append(f'<p style="font-size:15.5px;font-weight:600">{_inline(a.summary)}</p>') | |
| p.append(_markdown(a.explanation)) | |
| if a.units: | |
| p.append(f"<h3>{_e(L['units'])}</h3><table><tr><th>{_e(L['sig'])}</th><th>{_e(L['purpose'])}</th>" | |
| f"<th class=num>{_e(L['branches'])}</th></tr>" + "".join( | |
| f"<tr><td><code>{_e(u.get('signature') or u.get('name') or '')}</code></td>" | |
| f"<td>{_e(u.get('purpose', ''))}</td><td class=num>{_e(u.get('branches', '—'))}</td></tr>" | |
| for u in a.units) + "</table>") | |
| if a.edge_cases or a.test_plan: | |
| p.append('<div class="grid2">') | |
| p.append(f"<div><h3>{_e(L['edge'])}</h3><ul>" + "".join(f"<li>{_inline(x)}</li>" for x in a.edge_cases) + "</ul></div>") | |
| p.append(f"<div><h3>{_e(L['plan'])}</h3><ol>" + "".join(f"<li>{_inline(x)}</li>" for x in a.test_plan) + "</ol></div>") | |
| p.append("</div>") | |
| p.append("</section>") | |
| # ---------------------------------------------------------- 2 bulgular | |
| p.append(f'<section class="card" id="s2"><h2><span class="n">2</span>{_e(L["findings"])}</h2>') | |
| if not (r.suspected_bugs or r.rejected_bug_reports or a.risks): | |
| p.append(f'<div class="box ok">{_e(L["nofind"])}</div>') | |
| if r.suspected_bugs: | |
| p.append(f"<h3>{_e(L['bugs'])}</h3>" + "".join( | |
| f'<div class="box"><strong>{_e(b["test"])}</strong>: ' | |
| f'{_e(bug_reasons[i] if i < len(bug_reasons) else b["reason"])}</div>' for i, b in enumerate(r.suspected_bugs))) | |
| if r.rejected_bug_reports: | |
| p.append(f"<h3>{_e(L['rejected'])}</h3>" + "".join( | |
| f'<div class="box w"><strong>{_e(b["test"])}</strong>: {_e(verdicts[i] if i < len(verdicts) else b["verdict"])} ' | |
| f'<span class="muted">({_e(L["rule"] if b.get("by") == "kural" else L["judge"])})</span></div>' | |
| for i, b in enumerate(r.rejected_bug_reports))) | |
| if a.risks: | |
| p.append(f"<h3>{_e(L['risks'])}</h3>" + "".join(f'<div class="box i">{_inline(x)}</div>' for x in a.risks)) | |
| p.append("</section>") | |
| # ---------------------------------------------------------- 3 süreç | |
| p.append(f'<section class="card" id="s3"><h2><span class="n">3</span>{_e(L["process"])}</h2>') | |
| p.append(_rounds_chart(r.iterations, L, lang)) | |
| stop = r.stop_reason_i18n.get(lang) or r.stop_reason | |
| if stop: | |
| p.append(f'<p class="muted" style="margin:6px 0 0">{_e(L["stop"])}: <b style="color:var(--ink)">{_e(stop)}</b></p>') | |
| mr = r.mutation_round or {} | |
| if mr.get("accepted"): | |
| b0, b1 = mr.get("score_before") or 0, mr.get("score_after") or 0 | |
| p.append(f'<div class="box ok" style="margin-top:12px"><strong>{_e(L["mutfix"])}:</strong> ' | |
| f'{_e(L["mutfix.d"].format(n=mr.get("tests_added", 0)))}' | |
| f'<div style="display:grid;grid-template-columns:70px 1fr 60px;gap:6px 10px;align-items:center;margin-top:8px">' | |
| f'<span class="muted">{_e(L["before"])}</span><div class="bar"><span style="width:{b0}%;background:#c3cad8"></span></div><b>{_pct(b0, lang)}</b>' | |
| f'<span class="muted">{_e(L["after"])}</span><div class="bar"><span style="width:{b1}%;background:#16855f"></span></div><b>{_pct(b1, lang)}</b>' | |
| "</div></div>") | |
| p.append("</section>") | |
| # ---------------------------------------------------------- 4 testler | |
| p.append(f'<section class="card long" id="s4"><h2><span class="n">4</span>{_e(L["tests"])} ' | |
| f'<code style="font-weight:400;font-size:12px">{_e(r.test_filename)}</code></h2>') | |
| if run and run.cases: | |
| p.append(f"<h3 style='margin-top:0'>{_e(L['results'])}</h3>") | |
| for cs in run.cases: | |
| ok = cs.status == "passed" | |
| sym, cls = ("✓", "ok") if ok else (("–", "muted") if cs.status == "skipped" else ("✕", "bad")) | |
| p.append(f'<div class="case"><b class="{cls}">{sym}</b><div><code>{_e(cs.name)}</code>' | |
| + (f"<pre style='margin:6px 0 2px;font-size:11.5px'>{_e(cs.message[:1200])}</pre>" if cs.message and not ok else "") | |
| + "</div></div>") | |
| p.append(f"<pre style='margin-top:14px'>{_e(r.test_code)}</pre></section>") | |
| # ---------------------------------------------------------- 5 kapsama | |
| p.append(f'<section class="card long" id="s5"><h2><span class="n">5</span>{_e(L["covmap"])}</h2>') | |
| if run and run.coverage.percent is None: | |
| p.append(f'<div class="box w">{_e(L["covna"])}</div>') | |
| hit = set(run.coverage.covered_lines) if run else set() | |
| miss = set(run.coverage.missed_lines) if run else set() | |
| marks: dict[int, str] = {} | |
| for m in (r.mutation.mutants if r.mutation else []): | |
| if m.status == "survived": | |
| marks[m.line] = '<span class="bad">○</span>' | |
| elif m.status in ("killed", "timeout") and m.line not in marks: | |
| marks[m.line] = '<span class="ok">●</span>' | |
| p.append(f'<div class="legend"><span><i style="background:var(--hitbg)"></i>{_e(L["hit"])}</span>' | |
| f'<span><i style="background:var(--missbg)"></i>{_e(L["miss"])}</span>' | |
| f'<span><b class="ok">●</b> {_e(L["killed"])}</span><span><b class="bad">○</b> {_e(L["survived"])}</span></div><div class="src">') | |
| for n, line in enumerate(req.code.split("\n"), 1): | |
| cls = "h" if n in hit else "m" if n in miss else "" | |
| p.append(f'<div class="{cls}"><span class="ln">{n}</span><span>{marks.get(n, "")}</span><span>{_e(line) or " "}</span></div>') | |
| p.append("</div></section>") | |
| # ---------------------------------------------------------- 6 mutasyon | |
| p.append(f'<section class="card long" id="s6"><h2><span class="n">6</span>{_e(L["mutants"])}</h2>') | |
| if r.mutation and r.mutation.mutants: | |
| rep = r.mutation | |
| parts = [(L["killed"], rep.killed, "#16855f"), (L["timeout"], rep.timeout, "#5b6cf0"), | |
| (L["survived"], rep.survived, "#cf3f31"), (L["invalid"], rep.invalid, "#c3cad8")] | |
| byop: dict[str, list[int]] = defaultdict(lambda: [0, 0]) | |
| for m in rep.mutants: | |
| if m.status in ("killed", "timeout", "survived"): | |
| byop[op_name(m.operator, lang)][1] += 1 | |
| byop[op_name(m.operator, lang)][0] += m.status != "survived" | |
| p.append('<div class="grid2"><div style="display:flex;gap:16px;align-items:center">' | |
| f'<div style="position:relative">{_donut(parts)}<div style="position:absolute;inset:0;display:grid;place-items:center;' | |
| f'font-weight:700;font-size:20px">{_pct(rep.score, lang, 0)}</div></div><div>' | |
| + "".join(f'<div><i style="display:inline-block;width:11px;height:11px;border-radius:3px;background:{col};margin-right:6px"></i>' | |
| f'{_e(lbl)}: <b>{n}</b></div>' for lbl, n, col in parts) | |
| + f'</div></div><div><h3 style="margin-top:0">{_e(L["byop"])}</h3><table>' | |
| + "".join(f'<tr><td>{_e(op)}</td><td style="width:45%"><div class="bar"><span style="width:{100*k/t:.0f}%;' | |
| f'background:{"#16855f" if k == t else "#3046c5" if k/t >= .5 else "#cf3f31"}"></span></div></td>' | |
| f'<td class=num>{k}/{t}</td></tr>' for op, (k, t) in sorted(byop.items())) | |
| + "</table></div></div>") | |
| p.append(f"<table style='margin-top:14px'><tr><th class=num>{_e(L['line'])}</th><th>{_e(L['op'])}</th>" | |
| f"<th>{_e(L['change'])}</th><th>{_e(L['result'])}</th></tr>") | |
| for m in sorted(rep.mutants, key=lambda m: (m.line, m.id)): | |
| pill = {"killed": "p-ok", "timeout": "p-ok", "survived": "p-bad"}.get(m.status, "p-grey") | |
| note = f'<div class="muted" style="font-size:11.5px">{_e(L["uncovered"] if m.note == "kapsanmayan satır" else m.note)}</div>' if m.note else "" | |
| p.append(f"<tr><td class=num>{m.line}</td><td>{_e(op_name(m.operator, lang))}</td><td><code>{_e(m.original)}</code><br>" | |
| f"<code style='color:var(--sig)'>{_e(m.mutated)}</code></td>" | |
| f'<td><span class="pill {pill}">{_e(L.get(m.status, m.status))}</span>{note}</td></tr>') | |
| p.append("</table>") | |
| else: | |
| p.append('<p class="muted">—</p>') | |
| # öneriler | |
| tips = [] | |
| if run and run.coverage.percent is not None and run.coverage.percent < 85: | |
| tips.append(L["tip.cov"]) | |
| if r.mutation and r.mutation.survived: | |
| tips.append(L["tip.mut"]) | |
| p.append(f'<h3>{_e(L["tips"])}</h3>' + "".join(f'<div class="box i">{_e(t_)}</div>' for t_ in (tips or [L["tip.ok"]]))) | |
| p.append("</section>") | |
| # ---------------------------------------------------------- 7 çalışma bilgisi | |
| facts = [(model, L["model"]), (f"{r.llm_calls}", L["llm"]), (f"{r.tool_calls}", L["tools"])] | |
| if c.get("gpu_percent") is not None: | |
| g = int(c["gpu_percent"]) | |
| facts.append((f"%{g}" if lang == "tr" else f"{g}%", L["gpu"])) | |
| if c.get("tokens_per_s"): | |
| facts.append((f"{_num(c['tokens_per_s'], lang, 1)} {L['tokens']}/{L['sec']}", L["speed"])) | |
| if c.get("energy_wh") is not None: | |
| facts.append((f"{_num(c['energy_wh'], lang, 2)} Wh", L["energy"])) | |
| facts.append((f"{r.duration_s:.0f} {L['sec']}", L["duration"])) | |
| t_in = sum(int(e.data.get("tokens_in") or 0) for e in job.events if e.type == "llm") | |
| t_out = sum(int(e.data.get("tokens_out") or 0) for e in job.events if e.type == "llm") | |
| if t_in or t_out: | |
| facts += [(_num(t_in, lang), L["tok.in"]), (_num(t_out, lang), L["tok.out"])] | |
| p.append(f'<section class="card" id="s7"><h2><span class="n">7</span>{_e(L["run"])}</h2><div class="facts">' | |
| + "".join(f"<div><b>{_e(v)}</b><span>{_e(k)}</span></div>" for v, k in facts) + "</div>") | |
| # Koşunun ayarları: sonucun hangi koşullarda alındığını rapordan okuyabilmek için | |
| provider = c.get("provider") or ("openai" if model.startswith("openai:") else "anthropic" if model.startswith("anthropic:") else "ollama") | |
| cloud = provider in ("openai", "anthropic") | |
| dev = c.get("device") or req.device | |
| if req.run_mutation and req.mutation_rounds and "mutfix" not in req.ablate: | |
| mr = f'{L["on"]} ({req.mutation_rounds}) · ' + (L["cfg.mutround.done"] if r.mutation_round else L["cfg.mutround.na"]) | |
| else: | |
| mr = L["off"] | |
| rows = [(L["cfg.rounds"], str(req.max_iterations or settings.max_iterations)), | |
| (L["cfg.target"], _pct(req.coverage_target if req.coverage_target is not None else settings.coverage_target, lang, 0)), | |
| (L["cfg.mut"], f'{L["on"]} · {L["cfg.mutants"].format(n=req.max_mutants if req.max_mutants is not None else settings.max_mutants)}' | |
| if req.run_mutation else L["off"]), | |
| (L["cfg.mutround"], mr), | |
| (L["cfg.prune"], L["on"] if req.prune_failing else L["off"]), | |
| (L["cfg.provider"], {"ollama": "Ollama", "openai": "OpenAI", "anthropic": "Claude"}.get(provider, provider)), | |
| (L["cfg.device"], L["cfg.cloud"] if cloud else L.get(f"dev.{dev}", L["dev.auto"]) if dev else L["dev.auto"]), | |
| (L["cfg.temp"], _num(req.temperature, lang, 1))] | |
| if req.ablate: | |
| rows.append((L["cfg.abl"], ", ".join(req.ablate))) | |
| p.append(f'<h3>{_e(L["cfg"])}</h3><table>' + "".join( | |
| f"<tr><td style='width:40%'>{_e(k)}</td><td><b>{_e(v)}</b></td></tr>" for k, v in rows) + "</table>") | |
| if req.extra_files: | |
| p.append(f'<p style="margin-top:12px">{_e(L["files"])}: ' + ", ".join(f"<code>{_e(n)}</code>" for n in req.extra_files) + "</p>") | |
| p.append("</section>") | |
| p.append(f"<footer>{_e(L['footer'])}</footer></main>") | |
| if auto_print: | |
| p.append("<script>window.addEventListener('load',()=>setTimeout(()=>window.print(),400));</script>") | |
| p.append("</body></html>") | |
| return "\n".join(p) | |