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
126 changes: 122 additions & 4 deletions src/imgtests/logger.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,31 @@
import json
import logging
import re
import sys
from typing import TYPE_CHECKING, Literal, Self, TextIO
from enum import StrEnum
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 LIB_DATA_DIR

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


class LoggingPatterns(StrEnum):
TASK_STARTED_STATUS = "RUNNING"
TASK_STATUS = r"Task id=([\w-]+) path=[\w|\.]+ state=(\w+)"
TESTS_COUNT = r"Total amount of tests per run: (\d+)"
RUNS_COUNT = r"Starting test run (\d+) of (\d+)"
DEFAULT_TEST_START = r"Starting '(.*\.)'.*"
DEFAULT_TEST_FINISH = r"'(.*\.)' test finished."
SUITE_START = r"Running suite (.*)\."
PROFILED_TEST_START = r"\[PLAN\] run stage=([\w-]+) tool=([\w-]+) subsystem=([\w-]+).*"
PROFILED_TEST_FINISH = r"\[PLAN\] done .*"
PROFILE_DONE = r"\[PROFILED\] DONE profile=(\w+) pattern=(\w+) .*"


class StreamFormatter(logging.Formatter):
def __init__(self: Self) -> None:
self._level_fmt = "[%(levelname)s]"
Expand All @@ -23,6 +38,99 @@ def format(self: Self, record: logging.LogRecord) -> str:
return super().format(record)


class ProgressHandler(logging.Handler):
progress_template = { # noqa: RUF012
"total_test_count": 0,
"test_count": 0,
"total_run_count": 0,
"current_test_run": 0,
"current_suite": "Not started yet",
"last_profile_done": "Not done yet",
"current_test": "Not started yet",
}

def __init__(self, level: logging._Level = logging.DEBUG):
super().__init__(level)
self.progress_data = {}
self.proc_to_task = {}
Comment on lines +54 to +55

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.

Тут потецниальная "утечка" памяти, т.к. от каждого запуска будет забиваться и не вичищаться после тестов.


def emit(self, record: logging.LogRecord): # noqa: PLR0915
proc = str(record.process)
msg = self.format(record)

# detect task started or finished
match = re.search(LoggingPatterns.TASK_STATUS, msg)
if match:
task_id = match.group(1)
status = match.group(2)
# task started
if status == LoggingPatterns.TASK_STARTED_STATUS:
self.proc_to_task[proc] = task_id
# task finished or broke
else:
self.proc_to_task[proc] = None
# flush progress_data for process
self.progress_data[proc] = self.progress_template.copy()

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

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

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

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

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

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

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

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

Comment thread
Artanias marked this conversation as resolved.
if proc in self.proc_to_task and self.proc_to_task[proc] is not None:
task_id = self.proc_to_task[proc]
with Path.open(
LIB_DATA_DIR / (task_id + "_progress.log"),
"w",
encoding="utf-8",
) as file:
json.dump(self.progress_data[proc], file, indent=4)


def set_handlers(
logger: logging.Logger,
filename: Path,
Expand All @@ -45,6 +153,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 +191,12 @@ def filter(self: Self, record: logging.LogRecord) -> bool:
stdout_handler.setFormatter(StreamFormatter())

return stdout_handler


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

return progress_handle
Comment on lines +196 to +202

@Artanias Artanias Jul 25, 2026

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.

Лучше бы это всё проворачивать более явно, нежели чем через парсинг логов, т.к кажется, что поменяются логи, а они соответственно меняются, то всё сломается и будет сходу не понятно, что делать, где чинить.

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.

Также в БД попадают уже сейчас результаты тестов, может получится их расширить и оттуда вычислять значения?

28 changes: 28 additions & 0 deletions src/imgtests/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -647,6 +647,11 @@ def run_tests(
) -> None:
if mode == "default" and config is None:
config = load_test_config(distro)

total_tests_amount = __calc_total_tests_amount(config, mode)
logger.info("Total amount of tests per run: %d", total_tests_amount)
Comment thread
Artanias marked this conversation as resolved.

# 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 All @@ -661,3 +666,26 @@ def get_test_name(
if hasattr(test, "__class__"):
return test.__class__.__name__
return str(test)


def __calc_total_tests_amount(config: dict[str, Any] | None, mode: Runner) -> int:
from imgtests.suites.map import ALL_SUITES # noqa: PLC0415

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)
# default profile config consists of 3 stages
if config is None:
total_tests_amount *= 3
if tmp_config.run_matrix:
total_tests_amount *= len(tmp_config.matrix_profiles)
return total_tests_amount
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;
}
Loading
Loading