Skip to content

Commit 479f0fd

Browse files
committed
impl: add logging to library
improve debugging with logging statements
1 parent 6b5e858 commit 479f0fd

10 files changed

Lines changed: 78 additions & 0 deletions

File tree

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,34 @@ Test framework tools and helpers for performance stack project.
77
This repository provided utilities to assist with test automation, log handling, and result parsing.
88
It is designed to be a set of helper libraries for test frameworks or custom test runners.
99

10+
11+
## Logging
12+
13+
`testing-utils` uses Python's standard logging module. All modules log via a package logger, which you can configure in your application.
14+
By default, the logger uses a `NullHandler`. To see logs, configure logging in your main script:
15+
16+
```python
17+
import logging
18+
logging.basicConfig(level=logging.INFO)
19+
```
20+
21+
or provide logging config in pytest configuration:
22+
23+
```ini
24+
log_cli = true
25+
log_cli_level = DEBUG
26+
log_cli_format = %(asctime)s %(levelname)s %(message)s
27+
log_cli_date_format = %Y-%m-%d %H:%M:%S
28+
```
29+
30+
You can also attach handlers or change the log level for the `testing_utils` logger specifically:
31+
32+
```python
33+
import logging
34+
logger = logging.getLogger("testing_utils")
35+
logger.setLevel(logging.DEBUG)
36+
```
37+
1038
## Features
1139

1240
- **Test scenarios libraries**: Rust and C++ libraries for implementing test scenarios.

testing_utils/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,12 @@
2121
"result_entry",
2222
"scenario",
2323
]
24+
import logging
25+
2426
from . import cap_utils
2527
from .build_tools import BazelTools, BuildTools, CargoTools
2628
from .log_container import LogContainer
2729
from .result_entry import ResultEntry
2830
from .scenario import Scenario, ScenarioResult
31+
32+
logging.getLogger(__name__).addHandler(logging.NullHandler())

testing_utils/build_tools.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@
1717
__all__ = ["BuildTools", "CargoTools", "BazelTools"]
1818

1919
import json
20+
import logging
2021
from abc import ABC, abstractmethod
2122
from pathlib import Path
2223
from subprocess import PIPE, Popen, TimeoutExpired
2324
from typing import Any
2425

2526
import pytest
2627

28+
logger = logging.getLogger(__package__)
2729
# region common
2830

2931

@@ -47,6 +49,11 @@ def __init__(self, option_prefix: str = "", command_timeout: float = 10.0, build
4749
build_timeout : float
4850
Build command timeout in seconds.
4951
"""
52+
logger.debug(
53+
f"Initializing BuildTools: option_prefix={option_prefix}, "
54+
f"command_timeout={command_timeout}, "
55+
f"build_timeout={build_timeout}"
56+
)
5057
if option_prefix:
5158
self._target_path_flag = f"--{option_prefix}-target-path"
5259
self._target_name_flag = f"--{option_prefix}-target-name"
@@ -177,6 +184,7 @@ def metadata(self) -> dict[str, Any]:
177184
"""
178185
# Run command.
179186
command = ["cargo", "metadata", "--format-version", "1"]
187+
logger.debug(f"Running Cargo metadata command: `{' '.join(command)}`")
180188
with Popen(command, stdout=PIPE, text=True) as p:
181189
stdout, _ = p.communicate(timeout=self.command_timeout)
182190
if p.returncode != 0:
@@ -209,6 +217,7 @@ def find_target_path(self, target_name: str, *, expect_exists: bool = True) -> P
209217
if expect_exists and not target_path.exists():
210218
raise RuntimeError(f"Executable not found: {target_path}")
211219

220+
logger.debug(f"Found target path: {target_path}")
212221
return target_path
213222

214223
def build(self, target_name: str) -> Path:
@@ -237,6 +246,7 @@ def build(self, target_name: str) -> Path:
237246

238247
# Run build.
239248
command = ["cargo", "build", "--manifest-path", manifest_path]
249+
logger.debug(f"Running Cargo build command: `{' '.join(command)}`")
240250
with Popen(command, text=True) as p:
241251
_, _ = p.communicate(timeout=self.build_timeout)
242252
if p.returncode != 0:
@@ -284,6 +294,7 @@ def query(self, query: str = "//...") -> list[str]:
284294
"""
285295
# Run command.
286296
command = ["bazel", "query", query]
297+
logger.debug(f"Running Bazel query command: `{' '.join(command)}`")
287298
with Popen(command, stdout=PIPE, text=True) as p:
288299
stdout, _ = p.communicate(timeout=self.command_timeout)
289300
if p.returncode != 0:
@@ -322,6 +333,7 @@ def find_target_path(self, target_name: str, *, expect_exists: bool = True) -> P
322333
"--starlark:expr=target.files_to_run.executable.path",
323334
target_name,
324335
]
336+
logger.debug(f"Running Bazel cquery command: `{' '.join(command)}`")
325337
with Popen(command, stdout=PIPE, text=True) as p:
326338
target_str, _ = p.communicate(timeout=self.command_timeout)
327339
target_str = target_str.strip()
@@ -333,6 +345,7 @@ def find_target_path(self, target_name: str, *, expect_exists: bool = True) -> P
333345
if expect_exists and not target_path.exists():
334346
raise RuntimeError(f"Executable not found: {target_path}")
335347

348+
logger.debug(f"Found target path: {target_path}")
336349
return target_path
337350

338351
def build(self, target_name: str, *options) -> Path:
@@ -346,6 +359,7 @@ def build(self, target_name: str, *options) -> Path:
346359
"""
347360
# Run build.
348361
command = ["bazel", "build", target_name, *options]
362+
logger.debug(f"Running Bazel build command: `{' '.join(command)}`")
349363
with Popen(command, text=True) as p:
350364
_, _ = p.communicate(timeout=self.build_timeout)
351365
if p.returncode != 0:

testing_utils/cap_utils.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,14 @@
44

55
__all__ = ["get_caps", "set_caps"]
66

7+
8+
import logging
79
import re
810
from pathlib import Path
911
from subprocess import DEVNULL, PIPE, Popen
1012

13+
logger = logging.getLogger(__package__)
14+
1115

1216
def get_caps(executable_path: Path | str) -> dict[str, str]:
1317
"""
@@ -42,6 +46,7 @@ def get_caps(executable_path: Path | str) -> dict[str, str]:
4246
for name in names.split(","):
4347
result[name] = perms
4448

49+
logger.debug(f"Capabilities for {executable_path}: {result}")
4550
return result
4651

4752

@@ -70,6 +75,7 @@ def set_caps(executable_path: Path | str, caps: dict[str, str]) -> None:
7075
caps_str,
7176
str(executable_path),
7277
]
78+
logger.debug(f"Setting capabilities: `{' '.join(command)}`")
7379
with Popen(command) as p:
7480
_, _ = p.communicate()
7581
if p.returncode != 0:

testing_utils/log_container.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,16 @@
1616

1717
__all__ = ["LogContainer"]
1818

19+
import logging
1920
import re
2021
from itertools import groupby
2122
from operator import attrgetter
2223
from typing import Any
2324

2425
from .result_entry import ResultEntry
2526

27+
logger = logging.getLogger(__package__)
28+
2629

2730
class _NotSet:
2831
"""
@@ -98,6 +101,7 @@ def _logs_by_field_field_only(self, field: str, *, reverse: bool) -> list[Result
98101

99102
if not reverse:
100103
logs.append(log)
104+
logger.debug(f"Filtered {len(logs)} logs by {'NOT' if reverse else ''}{field=}")
101105
return logs
102106

103107
def _logs_by_field_regex_match(self, field: str, pattern: str, *, reverse: bool) -> list[ResultEntry]:
@@ -131,6 +135,7 @@ def _logs_by_field_regex_match(self, field: str, pattern: str, *, reverse: bool)
131135
found = regex.search(str(found_value)) is not None
132136
if found ^ reverse:
133137
logs.append(log)
138+
logger.debug(f"Filtered {len(logs)} logs by {field=} with {'reversed' if reverse else ''}{pattern=}")
134139
return logs
135140

136141
def _logs_by_field_exact_match(self, field: str, value: Any, *, reverse: bool) -> list[ResultEntry]:
@@ -159,6 +164,7 @@ def _logs_by_field_exact_match(self, field: str, value: Any, *, reverse: bool) -
159164
found = isinstance(found_value, type(value)) and found_value == value
160165
if found ^ reverse:
161166
logs.append(log)
167+
logger.debug(f"Filtered {len(logs)} logs by {field=} with {'reversed' if reverse else ''}{value=}")
162168
return logs
163169

164170
def _logs_by_field(

testing_utils/net/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,5 +18,9 @@
1818
"address",
1919
"connection",
2020
]
21+
import logging
22+
2123
from .address import Address, IPAddress
2224
from .connection import create_connection
25+
26+
logging.getLogger(__name__).addHandler(logging.NullHandler())

testing_utils/net/address.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
#
1111
# SPDX-License-Identifier: Apache-2.0
1212
# *******************************************************************************
13+
import logging
1314
from dataclasses import dataclass
1415
from ipaddress import IPv4Address, IPv6Address, ip_address
1516
from socket import (
@@ -20,6 +21,7 @@
2021
from typing import Any
2122

2223
type IPAddress = IPv4Address | IPv6Address
24+
logger = logging.getLogger(__package__)
2325

2426

2527
@dataclass

testing_utils/net/connection.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,16 @@
1010
#
1111
# SPDX-License-Identifier: Apache-2.0
1212
# *******************************************************************************
13+
import logging
1314
from socket import (
1415
SOCK_STREAM,
1516
socket,
1617
)
1718

1819
from .address import Address
1920

21+
logger = logging.getLogger(__package__)
22+
2023

2124
def create_connection(address: Address, timeout: float | None = 3.0) -> socket:
2225
"""
@@ -32,4 +35,5 @@ def create_connection(address: Address, timeout: float | None = 3.0) -> socket:
3235
s = socket(address.family(), SOCK_STREAM)
3336
s.settimeout(timeout)
3437
s.connect(address.to_raw())
38+
logger.debug(f"Created connection to {address} with {timeout=}s")
3539
return s

testing_utils/result_entry.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,12 @@
1616

1717
__all__ = ["ResultEntry"]
1818

19+
import logging
1920
import re
2021
from typing import Any
2122

23+
logger = logging.getLogger(__package__)
24+
2225

2326
class ResultEntry:
2427
"""

testing_utils/scenario.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
__all__ = ["ScenarioResult", "Scenario"]
1818

1919
import json
20+
import logging
2021
from abc import ABC, abstractmethod
2122
from dataclasses import dataclass
2223
from datetime import timedelta
@@ -30,6 +31,8 @@
3031
from .log_container import LogContainer
3132
from .result_entry import ResultEntry
3233

34+
logger = logging.getLogger(__package__)
35+
3336

3437
@dataclass
3538
class ScenarioResult:
@@ -142,14 +145,17 @@ def _run_command(self, command: list[str], execution_timeout: float, *args, **kw
142145
"""
143146
hang = False
144147
stderr_param = PIPE if self.capture_stderr() else None
148+
logger.info(f"Running command: `{' '.join(command)}` with timeout {execution_timeout}s")
145149
with Popen(command, stdout=PIPE, stderr=stderr_param, text=True) as p:
146150
try:
147151
stdout, stderr = p.communicate(timeout=execution_timeout)
148152
except TimeoutExpired:
153+
logger.warning(f"Command timed out after {execution_timeout}s")
149154
hang = True
150155
p.kill()
151156
stdout, stderr = p.communicate()
152157

158+
logger.debug(f"Command finished with return code {p.returncode}")
153159
return ScenarioResult(stdout, stderr, p.returncode, hang)
154160

155161
@pytest.fixture(scope="class")
@@ -198,4 +204,5 @@ def logs(self, results: ScenarioResult, *args, **kwargs) -> LogContainer:
198204

199205
# Convert messages to list of ResultEntry and create log container.
200206
result_entries = [ResultEntry(msg) for msg in messages]
207+
logger.debug(f"Captured {len(result_entries)} log entries from scenario results")
201208
return LogContainer(result_entries)

0 commit comments

Comments
 (0)