Skip to content

Commit 81d4784

Browse files
committed
Add artefact serving endpoint and UI enhancements
Introduces a /artefact FastAPI endpoint to securely serve local artefact files for development. Updates the static UI to proxy local artefact and report paths through the new endpoint, improving accessibility of generated reports and artefacts. Adds plugin descriptions in Turkish as a JSON template, enhances the report template with a modal for plugin details, and refines scorecard display. Updates test coverage for visual error handling. Registers new plugins in pyproject.toml.
1 parent 403f385 commit 81d4784

6 files changed

Lines changed: 210 additions & 17 deletions

File tree

patternlab/api.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
"""
99
from fastapi import FastAPI, UploadFile, File, BackgroundTasks, HTTPException, Body
1010
from fastapi.staticfiles import StaticFiles
11-
from fastapi.responses import RedirectResponse
11+
from fastapi.responses import RedirectResponse, FileResponse
1212
from fastapi.middleware.cors import CORSMiddleware
1313
from pydantic import BaseModel
1414
from typing import Optional, Dict, Any
@@ -96,6 +96,22 @@ async def report(job_id: str):
9696
return {"job_id": job_id, "status": entry["status"]}
9797
return {"job_id": job_id, "status": "error", "error": entry.get("error")}
9898

99+
@app.get("/artefact", include_in_schema=False)
100+
async def artefact(path: str):
101+
"""
102+
Serve an artefact file by absolute path (development helper).
103+
Security: only allows files inside the current workspace directory to be served.
104+
Usage: /artefact?path=/absolute/path/to/file.png
105+
"""
106+
# Resolve and restrict to workspace root for safety
107+
file_path = os.path.abspath(path)
108+
workspace_root = os.path.abspath(os.getcwd())
109+
if not file_path.startswith(workspace_root):
110+
raise HTTPException(status_code=403, detail="forbidden: path outside workspace")
111+
if not os.path.exists(file_path) or not os.path.isfile(file_path):
112+
raise HTTPException(status_code=404, detail="not_found")
113+
return FileResponse(file_path)
114+
99115
@app.get("/health")
100116
async def health():
101117
return {"status": "ok"}

patternlab/static_ui/index.html

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -153,25 +153,39 @@ <h2 style="margin-top:18px">Sonuç</h2>
153153
setStatus('Tamamlandı', false);
154154
const report = j.report || {};
155155
resultPre.textContent = JSON.stringify(report, null, 2);
156-
// If engine included html_report path in top-level, show iframe
156+
// If engine included html_report path in top-level, show iframe.
157+
// If the path looks like a local file (not an HTTP(S) URL), route it through
158+
// the API helper endpoint /artefact so browsers can fetch it via the server.
157159
if (report && report.html_report){
158160
const iframe = document.createElement('iframe');
159-
iframe.src = report.html_report;
161+
let src = report.html_report;
162+
// Treat as URL if it begins with http(s)://
163+
if (!/^https?:\/\//i.test(src)) {
164+
// Otherwise encode and route through artefact endpoint
165+
src = '/artefact?path=' + encodeURIComponent(src);
166+
}
167+
iframe.src = src;
160168
iframe.style.width = '100%';
161169
iframe.style.height = '500px';
162170
artefactsEl.appendChild(iframe);
163171
}
164-
// If artefacts listed as array `artefacts` with URLs, show images/links
172+
// If artefacts listed as array `artefacts` with URLs/paths, show images/links.
173+
// Non-HTTP paths will be proxied through /artefact?path=...
165174
if (report && report.artefacts && Array.isArray(report.artefacts)){
166175
report.artefacts.forEach(a=>{
167176
const aEl = document.createElement('div');
177+
// Normalize path: if it's not an http(s) URL, proxy via /artefact
178+
let href = a;
179+
if (!/^https?:\/\//i.test(href)) {
180+
href = '/artefact?path=' + encodeURIComponent(href);
181+
}
168182
if (/\.(png|jpg|jpeg|svg)$/i.test(a)){
169183
const img = document.createElement('img');
170-
img.src = a;
184+
img.src = href;
171185
aEl.appendChild(img);
172186
} else {
173187
const link = document.createElement('a');
174-
link.href = a;
188+
link.href = href;
175189
link.textContent = a;
176190
link.target = '_blank';
177191
aEl.appendChild(link);

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,12 @@ autocorrelation = "patternlab.plugins.autocorrelation:AutocorrelationTest"
4646
binary_matrix_rank = "patternlab.plugins.binary_matrix_rank:BinaryMatrixRankTest"
4747
block_frequency = "patternlab.plugins.block_frequency_test:BlockFrequencyTest"
4848
cusum = "patternlab.plugins.cusum:CumulativeSumsTest"
49+
dotplot = "patternlab.plugins.dotplot:DotplotPlugin"
4950
fft_placeholder = "patternlab.plugins.fft_placeholder:FFTPlaceholder"
5051
fft_spectral = "patternlab.plugins.fft_spectral:FFTSpectralTest"
5152
linear_complexity = "patternlab.plugins.linear_complexity:LinearComplexityTest"
5253
longest_run = "patternlab.plugins.longest_run:LongestRunOnesTest"
54+
lz_complexity = "patternlab.plugins.lz_complexity:LZComplexityPlugin"
5355
maurers_universal = "patternlab.plugins.maurers_universal:MaurersUniversalTest"
5456
monobit = "patternlab.plugins.monobit:MonobitTest"
5557
nist_dft_spectral = "patternlab.plugins.nist_dft_spectral:NISTDFTSpectralTest"

templates/descriptions.json

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
{
2+
"monobit": {
3+
"title": "Monobit (Frequency) Test",
4+
"short": "Bitlerin 0/1 dengesini kontrol eder.",
5+
"long": "Test, bit dizisindeki 0 ve 1 sayıları arasındaki oransal dengeyi değerlendirir. Aşırı sapma rasgele olmayanlığı gösterir.",
6+
"remediation": "Düşük p-değerleri tekrar eden bit kalıpları veya sabit bit akışı gösterir; veri kaynağını veya ön işleme (örneğin sıkıştırma) kontrol edin."
7+
},
8+
"runs": {
9+
"title": "Runs Test",
10+
"short": "Bitler arasındaki ardışık aynı değer (run) dağılımını kontrol eder.",
11+
"long": "Test, art arda gelen 0 veya 1 kümelerinin sayısını ve uzunluklarını değerlendirir; beklenenden fazla veya az run rasgeleliği bozar.",
12+
"remediation": "Sık tekrar eden alt diziler veya kalıp üretimi varsa, verinin kaynağını veya üretim sürecini inceleyin."
13+
},
14+
"block_frequency": {
15+
"title": "Block Frequency Test",
16+
"short": "Büyük bloklarda bit frekansını ölçer.",
17+
"long": "Veriyi sabit uzunluklu bloklara ayırır ve her blokta 0/1 oranını değerlendirir; blok içi tutarsızlıklar rasgelelik göstergesidir.",
18+
"remediation": "Özellikle yapılandırılmış veri formatlarında (header/payload) blok boyutunu ayarlayın veya transform uygulayın."
19+
},
20+
"cusum": {
21+
"title": "Cumulative Sum (CUSUM) Test",
22+
"short": "Bit dizisinin kümülatif toplama sapmalarını inceler.",
23+
"long": "CUSUM, pozitif ve negatif sapmaların kümülatif etkisini ölçer; büyük sapan eğilimler rasgele olmayanlığı işaret eder.",
24+
"remediation": "Trend oluşturan kaynakları ve sürekli bias'ı araştırın; ön işleme ile düzeltilip düzeltilmeyeceğini test edin."
25+
},
26+
"serial": {
27+
"title": "Serial (Overlapping) Test",
28+
"short": "N-gram (bit sıraları) frekanslarını karşılaştırır.",
29+
"long": "Seri test, n-bit alt dizilerin beklenen frekanslarını değerlendirir; örtüşen n-gram'ler arasındaki anormallikler ortaya çıkar.",
30+
"remediation": "Tekrarlayan motifleri, sıkıştırılmış veya kodlu dizileri kontrol edin; transform zinciri ile test edin."
31+
},
32+
"approximate_entropy": {
33+
"title": "Approximate Entropy",
34+
"short": "Verinin düzensizliğini entropi temelli ölçer.",
35+
"long": "Yaklaşık entropi, verideki tekrar eden paternlere duyarlıdır; düşük değerler deterministik yapıyı işaret edebilir.",
36+
"remediation": "Tekrar eden yapıların kaynağını araştırın veya farklı pencere boyutlarıyla yeniden değerlendir."
37+
},
38+
"maurers_universal": {
39+
"title": "Maurer's Universal Statistical Test",
40+
"short": "Sıkıştırılabilirlik üzerinden rastgeleliği değerlendirir.",
41+
"long": "Bu test, verinin ideal sıkıştırma oranına göre değerlendirilir; kolay sıkıştırılabilen veri rastgele değildir.",
42+
"remediation": "Verinin sıkıştırılabilirliğini kontrol edin; tek tip tekrarlar varsa veri kaynağını gözden geçirin."
43+
},
44+
"non_overlapping_template": {
45+
"title": "Non-overlapping Template Matching",
46+
"short": "Belirli şablonların varlığını sayar (çakışma yok).",
47+
"long": "Önceden tanımlı sabit uzunluklu şablonların tekrarını sayar; aşırı tekrar şüphelidir.",
48+
"remediation": "Şablon setini ve eşik değerlerini gözden geçirin; pre-transform uygulamayı deneyin."
49+
},
50+
"overlapping_template": {
51+
"title": "Overlapping Template Matching",
52+
"short": "Belirli şablonların tekrarını çakışma olsa bile sayar.",
53+
"long": "Örtüşen şablon örüntülerini sayarak lokal tekrarlılıkları tespit eder.",
54+
"remediation": "Verideki sık tekrar eden motifleri inceleyin; gerekirse windowing uygulayın."
55+
},
56+
"random_excursions": {
57+
"title": "Random Excursions",
58+
"short": "Saçılma (excursion) ziyaretlerinin dağılımını inceler.",
59+
"long": "Random walk temelli bu test, durumların ziyaret sıklıklarını değerlendirir; beklenmeyen dağılımlar düzensizlik göstergesi olabilir.",
60+
"remediation": "Uzun aralıklı bağımlılıkları kontrol edin; daha fazla örnekleme ile test edin."
61+
},
62+
"random_excursions_variant": {
63+
"title": "Random Excursions Variant",
64+
"short": "Çeşitlendirilmiş excursion sayım metrikleri sunar.",
65+
"long": "Variant, farklı durumların ziyaret istatistiklerini ölçerek daha hassas saptamalar yapar.",
66+
"remediation": "Anormal değerler görüldüğünde verinin segmentlerini analiz edin."
67+
},
68+
"nist_dft_spectral": {
69+
"title": "NIST DFT Spectral Test",
70+
"short": "Dizinin frekans alanındaki anormalliklerini arar.",
71+
"long": "Diskret Fourier Transform (DFT) kullanarak tepe (peak) frekansları tespit eder; belirgin spektral tepe rasgele olmayan düzenleri gösterir.",
72+
"remediation": "Periyodiklik üreten kaynakları (tekrar eden bloklar, sensor sampling) kontrol edin."
73+
},
74+
"fft_spectral": {
75+
"title": "FFT Spectral (Large-Scale)",
76+
"short": "Büyük veriler için spektral test ve downsample koruması.",
77+
"long": "FFT tabanlı test, NAIVE_DFT_BIT_LIMIT eşiği üzerinden otomatik örnekleme yapar; yüksek bellek kullanımını engeller.",
78+
"remediation": "Eşikleri ve downsample ayarlarını doğrulayın; veri segmentasyonu deneyin."
79+
},
80+
"linear_complexity": {
81+
"title": "Linear Complexity Test",
82+
"short": "Verinin LFSR tabanlı karmaşıklığını ölçer.",
83+
"long": "Lineer karmaşıklık, diziyi üretebilecek en kısa LFSR uzunluğunu değerlendirir; düşük karmaşıklık deterministik yapıya işaret eder.",
84+
"remediation": "Tekrarlayan yapı veya kısa periyotlu kaynakları inceleyin."
85+
},
86+
"lz_complexity": {
87+
"title": "LZ-Complexity",
88+
"short": "Lempel-Ziv sıkıştırma temelli karmaşıklık metriği.",
89+
"long": "Verinin LZ77/LZ78 tabanlı ayrıştırılabilirliğini ölçer; düşük değerler yüksek tekrarlılık gösterir.",
90+
"remediation": "Sık tekrar eden alt dizileri ve protokol başlıklarını kontrol edin."
91+
},
92+
"dotplot": {
93+
"title": "Dotplot (Self-Similarity)",
94+
"short": "Verinin kendi içinde tekrar eden bölümlerini görselleştirir.",
95+
"long": "Pencerelere bölünmüş veri arasındaki benzerlik matrisi, kopyalanmış veya tekrarlayan blokları gösteren bir ısı haritası üretir.",
96+
"remediation": "Tekrarlayan segmentleri ayıklayın veya segment bazlı analiz uygulayın; artefaktları raporlayın."
97+
},
98+
"binary_matrix_rank": {
99+
"title": "Binary Matrix Rank Test",
100+
"short": "Bit matrislerinin rank dağılımını test eder.",
101+
"long": "Veriden oluşturulan ikili matrislerin rank'leri beklenen dağılımdan saparsa rasgelelik sorgulanır.",
102+
"remediation": "Matris boyutlandırmasını ve veri segmentasyonunu kontrol edin."
103+
}
104+
}

templates/report_template.html

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,30 @@ <h5 class="mb-2">Meta</h5>
5757
<div class="col-12 col-lg-6">
5858
<div class="card p-3 h-100">
5959
<h5>Scorecard</h5>
60-
<div class="row">
61-
{% for k, v in scorecard.items() %}
62-
<div class="col-6 col-sm-4 mb-2">
63-
<div class="text-muted small">{{ k }}</div>
64-
<div class="score-metric">{{ v | tojson }}</div>
60+
<div class="d-flex align-items-center flex-wrap">
61+
<div class="me-4 text-center">
62+
<div class="text-muted small">Failed tests</div>
63+
<div class="score-metric" style="color:#d9534f">{{ scorecard.get("failed_tests", 0) }}</div>
64+
</div>
65+
<div class="me-4 text-center">
66+
<div class="text-muted small">Total tests</div>
67+
<div class="score-metric">{{ scorecard.get("total_tests", 0) }}</div>
68+
</div>
69+
<div class="me-4 text-center">
70+
<div class="text-muted small">Mean effect size</div>
71+
<div class="score-metric">{{ scorecard.get("mean_effect_size") | default("n/a") }}</div>
72+
</div>
73+
<div class="me-4 text-center">
74+
<div class="text-muted small">FDR q</div>
75+
<div class="score-metric">{{ scorecard.get("fdr_q") }}</div>
76+
</div>
77+
</div>
78+
<hr/>
79+
<div>
80+
<small class="text-muted">P-value distribution</small>
81+
<div class="mt-2">
82+
<canvas id="pvalMini" height="60"></canvas>
6583
</div>
66-
{% endfor %}
6784
</div>
6885
</div>
6986
</div>
@@ -181,6 +198,21 @@ <h6 class="mt-2">Visuals</h6>
181198

182199
</div>
183200

201+
<!-- Plugin description modal -->
202+
<div class="modal fade" id="pluginDescModal" tabindex="-1" aria-labelledby="pluginDescModalLabel" aria-hidden="true">
203+
<div class="modal-dialog modal-lg modal-dialog-centered">
204+
<div class="modal-content">
205+
<div class="modal-header">
206+
<h5 class="modal-title" id="pluginDescModalLabel">Plugin description</h5>
207+
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
208+
</div>
209+
<div class="modal-body">
210+
<!-- populated dynamically -->
211+
</div>
212+
</div>
213+
</div>
214+
</div>
215+
184216
<!-- Scripts -->
185217
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-3gJwYp2k6u9c2Y5nYq3X8bK5jv1Yj2Kp2t9Qjv6v+8w=" crossorigin="anonymous"></script>
186218
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js" integrity="sha384-abc" crossorigin="anonymous"></script>
@@ -223,6 +255,20 @@ <h6 class="mt-2">Visuals</h6>
223255
'</div>';
224256
// Use Bootstrap Popover (hover + focus to support keyboard)
225257
new bootstrap.Popover(btn, { title: d.title || key, content: content, html: true, trigger: 'hover focus', placement: 'auto' });
258+
// Add click handler to open modal with full description for accessibility/long text
259+
btn.addEventListener('click', function(ev){
260+
try {
261+
const modal = document.getElementById('pluginDescModal');
262+
const titleEl = modal.querySelector('.modal-title');
263+
const bodyEl = modal.querySelector('.modal-body');
264+
titleEl.textContent = d.title || key;
265+
bodyEl.innerHTML = '<p>' + (d.short ? d.short : '') + '</p>' + (d.remediation ? '<p><strong>Remediation:</strong> ' + d.remediation + '</p>' : '') + '<pre class="json" style="margin-top:8px">' + JSON.stringify(d, null, 2) + '</pre>';
266+
const bsModal = new bootstrap.Modal(modal);
267+
bsModal.show();
268+
} catch (e) {
269+
console.warn('plugin-help modal show failed', e);
270+
}
271+
});
226272
} catch (e) {
227273
// per-button errors should not break initialization
228274
console.warn('plugin-help init failed for', btn, e);

tests/test_engine.py

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -50,11 +50,22 @@ def test_visual_error_reported_in_visual_errors_and_does_not_fail_test():
5050
engine.register_test("goodtest", GoodTest())
5151
engine.register_visual("badvisual", BadVisual())
5252

53-
def test_visual_error_sets_result_status_error():
54-
"""Test that visual errors set result status to error when motor has new behavior."""
55-
engine = Engine()
56-
engine.register_test("goodtest", GoodTest())
57-
engine.register_visual("badvisual", BadVisual())
53+
out = engine.analyze(
54+
b"\x00\x01\x02",
55+
{
56+
"tests": [{"name": "goodtest", "params": {}}],
57+
},
58+
)
59+
60+
assert out["results"], "Expected at least one result"
61+
r = out["results"][0]
62+
63+
# Engine should keep the test completed while reporting visual errors separately.
64+
assert r.get("status") == "completed"
65+
# The serialized result must include the visual_errors entry for the failing visual plugin.
66+
assert "visual_errors" in r and any(v.get("visual_name") == "badvisual" for v in r.get("visual_errors"))
67+
# GoodTest returns passed=True with p_value=None in the TestResult above.
68+
assert r.get("passed") is True
5869

5970

6071

0 commit comments

Comments
 (0)