Skip to content

Commit d39ef2d

Browse files
authored
feat: add possibility to separate log file on testCase and testRun (#557)
1 parent f5cb7eb commit d39ef2d

8 files changed

Lines changed: 380 additions & 60 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,7 @@ Commits have to follow following convention: https://www.conventionalcommits.org
249249

250250
- Fix step report show wrong result ([#309](https://github.com/eclipse/kiso-testing/issues/309))
251251
- Update python version ([#314](https://github.com/eclipse/kiso-testing/issues/314))
252-
- Handle proxy autostart when being instanciated after auxiliaries and close channel at last
252+
- Handle proxy autostart when being instantiated after auxiliaries and close channel at last
253253
- Use shutil.move for renaming the merged trace to avoid windows errors
254254
- Add junitxml option to default pytest options ([#321](https://github.com/eclipse/kiso-testing/issues/321))
255255

@@ -590,7 +590,7 @@ Commits have to follow following convention: https://www.conventionalcommits.org
590590

591591
### Documentation
592592

593-
- Add whats new for 0.17.0
593+
- Add what's new for 0.17.0
594594
- Make usage of TestSuite elements more visible
595595
- Add quality goals
596596

@@ -619,8 +619,8 @@ Commits have to follow following convention: https://www.conventionalcommits.org
619619
### Documentation
620620

621621
- Remove not maintained 'list of limitations' section
622-
- Add section whats new ([#38](https://github.com/eclipse/kiso-testing/issues/38))
623-
- Replace all occurences of pipenv with poetry
622+
- Add section what's new ([#38](https://github.com/eclipse/kiso-testing/issues/38))
623+
- Replace all occurrences of pipenv with poetry
624624
- Rework getting_started
625625

626626
### New Features

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "pykiso"
3-
version = "1.4.0"
3+
version = "1.5.0"
44
description = "Embedded integration testing framework."
55
authors = ["Sebastian Fischer <sebastian.fischer@de.bosch.com>"]
66
license = "Eclipse Public License - v 2.0"
@@ -58,7 +58,7 @@ rich = { version = "^13.2.0", optional = true }
5858
robotframework = "3.2.2"
5959
tabulate = ">=0.8.9,<0.10.0"
6060
unittest-xml-reporting = "^3.2.0"
61-
viztracer= "^1.0.1"
61+
viztracer = "^1.0.1"
6262
fastapi = "^0.115.3"
6363
uvicorn = "^0.32.0"
6464
xmltodict = "^0.14.2"

src/pykiso/cli.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,15 @@ def parse_args(self, ctx, args):
183183
multiple=True,
184184
help="path to log-file or folder. If not set will log to STDOUT",
185185
)
186+
@click.option(
187+
"-s",
188+
"--log-file-strategy",
189+
required=False,
190+
default=None,
191+
type=click.Choice(["testRun", "testCase"], case_sensitive=True),
192+
help="Strategy for the log file creation, 'testRun' for one log file per run, 'testCase' for one log file per test case. Default is one log file for all tests."
193+
"This flag need to be used with --log-path option.",
194+
)
186195
@click.option(
187196
"--log-level",
188197
required=False,
@@ -237,7 +246,8 @@ def parse_args(self, ctx, args):
237246
def main(
238247
click_context: click.Context,
239248
test_configuration_file: Tuple[PathType],
240-
log_path: Tuple[PathType] = None,
249+
log_path: Tuple[PathType] | None = None,
250+
log_file_strategy: str | None = None,
241251
log_level: str = "INFO",
242252
report_type: str = "text",
243253
step_report: Optional[PathType] = None,
@@ -258,6 +268,8 @@ def main(
258268
:param click_context: click context
259269
:param test_configuration_file: path to the YAML config file
260270
:param log_path: path to existing directories or files to write logs to
271+
:param log_file_strategy: Strategy for the log file creation, 'testRun' for one log file per run, 'testCase'
272+
for one log file per test case. Default is one log file for all tests."
261273
:param log_level: any of DEBUG, INFO, WARNING, ERROR
262274
:param report_type: if "test", the standard report, if "junit", a junit report is generated
263275
:param variant: allow the user to execute a subset of tests based on variants
@@ -274,6 +286,9 @@ def main(
274286
f"Mismatch: {len(log_path)} log files were provided for {len(test_configuration_file)} yaml configuration files"
275287
)
276288

289+
if log_file_strategy is not None and log_path is None:
290+
raise click.UsageError("The --log-file-strategy option requires the --log-path option to be set.")
291+
277292
if junit is not None:
278293
report_type = "junit"
279294

@@ -302,14 +317,7 @@ def main(
302317
# Run tests
303318
with ConfigRegistry.provide_auxiliaries(cfg_dict):
304319
exit_code = test_execution.execute(
305-
cfg_dict,
306-
report_type,
307-
yaml_name,
308-
user_tags,
309-
step_report,
310-
pattern,
311-
failfast,
312-
junit,
320+
cfg_dict, report_type, yaml_name, user_tags, step_report, pattern, failfast, junit, log_file_strategy
313321
)
314322

315323
for handler in logging.getLogger().handlers:

src/pykiso/test_coordinator/test_execution.py

Lines changed: 24 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,11 @@
2727

2828
import functools
2929
import os
30-
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Union
30+
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Literal, Optional, Union
3131
from unittest import util
3232
from unittest.loader import VALID_MODULE_NAME
3333

34-
from pykiso.test_result.multi_result import MultiTestResult
34+
import pykiso.test_result.multi_result as multi_result
3535

3636
if TYPE_CHECKING:
3737
from ..lib.connectors.cc_pcan_can import CCPCanCan
@@ -120,7 +120,7 @@ def should_skip(test_case: BasicTest) -> bool:
120120
for cli_tag_id, cli_tag_value in usr_tags.items():
121121
# skip any test case that doesn't define a CLI-provided tag name
122122
if cli_tag_id not in test_case.tag.keys():
123-
test_case._skip_msg = f"provided tag {cli_tag_id!r} not present in test tags"
123+
test_case._skip_msg = f"provided tag {cli_tag_id!r} not present in test tags" # noqa: E713
124124
return True
125125
# skip any test case that which tag value don't match the provided tag's value
126126
cli_tag_values = cli_tag_value if isinstance(cli_tag_value, list) else [cli_tag_value]
@@ -415,6 +415,7 @@ def execute(
415415
pattern_inject: Optional[str] = None,
416416
failfast: bool = False,
417417
junit_path: str = "reports",
418+
log_file_strategy: Literal["testRun", "testCase"] | None = None,
418419
) -> int:
419420
"""Create test environment based on test configuration.
420421
@@ -429,6 +430,7 @@ def execute(
429430
run specific tests.
430431
:param failfast: stop the test run on the first error or failure.
431432
:param junit_path: path (file or dir) to junit report
433+
:param log_file_strategy: strategy for the log file creation
432434
433435
:return: exit code corresponding to the result of the test execution
434436
(tests failed, unexpected exception, ...)
@@ -456,6 +458,7 @@ def execute(
456458
)
457459

458460
log_file_path = get_logging_options().log_path
461+
current_time = datetime.today()
459462
# TestRunner selection: generate or not a junit report. Start the tests and publish the results
460463
if report_type == "junit":
461464
report_path = junit_path
@@ -464,30 +467,37 @@ def execute(
464467
junit_report_path = str(full_report_path)
465468
full_report_path.parent.mkdir(exist_ok=True)
466469
else:
467-
junit_report_path = str(full_report_path / Path(time.strftime(f"%Y-%m-%d_%H-%M-%S-{report_name}.xml")))
470+
junit_report_path = str(
471+
full_report_path / Path(current_time.strftime(f"%Y-%m-%d_%H-%M-%S-{report_name}.xml"))
472+
)
468473
full_report_path.mkdir(exist_ok=True)
469474
with open(junit_report_path, "wb") as junit_output, ResultStream(log_file_path) as stream:
470-
test_runner = xmlrunner.XMLTestRunner(
475+
multi_result.test_runner_instance = xmlrunner.XMLTestRunner(
471476
output=junit_output,
472-
resultclass=MultiTestResult(XmlTestResult, BannerTestResult),
477+
resultclass=multi_result.MultiTestResult(
478+
XmlTestResult, BannerTestResult, log_file_strategy=log_file_strategy
479+
),
473480
failfast=failfast,
474481
verbosity=0,
475482
stream=stream,
476483
)
477-
result = test_runner.run(all_tests_to_run)
484+
result = multi_result.test_runner_instance.run(all_tests_to_run)
485+
# Generate the html step report
486+
if step_report is not None:
487+
generate_step_report(result, step_report)
488+
478489
else:
479490
with ResultStream(log_file_path) as stream:
480-
test_runner = unittest.TextTestRunner(
491+
multi_result.test_runner_instance = unittest.TextTestRunner(
481492
stream=stream,
482-
resultclass=MultiTestResult(BannerTestResult),
493+
resultclass=multi_result.MultiTestResult(BannerTestResult, log_file_strategy=log_file_strategy),
483494
failfast=failfast,
484495
verbosity=0,
485496
)
486-
result = test_runner.run(all_tests_to_run)
487-
488-
# Generate the html step report
489-
if step_report is not None:
490-
generate_step_report(result, step_report)
497+
result = multi_result.test_runner_instance.run(all_tests_to_run)
498+
# Generate the html step report
499+
if step_report is not None:
500+
generate_step_report(result, step_report)
491501

492502
exit_code = failure_and_error_handling(result)
493503
except NameError:

src/pykiso/test_result/multi_result.py

Lines changed: 66 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,30 +20,47 @@
2020
"""
2121
from __future__ import annotations
2222

23+
import logging
24+
import unittest
25+
from datetime import datetime
2326
from inspect import signature
24-
from typing import Any, List, Optional, TextIO, Union
25-
from unittest import TestResult
27+
from pathlib import Path
28+
from typing import Any, List, Literal, Optional, Union
29+
from unittest import TestResult, util
2630
from unittest.case import _SubTest
2731

2832
from pykiso.types import ExcInfoType
2933

34+
from ..logging_initializer import get_internal_level, get_logging_options
3035
from ..test_coordinator.test_case import BasicTest
3136
from ..test_coordinator.test_suite import BaseTestSuite
3237
from .text_result import BannerTestResult
3338
from .xml_result import XmlTestResult
3439

40+
test_runner_instance: Optional[unittest.TextTestRunner] = None
41+
3542

3643
class MultiTestResult:
3744
"""Class that can take multiple test result classes and ran the
3845
test for all the classes.
3946
"""
4047

41-
def __init__(self, *result_classes: TestResult):
48+
def __init__(
49+
self,
50+
*result_classes: TestResult,
51+
log_file_strategy: Literal["testRun,testCase"] | None = None,
52+
):
4253
"""Initialize parameter
4354
4455
:param result_classes: test result classes
56+
:param log_file_strategy: strategy to use for the log file
57+
- testRun: log file will be created for each test run
58+
- testCase: log file will be created for each test case
4559
"""
46-
self.result_classes = result_classes
60+
self.result_classes: list[TestResult] = result_classes
61+
self._log_file_strategy = log_file_strategy
62+
self._list_test_results: list[BasicTest] = []
63+
self.current_log_file: Path | None = None
4764

4865
def __call__(self, *args, **kwargs) -> MultiTestResult:
4966
"""Initialize the result classes with the parameters passed in arguments."""
@@ -83,7 +100,7 @@ def __setattr__(self, name: str, value: Any) -> Any:
83100
"""
84101
super().__setattr__(name, value)
85102
# condition to avoid infinite loop with the __getattr__
86-
if name != "result_classes":
103+
if name not in ["result_classes", "_log_file_strategy", "_list_test_results", "current_log_file"]:
87104
for result in self.result_classes:
88105
setattr(result, name, value)
89106

@@ -99,9 +116,43 @@ def startTest(self, test: Union[BasicTest, BaseTestSuite]) -> None:
99116
100117
:param test: running testcase
101118
"""
119+
self.handle_log_file_strategy(test)
120+
102121
for result in self.result_classes:
103122
result.startTest(test)
104123

124+
def handle_log_file_strategy(self, test: Union[BasicTest, BaseTestSuite]) -> None:
125+
"""Handle the log file strategy for the given test.
126+
127+
:param test: The test case or test suite being executed.
128+
"""
129+
if self._log_file_strategy is None:
130+
return
131+
132+
log_options = get_logging_options()
133+
134+
if test.__class__ not in self._list_test_results or self._log_file_strategy == "testRun":
135+
log_file_name = util.strclass(test.__class__).replace(".", "_").replace("-", "_")
136+
if self._log_file_strategy == "testRun":
137+
log_file_name += "_" + test._testMethodName
138+
139+
log_file_name += f"_{datetime.today().strftime('%Y%d%m%H%M%S')}.log"
140+
self.current_log_file = log_options.log_path.parent / log_file_name
141+
142+
# Setup the file handler for logging
143+
log_format = logging.Formatter("%(asctime)s [%(levelname)s] %(module)s:%(lineno)d: %(message)s")
144+
root_logger = logging.getLogger()
145+
file_handler = logging.FileHandler(self.current_log_file, "a+")
146+
file_handler.setFormatter(log_format)
147+
file_handler.name = "strategy_log_file_handler"
148+
# always include internal logs in log files
149+
file_handler.setLevel(get_internal_level(log_options.log_level))
150+
root_logger.addHandler(file_handler)
151+
152+
# Add the log file to the test runner instance to get the banner information
153+
test_runner_instance.stream.stream.multifile_handler.add_file(self.current_log_file)
154+
self._list_test_results.append(test.__class__)
155+
105156
def startTestRun(self) -> None:
106157
"""Call the startTestRun function for all result classes."""
107158
for result in self.result_classes:
@@ -125,6 +176,16 @@ def stopTest(self, test: Union[BasicTest, BaseTestSuite]) -> None:
125176
for result in self.result_classes:
126177
result.stopTest(test)
127178

179+
if self._log_file_strategy is None:
180+
return
181+
# Remove the log file
182+
test_runner_instance.stream.stream.multifile_handler.remove_file(self.current_log_file)
183+
root_logger = logging.getLogger()
184+
for handler in root_logger.handlers:
185+
if handler.name == "strategy_log_file_handler":
186+
root_logger.removeHandler(handler)
187+
handler.close()
188+
128189
def addSuccess(self, test: Union[BasicTest, BaseTestSuite]) -> None:
129190
"""Call the addSuccess function for all result classes.
130191

0 commit comments

Comments
 (0)