Skip to content

Commit 8bdef2a

Browse files
Merge pull request #217 from PyFixate/fix-logging-encoding-errors
Fix encoding errors in csv-writer and logger
2 parents 37e7652 + ce28cb4 commit 8bdef2a

File tree

5 files changed

+79
-85
lines changed

5 files changed

+79
-85
lines changed

docs/release-notes.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ Improvements
1414
############
1515

1616
- fxconfig now prevents duplicate entries from being added to the config file.
17+
- csv-writer thread crash will now abort the test.
18+
- UTF-8 encoding is now explicitly used for the csv test log and the debug log file. Improves reliability.
1719

1820
*************
1921
Version 0.6.3

src/fixate/main.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import fixate.config
1313
from fixate.core.exceptions import SequenceAbort
1414
from fixate.core.ui import user_info_important, user_serial, user_ok
15-
from fixate.reporting import register_csv, unregister_csv
1615
from fixate.ui_cmdline import register_cmd_line, unregister_cmd_line
1716
import fixate.sequencer
1817

@@ -314,8 +313,6 @@ def ui_run(self):
314313
]
315314
except (AttributeError, KeyError):
316315
pass
317-
register_csv()
318-
self.sequencer.status = "Running"
319316

320317
self.sequencer.run_sequence()
321318
if not self.sequencer.non_interactive:
@@ -328,7 +325,6 @@ def ui_run(self):
328325
input(traceback.print_exc())
329326
raise
330327
finally:
331-
unregister_csv()
332328
if serial_response == "ABORT_FORCE" or test_selector == "ABORT_FORCE":
333329
return ReturnCodes.ABORTED
334330
if serial_number is None:
@@ -410,7 +406,7 @@ def run_main_program(test_script_path=None, main_args=None):
410406
args.diagnostic_log_dir.mkdir(parents=True, exist_ok=True)
411407

412408
handler = RotateEachInstanceHandler(
413-
args.diagnostic_log_dir / "fixate.log", backupCount=10
409+
args.diagnostic_log_dir / "fixate.log", backupCount=10, encoding="utf-8"
414410
)
415411
handler.setFormatter(
416412
logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")

src/fixate/reporting/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
from fixate.reporting.csv import register_csv, unregister_csv
1+
from fixate.reporting.csv import CSVWriter

src/fixate/reporting/csv.py

Lines changed: 55 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -115,45 +115,55 @@ class CSVWriter:
115115
def __init__(self):
116116
self.csv_queue = Queue()
117117
self.csv_writer = None
118-
self.reporting = CsvReporting()
118+
119+
self.exception_in_test = False
120+
self.failed = False
121+
self.chk_cnt = 0
122+
self.csv_path = ""
123+
self.test_module = None
124+
self.start_time = None
125+
self.current_test = None
126+
self.data = fixate.config.get_config_dict()
127+
self.data.update(fixate.config.get_plugin_data("plg_csv"))
128+
self.exception = None
129+
130+
self._topics = [
131+
(self.test_start, "Test_Start"),
132+
(self.test_comparison, "Check"),
133+
(self.test_exception, "Test_Exception"),
134+
(self.test_complete, "Test_Complete"),
135+
(self.sequence_update, "Sequence_Update"),
136+
(self.sequence_complete, "Sequence_Complete"),
137+
(self.user_wait_start, "UI_block_start"),
138+
(self.user_wait_end, "UI_block_end"),
139+
(self.driver_open, "driver_open"),
140+
]
119141

120142
def install(self):
121-
self.csv_writer = ExcThread(
122-
target=self._csv_write, args=(self.csv_queue,), name="csv-writer"
123-
)
143+
self.csv_writer = ExcThread(target=self._csv_write, name="csv-writer")
124144
self.csv_writer.start()
125145

146+
for callback, topic in self._topics:
147+
pub.subscribe(callback, topic)
148+
126149
def uninstall(self):
150+
for callback, topic in self._topics:
151+
pub.unsubscribe(callback, topic)
152+
127153
if self.csv_writer:
128154
self.csv_queue.put(None)
129155
self.csv_writer.join()
130156
self.csv_writer = None
131157

132-
def _csv_write(self, cmd_q):
133-
while True:
134-
line = cmd_q.get()
135-
if line is None:
136-
break # Command send to close csv_writer
137-
try:
138-
os.makedirs(os.path.dirname(self.reporting.csv_path))
139-
except OSError as e:
140-
pass
141-
with open(self.reporting.csv_path, "a+", newline="") as f:
142-
writer = csv.writer(f, quoting=csv.QUOTE_MINIMAL)
143-
writer.writerow(line)
144-
158+
def ensure_alive(self):
159+
if self.exception:
160+
raise RuntimeError(
161+
f"Exception in {self.csv_writer.name} thread"
162+
) from self.exception
145163

146-
class CsvReporting:
147-
def __init__(self):
148-
self.exception_in_test = False
149-
self.failed = False
150-
self.chk_cnt = 0
151-
self.csv_path = ""
152-
self.test_module = None
153-
self.start_time = None
154-
self.current_test = None
155-
self.data = fixate.config.get_config_dict()
156-
self.data.update(fixate.config.get_plugin_data("plg_csv"))
164+
if not self.csv_writer.is_alive():
165+
# If thread has exited without throwing an exception
166+
raise RuntimeError("csv-writer thread not active")
157167

158168
def sequence_update(self, status):
159169
# Do Start Sequence Reporting
@@ -336,59 +346,26 @@ def extract_test_parameters(test_cls):
336346
keys = sorted(set(test_cls.__dict__) - set(comp.__dict__))
337347
return [(key, test_cls.__dict__[key]) for key in keys]
338348

349+
def _csv_write(self):
350+
while True:
351+
line = self.csv_queue.get()
352+
if line is None:
353+
break # Command send to close csv_writer
354+
try:
355+
os.makedirs(os.path.dirname(self.csv_path))
356+
except OSError as e:
357+
pass
358+
with open(self.csv_path, "a+", newline="", encoding="utf-8") as f:
359+
writer = csv.writer(f, quoting=csv.QUOTE_MINIMAL)
360+
try:
361+
writer.writerow(line)
362+
except Exception as e:
363+
self.exception = e
364+
339365
def _write_line_to_csv(self, line):
340366
"""
341367
:param line:
342368
single line of data with each column as an element in the list
343369
:return:
344370
"""
345-
global writer
346-
writer.csv_queue.put(line)
347-
# try:
348-
# os.makedirs(self.csv_dir)
349-
# except OSError:
350-
# pass
351-
# with open(self.csv_path, 'a+', newline='') as f:
352-
# writer = csv.writer(f, quoting=csv.QUOTE_MINIMAL)
353-
# writer.writerow(line)
354-
355-
356-
writer = None
357-
358-
359-
def register_csv():
360-
"""
361-
:param csv_dir: Base directory for for csv file
362-
:param args: Args as parsed into the command line interface
363-
:return:
364-
"""
365-
global writer
366-
writer = CSVWriter()
367-
writer.install()
368-
pub.subscribe(writer.reporting.test_start, "Test_Start")
369-
pub.subscribe(writer.reporting.test_comparison, "Check")
370-
pub.subscribe(writer.reporting.test_exception, "Test_Exception")
371-
pub.subscribe(writer.reporting.test_complete, "Test_Complete")
372-
pub.subscribe(writer.reporting.sequence_update, "Sequence_Update")
373-
pub.subscribe(writer.reporting.sequence_complete, "Sequence_Complete")
374-
pub.subscribe(writer.reporting.user_wait_start, "UI_block_start")
375-
pub.subscribe(writer.reporting.user_wait_end, "UI_block_end")
376-
pub.subscribe(writer.reporting.driver_open, "driver_open")
377-
378-
379-
def unregister_csv():
380-
"""
381-
Note, will disable the final result eg. Unit Passed
382-
:return:
383-
"""
384-
global writer
385-
if writer is not None:
386-
pub.unsubscribe(writer.reporting.test_start, "Test_Start")
387-
pub.unsubscribe(writer.reporting.test_comparison, "Check")
388-
pub.unsubscribe(writer.reporting.test_exception, "Test_Exception")
389-
pub.unsubscribe(writer.reporting.test_complete, "Test_Complete")
390-
pub.unsubscribe(writer.reporting.sequence_update, "Sequence_Update")
391-
pub.unsubscribe(writer.reporting.sequence_complete, "Sequence_Complete")
392-
pub.unsubscribe(writer.reporting.user_wait_start, "UI_block_start")
393-
pub.unsubscribe(writer.reporting.user_wait_end, "UI_block_end")
394-
writer.uninstall()
371+
self.csv_queue.put(line)

src/fixate/sequencer.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from fixate.core.exceptions import SequenceAbort, CheckFail
77
from fixate.core.ui import user_retry_abort_fail
88
from fixate.core.checks import CheckResult
9+
from fixate.reporting import CSVWriter
910

1011
STATUS_STATES = ["Idle", "Running", "Paused", "Finished", "Restart", "Aborted"]
1112

@@ -109,6 +110,7 @@ def __init__(self):
109110
self.context = ContextStack()
110111
self.context_data = {}
111112
self.end_status = "N/A"
113+
self.reporting_service = CSVWriter()
112114

113115
# Sequencer behaviour. Don't ask the user when things to wrong, just marks tests as failed.
114116
# This does not change the behaviour of tests that call out to the user. They will still block as required.
@@ -215,6 +217,7 @@ def run_sequence(self):
215217
Runs the sequence from the beginning to end once
216218
:return:
217219
"""
220+
self.reporting_service.install()
218221
self.status = "Running"
219222
try:
220223
self.run_once()
@@ -225,13 +228,28 @@ def run_sequence(self):
225228
top.current().exit()
226229
self.context.pop()
227230

231+
self.reporting_service.uninstall()
232+
228233
def run_once(self):
229234
"""
230235
Runs through the tests once as are pushed onto the context stack.
231236
Ie. One run through of the tests
232237
Once finished sets the status to Finished
233238
"""
234239
while self.context:
240+
try:
241+
self.reporting_service.ensure_alive()
242+
except Exception as e:
243+
# We cannot log to file. Abort testing and exit
244+
pub.sendMessage(
245+
"Test_Exception",
246+
exception=e,
247+
test_index=self.levels(),
248+
)
249+
pub.sendMessage("Sequence_Abort", exception=e)
250+
self._handle_sequence_abort()
251+
return
252+
235253
if self.status == "Running":
236254
try:
237255
top = self.context.top()
@@ -373,7 +391,8 @@ def retry_prompt(self):
373391
"""Prompt the user when something goes wrong.
374392
375393
For retry return True, to fail return False and to abort raise and abort exception. Respect the
376-
non_interactive flag, which can be set by the command line option --non-interactive"""
394+
non_interactive flag, which can be set by the command line option --non-interactive
395+
"""
377396

378397
if self.non_interactive:
379398
return False

0 commit comments

Comments
 (0)