Skip to content

Commit 9a3a41e

Browse files
committed
implement tests for rotating loggers
1 parent 88a65b5 commit 9a3a41e

File tree

3 files changed

+288
-10
lines changed

3 files changed

+288
-10
lines changed

can/io/logger.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -119,12 +119,19 @@ class BaseRotatingCanLogger(Listener, ABC):
119119
namer: Optional[Callable] = None
120120
rotator: Optional[Callable] = None
121121
rollover_count: int = 0
122-
writer: FileIOMessageWriter
122+
_writer: Optional[FileIOMessageWriter] = None
123123

124124
def __init__(self, *args, **kwargs):
125125
self.writer_args = args
126126
self.writer_kwargs = kwargs
127127

128+
@property
129+
def writer(self) -> FileIOMessageWriter:
130+
if not self._writer:
131+
raise ValueError("Attempt to access writer failed.")
132+
133+
return self._writer
134+
128135
def rotation_filename(self, default_name: StringPathLike):
129136
"""Modify the filename of a log file when rotating.
130137
@@ -176,7 +183,7 @@ def on_message_received(self, msg: Message):
176183

177184
self.writer.on_message_received(msg)
178185

179-
def get_new_writer(self, filename: StringPathLike) -> FileIOMessageWriter:
186+
def get_new_writer(self, filename: StringPathLike):
180187
"""Instantiate a new writer.
181188
182189
:param filename:
@@ -193,7 +200,10 @@ def get_new_writer(self, filename: StringPathLike) -> FileIOMessageWriter:
193200
f'Log format with suffix"{suffix}" is '
194201
f"not supported by {self.__class__.__name__}."
195202
)
196-
return writer_class(filename, *self.writer_args, **self.writer_kwargs)
203+
else:
204+
self._writer = writer_class(
205+
filename, *self.writer_args, **self.writer_kwargs
206+
)
197207

198208
def stop(self):
199209
"""Stop handling new messages.
@@ -227,7 +237,7 @@ class SizedRotatingCanLogger(BaseRotatingCanLogger):
227237
228238
Example::
229239
230-
from can import Notifier, RotatingFileLogger
240+
from can import Notifier, SizedRotatingCanLogger
231241
from can.interfaces.vector import VectorBus
232242
233243
bus = VectorBus(channel=[0], app_name="CANape", fd=True)
@@ -248,7 +258,6 @@ class SizedRotatingCanLogger(BaseRotatingCanLogger):
248258
* .txt :class:`can.Printer`
249259
250260
The log files may be incomplete until `stop()` is called due to buffering.
251-
252261
"""
253262

254263
def __init__(
@@ -261,17 +270,16 @@ def __init__(
261270
:param max_bytes:
262271
The size threshold at which a new log file shall be created. If set to 0, no
263272
rollover will be performed.
264-
265273
"""
266274
super(SizedRotatingCanLogger, self).__init__(*args, **kwargs)
267275

268276
self.base_filename = os.path.abspath(base_filename)
269277
self.max_bytes = max_bytes
270278

271-
self.writer = self.get_new_writer(self.base_filename)
279+
self.get_new_writer(self.base_filename)
272280

273281
def should_rollover(self, msg: Message) -> bool:
274-
if self.max_bytes == 0:
282+
if self.max_bytes <= 0:
275283
return False
276284

277285
if self.writer.file.tell() >= self.max_bytes:
@@ -287,7 +295,7 @@ def do_rollover(self):
287295
dfn = self.rotation_filename(self._default_name())
288296
self.rotate(sfn, dfn)
289297

290-
self.writer = self.get_new_writer(self.base_filename)
298+
self.get_new_writer(self.base_filename)
291299

292300
def _default_name(self) -> StringPathLike:
293301
"""Generate the default rotation filename."""

can/logger.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,9 @@ def main():
153153
print(f"Can Logger (Started on {datetime.now()})")
154154

155155
if results.file_size:
156-
logger = SizedRotatingCanLogger(base_filename=results.log_file, max_bytes=results.file_size)
156+
logger = SizedRotatingCanLogger(
157+
base_filename=results.log_file, max_bytes=results.file_size
158+
)
157159
else:
158160
logger = Logger(filename=results.log_file)
159161

test/test_rotating_loggers.py

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
#!/usr/bin/env python
2+
# coding: utf-8
3+
4+
"""
5+
Test rotating loggers
6+
"""
7+
import os
8+
from pathlib import Path
9+
import tempfile
10+
from unittest.mock import Mock
11+
12+
import pytest
13+
14+
import can
15+
from .data.example_data import generate_message
16+
17+
18+
class TestBaseRotatingCanLogger:
19+
@staticmethod
20+
def _get_instance(*args, **kwargs):
21+
class SubClass(can.io.BaseRotatingCanLogger):
22+
"""Subclass that implements abstract methods for testing."""
23+
24+
def should_rollover(self, msg):
25+
...
26+
27+
def do_rollover(self):
28+
...
29+
30+
return SubClass(*args, **kwargs)
31+
32+
def test_import(self):
33+
assert hasattr(can.io, "BaseRotatingCanLogger")
34+
35+
def test_attributes(self):
36+
assert issubclass(can.io.BaseRotatingCanLogger, can.Listener)
37+
assert hasattr(can.io.BaseRotatingCanLogger, "supported_writers")
38+
assert hasattr(can.io.BaseRotatingCanLogger, "namer")
39+
assert hasattr(can.io.BaseRotatingCanLogger, "rotator")
40+
assert hasattr(can.io.BaseRotatingCanLogger, "rollover_count")
41+
assert hasattr(can.io.BaseRotatingCanLogger, "writer")
42+
assert hasattr(can.io.BaseRotatingCanLogger, "rotation_filename")
43+
assert hasattr(can.io.BaseRotatingCanLogger, "rotate")
44+
assert hasattr(can.io.BaseRotatingCanLogger, "on_message_received")
45+
assert hasattr(can.io.BaseRotatingCanLogger, "get_new_writer")
46+
assert hasattr(can.io.BaseRotatingCanLogger, "stop")
47+
assert hasattr(can.io.BaseRotatingCanLogger, "should_rollover")
48+
assert hasattr(can.io.BaseRotatingCanLogger, "do_rollover")
49+
50+
def test_supported_writers(self):
51+
supported_writers = can.io.BaseRotatingCanLogger.supported_writers
52+
assert supported_writers[".asc"] == can.ASCWriter
53+
assert supported_writers[".blf"] == can.BLFWriter
54+
assert supported_writers[".csv"] == can.CSVWriter
55+
assert supported_writers[".log"] == can.CanutilsLogWriter
56+
assert supported_writers[".txt"] == can.Printer
57+
58+
def test_get_new_writer(self):
59+
logger_instance = self._get_instance()
60+
61+
# access to non existing writer shall raise ValueError
62+
with pytest.raises(ValueError):
63+
_ = logger_instance.writer
64+
65+
with tempfile.TemporaryDirectory() as temp_dir:
66+
logger_instance.get_new_writer(os.path.join(temp_dir, "file.ASC"))
67+
assert isinstance(logger_instance.writer, can.ASCWriter)
68+
logger_instance.stop()
69+
70+
logger_instance.get_new_writer(os.path.join(temp_dir, "file.BLF"))
71+
assert isinstance(logger_instance.writer, can.BLFWriter)
72+
logger_instance.stop()
73+
74+
logger_instance.get_new_writer(os.path.join(temp_dir, "file.CSV"))
75+
assert isinstance(logger_instance.writer, can.CSVWriter)
76+
logger_instance.stop()
77+
78+
logger_instance.get_new_writer(os.path.join(temp_dir, "file.LOG"))
79+
assert isinstance(logger_instance.writer, can.CanutilsLogWriter)
80+
logger_instance.stop()
81+
82+
logger_instance.get_new_writer(os.path.join(temp_dir, "file.TXT"))
83+
assert isinstance(logger_instance.writer, can.Printer)
84+
logger_instance.stop()
85+
86+
def test_rotation_filename(self):
87+
logger_instance = self._get_instance()
88+
89+
default_name = "default"
90+
assert logger_instance.rotation_filename(default_name) == "default"
91+
92+
logger_instance.namer = lambda x: x + "_by_namer"
93+
assert logger_instance.rotation_filename(default_name) == "default_by_namer"
94+
95+
def test_rotate(self):
96+
logger_instance = self._get_instance()
97+
98+
# test without rotator
99+
with tempfile.TemporaryDirectory() as temp_dir:
100+
source = os.path.join(temp_dir, "source.txt")
101+
dest = os.path.join(temp_dir, "dest.txt")
102+
103+
assert os.path.exists(source) is False
104+
assert os.path.exists(dest) is False
105+
106+
logger_instance.get_new_writer(source)
107+
logger_instance.stop()
108+
109+
assert os.path.exists(source) is True
110+
assert os.path.exists(dest) is False
111+
112+
logger_instance.rotate(source, dest)
113+
114+
assert os.path.exists(source) is False
115+
assert os.path.exists(dest) is True
116+
117+
# test with rotator
118+
rotator_func = Mock()
119+
logger_instance.rotator = rotator_func
120+
with tempfile.TemporaryDirectory() as temp_dir:
121+
source = os.path.join(temp_dir, "source.txt")
122+
dest = os.path.join(temp_dir, "dest.txt")
123+
124+
assert os.path.exists(source) is False
125+
assert os.path.exists(dest) is False
126+
127+
logger_instance.get_new_writer(source)
128+
logger_instance.stop()
129+
130+
assert os.path.exists(source) is True
131+
assert os.path.exists(dest) is False
132+
133+
logger_instance.rotate(source, dest)
134+
rotator_func.assert_called_with(source, dest)
135+
136+
# assert that no rotation was performed since rotator_func
137+
# does not do anything
138+
assert os.path.exists(source) is True
139+
assert os.path.exists(dest) is False
140+
141+
def test_stop(self):
142+
"""Test if stop() method of writer is called."""
143+
logger_instance = self._get_instance()
144+
145+
with tempfile.TemporaryDirectory() as temp_dir:
146+
logger_instance.get_new_writer(os.path.join(temp_dir, "file.ASC"))
147+
148+
# replace stop method of writer with Mock
149+
org_stop = logger_instance.writer.stop
150+
mock_stop = Mock()
151+
logger_instance.writer.stop = mock_stop
152+
153+
logger_instance.stop()
154+
mock_stop.assert_called()
155+
156+
# close file.ASC to enable cleanup of temp_dir
157+
org_stop()
158+
159+
def test_on_message_received(self):
160+
logger_instance = self._get_instance()
161+
162+
with tempfile.TemporaryDirectory() as temp_dir:
163+
logger_instance.get_new_writer(os.path.join(temp_dir, "file.ASC"))
164+
165+
"""Test without rollover"""
166+
should_rollover = Mock(return_value=False)
167+
do_rollover = Mock()
168+
writers_on_message_received = Mock()
169+
170+
logger_instance.should_rollover = should_rollover
171+
logger_instance.do_rollover = do_rollover
172+
logger_instance.writer.on_message_received = writers_on_message_received
173+
174+
msg = generate_message(0x123)
175+
logger_instance.on_message_received(msg)
176+
177+
should_rollover.assert_called_with(msg)
178+
do_rollover.assert_not_called()
179+
writers_on_message_received.assert_called_with(msg)
180+
181+
"""Test with rollover"""
182+
should_rollover = Mock(return_value=True)
183+
do_rollover = Mock()
184+
writers_on_message_received = Mock()
185+
186+
logger_instance.should_rollover = should_rollover
187+
logger_instance.do_rollover = do_rollover
188+
logger_instance.writer.on_message_received = writers_on_message_received
189+
190+
msg = generate_message(0x123)
191+
logger_instance.on_message_received(msg)
192+
193+
should_rollover.assert_called_with(msg)
194+
do_rollover.assert_called()
195+
writers_on_message_received.assert_called_with(msg)
196+
197+
# stop writer to enable cleanup of temp_dir
198+
logger_instance.stop()
199+
200+
201+
class TestSizedRotatingCanLogger:
202+
def test_import(self):
203+
assert hasattr(can.io, "SizedRotatingCanLogger")
204+
assert hasattr(can, "SizedRotatingCanLogger")
205+
206+
def test_attributes(self):
207+
assert issubclass(can.SizedRotatingCanLogger, can.io.BaseRotatingCanLogger)
208+
assert hasattr(can.SizedRotatingCanLogger, "supported_writers")
209+
assert hasattr(can.SizedRotatingCanLogger, "namer")
210+
assert hasattr(can.SizedRotatingCanLogger, "rotator")
211+
assert hasattr(can.SizedRotatingCanLogger, "should_rollover")
212+
assert hasattr(can.SizedRotatingCanLogger, "do_rollover")
213+
214+
def test_create_instance(self):
215+
base_filename = "mylogfile.ASC"
216+
max_bytes = 512
217+
218+
with tempfile.TemporaryDirectory() as temp_dir:
219+
logger_instance = can.SizedRotatingCanLogger(
220+
base_filename=os.path.join(temp_dir, base_filename), max_bytes=max_bytes
221+
)
222+
assert Path(logger_instance.base_filename).name == base_filename
223+
assert logger_instance.max_bytes == max_bytes
224+
assert logger_instance.rollover_count == 0
225+
assert isinstance(logger_instance.writer, can.ASCWriter)
226+
227+
logger_instance.stop()
228+
229+
def test_should_rollover(self):
230+
base_filename = "mylogfile.ASC"
231+
max_bytes = 512
232+
233+
with tempfile.TemporaryDirectory() as temp_dir:
234+
logger_instance = can.SizedRotatingCanLogger(
235+
base_filename=os.path.join(temp_dir, base_filename), max_bytes=max_bytes
236+
)
237+
msg = generate_message(0x123)
238+
do_rollover = Mock()
239+
logger_instance.do_rollover = do_rollover
240+
241+
logger_instance.writer.file.tell = Mock(return_value=511)
242+
assert logger_instance.should_rollover(msg) is False
243+
logger_instance.on_message_received(msg)
244+
do_rollover.assert_not_called()
245+
246+
logger_instance.writer.file.tell = Mock(return_value=512)
247+
assert logger_instance.should_rollover(msg) is True
248+
logger_instance.on_message_received(msg)
249+
do_rollover.assert_called()
250+
251+
logger_instance.stop()
252+
253+
def test_logfile_size(self):
254+
base_filename = "mylogfile.ASC"
255+
max_bytes = 1024
256+
msg = generate_message(0x123)
257+
258+
with tempfile.TemporaryDirectory() as temp_dir:
259+
logger_instance = can.SizedRotatingCanLogger(
260+
base_filename=os.path.join(temp_dir, base_filename), max_bytes=max_bytes
261+
)
262+
for _ in range(128):
263+
logger_instance.on_message_received(msg)
264+
265+
for file_path in os.listdir(temp_dir):
266+
assert os.path.getsize(os.path.join(temp_dir, file_path)) <= 1100
267+
268+
logger_instance.stop()

0 commit comments

Comments
 (0)