Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/imgtests/database/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
from imgtests.database.models.util_run_result import UtilRunResult, UtilType

if TYPE_CHECKING:
from collections.abc import Sequence

from imgtests.sysrep import SystemInfo
from imgtests.types import TestsCounts

Expand Down Expand Up @@ -217,6 +219,16 @@ def return_table(self, table_name: Table) -> list[Any]:

return session.query(models[table_name]).all()

def list_experiments(self) -> Sequence[ExperimentBase]:
self._check_session()
with self.session() as session:
return (
session.query(ExperimentBase)
.options(selectinload(ExperimentBase.configuration))
.order_by(ExperimentBase.experiment_id.desc())
.all()
)

def get_experiment_with_details(self, experiment_id: int) -> ExperimentBase:
"""Gets a single experiment with all related entities.

Expand Down
21 changes: 0 additions & 21 deletions src/imgtests/reporting/html_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -223,27 +223,6 @@ def generate_compare_html_report(
)
return report_path

def generate_last_two_experiments_report(self, out_dir: Path) -> Path | None:
from sqlalchemy import desc # noqa: PLC0415

from imgtests.database.models.experiment import ExperimentBase # noqa: PLC0415

self._database._check_session() # noqa: SLF001
with self._database.session() as session:
experiments = (
session.query(ExperimentBase)
.order_by(desc(ExperimentBase.started_at))
.limit(2)
.all()
)
ids = [exp.experiment_id for exp in experiments]
if len(ids) != 2: # noqa: PLR2004
self._logger.error(
"Couldn't build a report: there are incorrect amount of experiments.",
)
return None
return self.generate_compare_html_report(sorted(ids), out_dir)

def __distribute_metrics(
self,
metrics_by_exp: list[list[MetricSample]],
Expand Down
6 changes: 1 addition & 5 deletions src/imgtests/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -709,8 +709,7 @@ def _get_clients(distro: str) -> tuple[SSHClient | None, SSHClient | None]:
return suse_client, poky_client


def _run_single(distro: Distro, mode: Runner, config: dict[str, Any]) -> None: # noqa: PLR0912, PLR0915, C901
from imgtests.reporting.html_report import ReportGenerator # noqa: PLC0415
def _run_single(distro: Distro, mode: Runner, config: dict[str, Any]) -> None: # noqa: PLR0912, C901
from imgtests.suites.map import ( # noqa: PLC0415
ALL_SUBSYSTEMS_SUITE,
ALL_SUITES,
Expand Down Expand Up @@ -785,9 +784,6 @@ def _run_single(distro: Distro, mode: Runner, config: dict[str, Any]) -> None:
if suse_client:
run_profiled(suse_client, database)

report_generator = ReportGenerator(database)
report_generator.generate_last_two_experiments_report(out_dir=REPORTS_DIR)

database.session.close_all()


Expand Down
79 changes: 79 additions & 0 deletions src/imgtests/web/static/js/html_reports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
const select1 = document.getElementById("exp-select-1");
const select2 = document.getElementById("exp-select-2");
const genBtn = document.getElementById("generate-btn");
const resultDiv = document.getElementById("compare-result");

function fillSelect(select, data) {
data.forEach((exp) => {
const opt = document.createElement("option");
opt.value = exp.id;
opt.textContent = `#${exp.id} | ${exp.os} | ${exp.description || "(no description)"} | ${exp.started_at || "?"}`;
select.appendChild(opt);
});
}

function updateButton() {
const v1 = select1.value;
const v2 = select2.value;
genBtn.disabled = !v1 || !v2 || v1 === v2;
}

fetch("/api/experiments/")
.then((r) => r.json())
.then((data) => {
if (data.experiments) {
fillSelect(select1, data.experiments);
fillSelect(select2, data.experiments);
}
})
.catch(() => {});

select1.addEventListener("change", updateButton);
select2.addEventListener("change", updateButton);

genBtn.addEventListener("click", function () {
const id1 = parseInt(select1.value, 10);
const id2 = parseInt(select2.value, 10);
genBtn.disabled = true;
genBtn.textContent = "Generating...";
resultDiv.style.display = "none";

fetch("/api/generate-compare-report/", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRFToken": "{{ csrf_token }}",
},
body: JSON.stringify({
experiment_id_1: id1,
experiment_id_2: id2,
}),
})
.then((r) => r.json())
.then((data) => {
if (data.success) {
const reportUrl =
"/reports/view/" + data.report_url.replace(/\\/g, "/");
resultDiv.innerHTML =
'<span style="color: #2e7d32;">Report generated! Reload page to see it';
resultDiv.style.display = "block";
} else {
resultDiv.innerHTML =
'<span style="color: #c62828;">Error: ' +
(data.error || "unknown") +
"</span>";
resultDiv.style.display = "block";
}
})
.catch((err) => {
resultDiv.innerHTML =
'<span style="color: #c62828;">Request failed: ' +
err.message +
"</span>";
resultDiv.style.display = "block";
})
.finally(() => {
genBtn.disabled = false;
genBtn.textContent = "Generate";
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,85 @@ <h1>Test Reports</h1>
</div>
</div>

<div
class="compare-section"
style="
margin-bottom: 24px;
padding: 20px;
background: #fff8e1;
border-radius: 12px;
border: 1px solid #ffe082;
"
>
<h3 style="margin: 0 0 12px; color: #000000">
Generate Comparison Report
</h3>
<div
style="
display: flex;
gap: 16px;
flex-wrap: wrap;
align-items: flex-end;
"
>
<div style="flex: 1; min-width: 200px">
<label
style="
display: block;
margin-bottom: 4px;
font-size: 0.85rem;
color: #795548;
"
>Experiment 1</label
>
<select
id="exp-select-1"
style="
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 6px;
"
>
<option value="">Select</option>
</select>
</div>
<div style="flex: 1; min-width: 200px">
<label
style="
display: block;
margin-bottom: 4px;
font-size: 0.85rem;
color: #795548;
"
>Experiment 2</label
>
<select
id="exp-select-2"
style="
width: 100%;
padding: 8px;
border: 1px solid #ccc;
border-radius: 6px;
"
>
<option value="">Select</option>
</select>
</div>
<div>
<button
id="generate-btn"
class="btn"
style="padding: 8px 20px"
disabled
>
Generate
</button>
</div>
</div>
<div id="compare-result" style="margin-top: 12px; display: none"></div>
</div>

{% if reports %}
<div class="reports-list">
{% for report in reports %}
Expand Down Expand Up @@ -97,6 +176,9 @@ <h3 style="color: #5d4037; margin-bottom: 10px">No Reports Found</h3>
{% endif %}
</div>

{% load static %}
<script src="{% static 'js/html_reports.js' %}"></script>

<style>
.report-item:hover {
background: #fef9e7;
Expand Down
10 changes: 10 additions & 0 deletions src/imgtests/web/tests_interface/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,14 @@
name="download_excel_report",
),
path("api/export-excel/", views.api_export_excel, name="api_export_excel"),
path(
"api/experiments/",
views.api_list_experiments,
name="api_list_experiments",
),
path(
"api/generate-compare-report/",
views.api_generate_compare_report,
name="api_generate_compare_report",
),
]
62 changes: 62 additions & 0 deletions src/imgtests/web/tests_interface/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
from sqlalchemy import create_engine

from imgtests.constant import CONFIG_DIR, EXCEL_REPORTS_DIR, REPORTS_DIR
from imgtests.database.database import ImgtestsDatabase
from imgtests.reporting.excel_export import export_database_to_excel
from imgtests.reporting.html_report import ReportGenerator
from imgtests.runner import get_test_name
from imgtests.suites.map import ALL_SUITES

Expand Down Expand Up @@ -431,3 +433,63 @@ def download_excel_report(request: HttpRequest, filename: str) -> FileResponse:
filename=filename,
as_attachment=True,
)


def api_list_experiments(request: HttpRequest) -> JsonResponse: # noqa: ARG001
try:
experiments = ImgtestsDatabase().list_experiments()
except Exception as e: # noqa: BLE001
return JsonResponse({"error": str(e)}, status=500)

return JsonResponse(
{
"experiments": [
{
"id": exp.experiment_id,
"description": exp.description,
"os": exp.configuration.os if exp.configuration else "unknown",
"started_at": (exp.started_at.isoformat() if exp.started_at else None),
"ended_at": exp.ended_at.isoformat() if exp.ended_at else None,
"tests_total": exp.tests_total,
"tests_passed": exp.tests_passed,
"tests_failed": exp.tests_failed,
"tests_broken": exp.tests_broken,
"tests_skipped": exp.tests_skipped,
}
for exp in experiments
],
},
)


@csrf_exempt
@require_http_methods(["POST"])
def api_generate_compare_report(request: HttpRequest) -> JsonResponse:
try:
body = json.loads(request.body)
except json.JSONDecodeError:
return JsonResponse({"error": "Invalid request body"}, status=400)

try:
exp_id_1 = int(body.get("experiment_id_1"))
exp_id_2 = int(body.get("experiment_id_2"))
except TypeError, ValueError:
return JsonResponse({"error": "Invalid experiment id type"}, status=400)

try:
report_gen = ReportGenerator(ImgtestsDatabase())
report_path = report_gen.generate_compare_html_report(
sorted([exp_id_1, exp_id_2]),
out_dir=REPORTS_DIR,
)
except Exception as e: # noqa: BLE001
return JsonResponse({"error": str(e)}, status=500)

if report_path is None:
return JsonResponse({"error": "Failed to generate report"}, status=500)
return JsonResponse(
{
"success": True,
"report_url": str(report_path.relative_to(REPORTS_DIR)),
},
)
Loading