Skip to content
Open
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
1 change: 1 addition & 0 deletions src/imgtests/constant.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
# Paths
LIB_DATA_DIR: Final = Path.home() / LIB_NAME
LOG_PATH: Final = LIB_DATA_DIR / "processing.log"
PROG_LOG_PATH: Final = LIB_DATA_DIR / "test_progress.log"
CONFIG_DIR: Final = LIB_DATA_DIR / "test_configs"
CONFIG_FILE: Final = LIB_DATA_DIR / "user_distros.json"
REPORTS_DIR: Final = LIB_DATA_DIR / "results"
Expand Down
104 changes: 100 additions & 4 deletions src/imgtests/logger.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import json
import logging
import re
import sys
from typing import TYPE_CHECKING, Literal, Self, TextIO
from pathlib import Path
from typing import Literal, Self, TextIO

from pythonjsonlogger.json import JsonFormatter

if TYPE_CHECKING:
from pathlib import Path

from imgtests.constant import PROG_LOG_PATH

LogLevel = Literal["debug", "info", "warning", "error", "critical"]

Expand All @@ -23,6 +24,91 @@ def format(self: Self, record: logging.LogRecord) -> str:
return super().format(record)


class ProgressHandle(logging.Handler):
tests_count_pattern = r"Total amount of tests per run: (\d+)"
test_runs_pattern = r"Starting test run (\d+) of (\d+)"
default_test_start_pattern = r"Starting '(.*\.)'.*"
default_test_finish_pattern = r"'(.*\.)' test finished."
suite_start_pattern = r"Running suite (.*)\."
profiled_test_start_pattern = r"\[PLAN\] run stage=([\w-]+) tool=([\w-]+) subsystem=([\w-]+).*"
profiled_test_finish_pattern = r"\[PLAN\] done .*"
profile_done_pattern = r"\[PROFILED\] DONE profile=(\w+) pattern=(\w+) .*"

def __init__(self, level: logging._Level = logging.DEBUG):
super().__init__(level)
self.progress_data = {}
self.flush()

def flush(self):
self.progress_data = {
"total_test_count": 0,
"test_count": 0,
"total_run_count": 0,
"current_test_run": 0,
"current_suite": "Not starter yet",
"last_profile_done": "Not done yet",
"current_test": "Not starter yet",
}

def get_progress_data(self):
return self.progress_data

def emit(self, record: logging.LogRecord):
msg = self.format(record)

match = re.search(self.tests_count_pattern, msg)
if match:
total = int(match.group(1))
self.progress_data["total_test_count"] = total

match = re.search(self.test_runs_pattern, msg)
if match:
# set current run
cur = int(match.group(1))
total = int(match.group(2))
self.progress_data["current_test_run"] = cur
self.progress_data["total_run_count"] = total
# reset tests count
self.progress_data["test_count"] = 0

# default runner matches
match = re.search(self.suite_start_pattern, msg)
if match:
suite = match.group(1)
self.progress_data["current_suite"] = suite

match = re.search(self.default_test_start_pattern, msg)
if match:
test = match.group(1)
self.progress_data["current_test"] = test

match = re.search(self.default_test_finish_pattern, msg)
if match:
self.progress_data["test_count"] += 1
self.progress_data["current_test"] = "Not started yet"

# profiled runner matches
match = re.search(self.profile_done_pattern, msg)
if match:
profile = "-".join([match.group(1), match.group(2)])
self.progress_data["last_profile_done"] = profile

match = re.search(self.profiled_test_start_pattern, msg)
if match:
subsystem = match.group(3)
profile = match.group(1)
tool = match.group(2)
self.progress_data["current_test"] = f"{subsystem}-{profile} via {tool}"

match = re.search(self.profiled_test_finish_pattern, msg)
if match:
self.progress_data["test_count"] += 1
self.progress_data["current_test"] = "Not started yet"

with Path.open(PROG_LOG_PATH, "w", encoding="utf-8") as file:
json.dump(self.progress_data, file, indent=4)


def set_handlers(
logger: logging.Logger,
filename: Path,
Expand All @@ -45,6 +131,7 @@ def set_handlers(
"""
levelno = getattr(logging, log_level.upper())
logger.setLevel(levelno)
logger.addHandler(__get_progress_handler())
logger.addHandler(__get_file_handler(filename))
if levelno in [logging.INFO, logging.DEBUG]:
logger.addHandler(__get_stdout_handler())
Expand Down Expand Up @@ -82,3 +169,12 @@ def filter(self: Self, record: logging.LogRecord) -> bool:
stdout_handler.setFormatter(StreamFormatter())

return stdout_handler


def __get_progress_handler() -> ProgressHandle:
progress_handle = ProgressHandle()
progress_handle.setLevel(logging.DEBUG)
progress_handle.setFormatter(StreamFormatter())
progress_handle.set_name("progress_handler")

return progress_handle
21 changes: 21 additions & 0 deletions src/imgtests/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -637,8 +637,29 @@ def run_tests(
test_runs_count: int = 1,
config: dict[str, Any] | None = None,
) -> None:
from imgtests.suites.map import ALL_SUITES # noqa: PLC0415

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Лучше по возможности вообще не делать так импорты без крайней необходимости, а если таким образом попытка избежать циклических импортов -> скорее всего требуется провести рефакторинг кода или поместить логику в другой модуль.


if mode == "default" and config is None:
config = load_test_config(distro)

# log tests amount for web ui progress card
total_tests_amount = 0
if mode == "default" and config:
for suite in config["suites"]:
if suite in config["selected_tests"]:
total_tests_amount += len(config["selected_tests"][suite])
else:
total_tests_amount += len(ALL_SUITES[suite].tests)
# default runner runs 2 system tests for each suite (runner.py: 616 -> 621 -> 149)
total_tests_amount += 2
if mode == "profiled":
tmp_config = build_profiled_settings(config=config)
total_tests_amount = len(tmp_config.subsystems)
if tmp_config.run_matrix:
total_tests_amount *= len(tmp_config.matrix_profiles)
logger.info("Total amount of tests per run: %d", total_tests_amount)

# start test runs
for i in range(test_runs_count):
logger.info("Starting test run %d of %d", i + 1, test_runs_count)
_run_single(distro, mode, config)
Expand Down
81 changes: 81 additions & 0 deletions src/imgtests/web/static/css/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -403,3 +403,84 @@ h1 i {
color: rgba(255, 255, 255, 0.9);
margin: 0;
}

.progress-card {
background: white;
margin-bottom: 20px;
padding: 30px;
width: 100%;
border-radius: 12px;
border: 1px solid #ddd;
}

.progress-group {
margin-bottom: 20px;
}

.progress-label {
display: flex;
justify-content: space-between;
margin-bottom: 8px;
font-size: 14px;
font-weight: 600;
color: #7f8c8d;
}

.progress-bg {
background-color: #e0e0e0;
border-radius: 8px;
overflow: hidden;
height: 16px;
width: 100%;
}

.progress-bar {
background-color: #2ecc71;
height: 100%;
width: 0%;
transition: width 0.4s ease-out;
}

#runs-bar {
background-color: #3498db;
}

@keyframes pulse-animation {
0% { opacity: 1; }
50% { opacity: 0.4; }
100% { opacity: 1; }
}

.pulse {
animation: pulse-animation 1.5s infinite ease-in-out;
background-image: linear-gradient(45deg, rgba(255,255,255,.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,.15) 50%, rgba(255,255,255,.15) 75%, transparent 75%, transparent);
background-size: 1rem 1rem;
}

.info-section {
margin-top: 25px;
border-top: 1px solid #ecf0f1;
padding-top: 15px;
}

.info-item {
margin-bottom: 10px;
font-size: 14px;
}

.info-item strong {
color: #34495e;
}

.info-item span {
color: #16a085;
font-weight: 500;
}

.status-error {
color: #e74c3c;
text-align: center;
font-size: 12px;
margin-top: 10px;
display: none;
}
73 changes: 71 additions & 2 deletions src/imgtests/web/static/js/launch_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ document.getElementById("runTestsBtn").addEventListener("click", function () {
btn.textContent = "Running...";
outputContainer.textContent = "Tests Running...";

// flush progress handler the run tests
fetch("/flush-progress/").then(() => {
fetch("/run-tests/", {
method: "POST",
headers: {
Expand All @@ -89,25 +91,37 @@ document.getElementById("runTestsBtn").addEventListener("click", function () {
testing_mode: testing_mode,
config: config,
}),
})
.then((response) => response.json())
}).then((response) => response.json())
.then((data) => {
if (data.success && data.task_id) {
outputContainer.textContent =
"Tests running... (Task ID: " + data.task_id + ")";
pollStatus(data.task_id);
// add progress display
document.getElementById("progress-card").style.display = "block";
// hide suite | profile depending on selected mode
if (testing_mode === "profiled") {
document.getElementById("current-suite-div").style.display = "none";
document.getElementById("last-profile-div").style.display = "inline";
} else {
document.getElementById("current-suite-div").style.display = "inline";
document.getElementById("last-profile-div").style.display = "none";
}
} else {
outputContainer.textContent =
"Error: " + (data.error || "Failed to start tests");
btn.disabled = false;
btn.textContent = "Run tests";
document.getElementById("progress-card").style.display = "none";
}
})
.catch((error) => {
outputContainer.textContent = "Error: " + error;
btn.disabled = false;
btn.textContent = "Run tests";
document.getElementById("progress-card").style.display = "none";
});
});
});

function pollStatus(taskId) {
Expand All @@ -127,6 +141,7 @@ function pollStatus(taskId) {
data.output || "Tests completed successfully.";
btn.disabled = false;
btn.textContent = "Run tests";
document.getElementById("progress-card").style.display = "none";
} else if (data.status === "failed") {
let errorMsg = data.error || "Test failed";
if (data.stderr) {
Expand All @@ -138,18 +153,72 @@ function pollStatus(taskId) {
outputContainer.textContent = errorMsg;
btn.disabled = false;
btn.textContent = "Run tests";
document.getElementById("progress-card").style.display = "none";
} else {
outputContainer.textContent = "Unknown status";
btn.disabled = false;
btn.textContent = "Run tests";
document.getElementById("progress-card").style.display = "none";
}
})
.catch((error) => {
outputContainer.textContent = "Error checking status: " + error;
btn.disabled = false;
btn.textContent = "Run tests";
document.getElementById("progress-card").style.display = "none";
});
};

checkStatus();
}

const PRGORESS_POLLING_INTERVAL = 3000;

function updateDashboard() {
fetch("/current-progress/", { cache: "no-store" })
.then(response => {
if (!response.ok) throw new Error('Ошибка загрузки файла');
return response.json();
})
.then(data => {
document.getElementById('error-msg').style.display = 'none';

const totalTests = data.total_test_count || 0;
const currentTests = data.test_count || 0;
const testsPercent = totalTests > 0 ? Math.min(Math.round((currentTests / totalTests) * 100), 100) : 0;

document.getElementById('tests-text').textContent = `${currentTests} / ${totalTests} (${testsPercent}%)`;
document.getElementById('tests-bar').style.width = `${testsPercent}%`;


const totalRuns = data.total_run_count || 0;
const currentRun = data.current_test_run || 0;
const runsPercent = totalRuns > 0 ? Math.min(Math.round((currentRun / totalRuns) * 100), 100) : 0;

const runsBar = document.getElementById('runs-bar');
const runsText = document.getElementById('runs-text');

runsBar.style.width = `${runsPercent}%`;

if (currentRun > 0 && currentRun <= totalRuns) {
runsBar.classList.add('pulse');
runsText.textContent = `Run ${currentRun} out of ${totalRuns} is in progress (${runsPercent}%)`;
runsText.style.color = '#3498db';
} else {
runsBar.classList.remove('pulse');
runsText.textContent = `${currentRun} / ${totalRuns} (${runsPercent}%)`;
runsText.style.color = '#7f8c8d';
}

document.getElementById('current-suite').textContent = data.current_suite;
document.getElementById('current-test').textContent = data.current_test;
document.getElementById('last-profile').textContent = data.last_profile_done;
})
.catch(error => {
console.error('JSON processing error:', error);
document.getElementById('error-msg').style.display = 'block';
});
}

updateDashboard();
let interval = setInterval(updateDashboard, PRGORESS_POLLING_INTERVAL);
Loading
Loading