Skip to content

Commit 297e1bc

Browse files
committed
dts: make log files into artifacts
Make log files behave like artifacts as dictated by the Artifact class. Implicitly, this will automatically place all the logs in a structured manner. Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
1 parent c950cb6 commit 297e1bc

3 files changed

Lines changed: 83 additions & 81 deletions

File tree

dts/framework/logger.py

Lines changed: 73 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -2,43 +2,54 @@
22
# Copyright(c) 2010-2014 Intel Corporation
33
# Copyright(c) 2022-2023 PANTHEON.tech s.r.o.
44
# Copyright(c) 2022-2023 University of New Hampshire
5+
# Copyright(c) 2025 Arm Limited
56

67
"""DTS logger module.
78
89
The module provides several additional features:
910
1011
* The storage of DTS execution stages,
11-
* Logging to console, a human-readable log file and a machine-readable log file,
12-
* Optional log files for specific stages.
12+
* Logging to console, a human-readable log artifact and a machine-readable log artifact,
13+
* Optional log artifacts for specific stages.
1314
"""
1415

1516
import logging
16-
from logging import FileHandler, StreamHandler
17-
from pathlib import Path
18-
from typing import ClassVar
17+
from logging import StreamHandler
18+
from typing import TYPE_CHECKING, ClassVar, NamedTuple
19+
20+
if TYPE_CHECKING:
21+
from framework.testbed_model.artifact import Artifact
1922

2023
date_fmt = "%Y/%m/%d %H:%M:%S"
2124
stream_fmt = "%(asctime)s - %(stage)s - %(name)s - %(levelname)s - %(message)s"
2225
dts_root_logger_name = "dts"
2326

2427

28+
class ArtifactHandler(NamedTuple):
29+
"""A logger handler with an associated artifact."""
30+
31+
artifact: "Artifact"
32+
handler: StreamHandler
33+
34+
2535
class DTSLogger(logging.Logger):
2636
"""The DTS logger class.
2737
2838
The class extends the :class:`~logging.Logger` class to add the DTS execution stage information
2939
to log records. The stage is common to all loggers, so it's stored in a class variable.
3040
31-
Any time we switch to a new stage, we have the ability to log to an additional log file along
32-
with a supplementary log file with machine-readable format. These two log files are used until
33-
a new stage switch occurs. This is useful mainly for logging per test suite.
41+
Any time we switch to a new stage, we have the ability to log to an additional log artifact
42+
along with a supplementary log artifact with machine-readable format. These two log artifacts
43+
are used until a new stage switch occurs. This is useful mainly for logging per test suite.
3444
"""
3545

3646
_stage: ClassVar[str] = "pre_run"
37-
_extra_file_handlers: list[FileHandler] = []
47+
_root_artifact_handlers: list[ArtifactHandler] = []
48+
_extra_artifact_handlers: list[ArtifactHandler] = []
3849

3950
def __init__(self, *args, **kwargs):
40-
"""Extend the constructor with extra file handlers."""
41-
self._extra_file_handlers = []
51+
"""Extend the constructor with extra artifact handlers."""
52+
self._extra_artifact_handlers = []
4253
super().__init__(*args, **kwargs)
4354

4455
def makeRecord(self, *args, **kwargs) -> logging.LogRecord:
@@ -56,7 +67,7 @@ def makeRecord(self, *args, **kwargs) -> logging.LogRecord:
5667
record.stage = DTSLogger._stage
5768
return record
5869

59-
def add_dts_root_logger_handlers(self, verbose: bool, output_dir: str) -> None:
70+
def add_dts_root_logger_handlers(self, verbose: bool) -> None:
6071
"""Add logger handlers to the DTS root logger.
6172
6273
This method should be called only on the DTS root logger.
@@ -65,18 +76,16 @@ def add_dts_root_logger_handlers(self, verbose: bool, output_dir: str) -> None:
6576
Three handlers are added:
6677
6778
* A console handler,
68-
* A file handler,
69-
* A supplementary file handler with machine-readable logs
79+
* An artifact handler,
80+
* A supplementary artifact handler with machine-readable logs
7081
containing more debug information.
7182
72-
All log messages will be logged to files. The log level of the console handler
83+
All log messages will be logged to artifacts. The log level of the console handler
7384
is configurable with `verbose`.
7485
7586
Args:
7687
verbose: If :data:`True`, log all messages to the console.
7788
If :data:`False`, log to console with the :data:`logging.INFO` level.
78-
output_dir: The directory where the log files will be located.
79-
The names of the log files correspond to the name of the logger instance.
8089
"""
8190
self.setLevel(1)
8291

@@ -86,70 +95,81 @@ def add_dts_root_logger_handlers(self, verbose: bool, output_dir: str) -> None:
8695
sh.setLevel(logging.INFO)
8796
self.addHandler(sh)
8897

89-
self._add_file_handlers(Path(output_dir, self.name))
98+
self._root_artifact_handlers = self._add_artifact_handlers(self.name)
9099

91-
def set_stage(self, stage: str, log_file_path: Path | None = None) -> None:
92-
"""Set the DTS execution stage and optionally log to files.
93-
94-
Set the DTS execution stage of the DTSLog class and optionally add
95-
file handlers to the instance if the log file name is provided.
96-
97-
The file handlers log all messages. One is a regular human-readable log file and
98-
the other one is a machine-readable log file with extra debug information.
100+
def set_stage(self, stage: str) -> None:
101+
"""Set the DTS execution stage.
99102
100103
Args:
101104
stage: The DTS stage to set.
102-
log_file_path: An optional path of the log file to use. This should be a full path
103-
(either relative or absolute) without suffix (which will be appended).
104105
"""
105-
self._remove_extra_file_handlers()
106-
107106
if DTSLogger._stage != stage:
108107
self.info(f"Moving from stage '{DTSLogger._stage}' to stage '{stage}'.")
109108
DTSLogger._stage = stage
110109

111-
if log_file_path:
112-
self._extra_file_handlers.extend(self._add_file_handlers(log_file_path))
110+
def set_custom_log_file(self, log_file_name: str | None) -> None:
111+
"""Set a custom log file.
113112
114-
def _add_file_handlers(self, log_file_path: Path) -> list[FileHandler]:
115-
"""Add file handlers to the DTS root logger.
113+
Add artifact handlers to the instance if the log artifact file name is provided. Otherwise,
114+
stop logging to any custom log file.
116115
117-
Add two type of file handlers:
116+
The artifact handlers log all messages. One is a regular human-readable log artifact and
117+
the other one is a machine-readable log artifact with extra debug information.
118118
119-
* A regular file handler with suffix ".log",
120-
* A machine-readable file handler with suffix ".verbose.log".
119+
Args:
120+
log_file_name: An optional name of the log artifact file to use. This should be without
121+
suffix (which will be appended).
122+
"""
123+
self._remove_extra_artifact_handlers()
124+
125+
if log_file_name:
126+
self._extra_artifact_handlers.extend(self._add_artifact_handlers(log_file_name))
127+
128+
def _add_artifact_handlers(self, log_file_name: str) -> list[ArtifactHandler]:
129+
"""Add artifact handlers to the DTS root logger.
130+
131+
Add two type of artifact handlers:
132+
133+
* A regular artifact handler with suffix ".log",
134+
* A machine-readable artifact handler with suffix ".verbose.log".
121135
This format provides extensive information for debugging and detailed analysis.
122136
123137
Args:
124-
log_file_path: The full path to the log file without suffix.
138+
log_file_name: The name of the artifact log file without suffix.
125139
126140
Returns:
127-
The newly created file handlers.
128-
141+
The newly created artifact handlers.
129142
"""
130-
fh = FileHandler(f"{log_file_path}.log")
131-
fh.setFormatter(logging.Formatter(stream_fmt, date_fmt))
132-
self.addHandler(fh)
143+
from framework.testbed_model.artifact import Artifact
144+
145+
log_artifact = Artifact("local", f"{log_file_name}.log")
146+
handler = StreamHandler(log_artifact.open("w"))
147+
handler.setFormatter(logging.Formatter(stream_fmt, date_fmt))
148+
self.addHandler(handler)
133149

134-
verbose_fh = FileHandler(f"{log_file_path}.verbose.log")
135-
verbose_fh.setFormatter(
150+
verbose_log_artifact = Artifact("local", f"{log_file_name}.verbose.log")
151+
verbose_handler = StreamHandler(verbose_log_artifact.open("w"))
152+
verbose_handler.setFormatter(
136153
logging.Formatter(
137154
"%(asctime)s|%(stage)s|%(name)s|%(levelname)s|%(pathname)s|%(lineno)d|"
138155
"%(funcName)s|%(process)d|%(thread)d|%(threadName)s|%(message)s",
139156
datefmt=date_fmt,
140157
)
141158
)
142-
self.addHandler(verbose_fh)
159+
self.addHandler(verbose_handler)
143160

144-
return [fh, verbose_fh]
161+
return [
162+
ArtifactHandler(log_artifact, handler),
163+
ArtifactHandler(verbose_log_artifact, verbose_handler),
164+
]
145165

146-
def _remove_extra_file_handlers(self) -> None:
147-
"""Remove any extra file handlers that have been added to the logger."""
148-
if self._extra_file_handlers:
149-
for extra_file_handler in self._extra_file_handlers:
150-
self.removeHandler(extra_file_handler)
166+
def _remove_extra_artifact_handlers(self) -> None:
167+
"""Remove any extra artifact handlers that have been added to the logger."""
168+
if self._extra_artifact_handlers:
169+
for extra_artifact_handler in self._extra_artifact_handlers:
170+
self.removeHandler(extra_artifact_handler.handler)
151171

152-
self._extra_file_handlers = []
172+
self._extra_artifact_handlers = []
153173

154174

155175
def get_dts_logger(name: str | None = None) -> DTSLogger:

dts/framework/runner.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
The module is responsible for preparing DTS and running the test run.
1010
"""
1111

12-
import os
1312
import sys
1413
import textwrap
1514

@@ -45,9 +44,7 @@ def __init__(self):
4544
sys.exit(e.severity)
4645

4746
self._logger = get_dts_logger()
48-
if not os.path.exists(SETTINGS.output_dir):
49-
os.makedirs(SETTINGS.output_dir)
50-
self._logger.add_dts_root_logger_handlers(SETTINGS.verbose, SETTINGS.output_dir)
47+
self._logger.add_dts_root_logger_handlers(SETTINGS.verbose)
5148

5249
test_suites_result = ResultNode(label="test_suites")
5350
self._result = TestRunResult(test_suites=test_suites_result)

dts/framework/test_run.py

Lines changed: 9 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,6 @@
103103
from collections.abc import Iterable
104104
from dataclasses import dataclass
105105
from functools import cached_property
106-
from pathlib import Path
107106
from types import MethodType
108107
from typing import ClassVar, Protocol, Union
109108

@@ -115,7 +114,6 @@
115114
from framework.settings import SETTINGS
116115
from framework.test_result import Result, ResultNode, TestRunResult
117116
from framework.test_suite import BaseConfig, TestCase, TestSuite
118-
from framework.testbed_model.artifact import Artifact
119117
from framework.testbed_model.capability import (
120118
Capability,
121119
get_supported_capabilities,
@@ -259,11 +257,11 @@ class State(Protocol):
259257
test_run: TestRun
260258
result: TestRunResult | ResultNode
261259

262-
def before(self):
260+
def before(self) -> None:
263261
"""Hook before the state is processed."""
264-
self.logger.set_stage(self.logger_name, self.log_file_path)
262+
self.logger.set_stage(self.logger_name)
265263

266-
def after(self):
264+
def after(self) -> None:
267265
"""Hook after the state is processed."""
268266
return
269267

@@ -276,17 +274,6 @@ def logger(self) -> DTSLogger:
276274
"""A reference to the root logger."""
277275
return get_dts_logger()
278276

279-
def get_log_file_name(self) -> str | None:
280-
"""Name of the log file for this state."""
281-
return None
282-
283-
@property
284-
def log_file_path(self) -> Path | None:
285-
"""Path to the log file for this state."""
286-
if file_name := self.get_log_file_name():
287-
return Path(SETTINGS.output_dir, file_name)
288-
return None
289-
290277
def next(self) -> Union["State", None]:
291278
"""Next state."""
292279

@@ -463,17 +450,18 @@ class TestSuiteState(State):
463450
test_suite: TestSuite
464451
result: ResultNode
465452

466-
def get_log_file_name(self) -> str | None:
467-
"""Get the log file name."""
468-
return self.test_suite.name
469-
470453

471454
@dataclass
472455
class TestSuiteSetup(TestSuiteState):
473456
"""Test suite setup."""
474457

475458
logger_name: ClassVar[str] = "test_suite_setup"
476459

460+
def before(self) -> None:
461+
"""Hook before the state is processed."""
462+
super().before()
463+
self.logger.set_custom_log_file(self.test_suite.name)
464+
477465
@property
478466
def description(self) -> str:
479467
"""State description."""
@@ -591,6 +579,7 @@ def after(self) -> None:
591579
"The remaining test suites will be skipped."
592580
)
593581
self.test_run.blocked = True
582+
self.logger.set_custom_log_file(None)
594583

595584

596585
@dataclass
@@ -602,10 +591,6 @@ class TestCaseState(State):
602591
test_case: type[TestCase]
603592
result: ResultNode
604593

605-
def get_log_file_name(self) -> str | None:
606-
"""Get the log file name."""
607-
return self.test_suite.name
608-
609594

610595
@dataclass
611596
class TestCaseSetup(TestCaseState):

0 commit comments

Comments
 (0)