blob: 68dfcb0e9f28abe8c166f16d55195bfbfed3073d [file] [log] [blame]
# Copyright 2020 The LUCI Authors. All rights reserved.
# Use of this source code is governed under the Apache License, Version 2.0
# that can be found in the LICENSE file.
from builtins import object
from typing import TextIO
import attr
class ClosedFailFile:
"""Sentinel class that replaced the fail file after it's been closed."""
pass
class FailFileAlreadyClosedException(Exception):
"""An exception that is raised when FailTracker.cleanup() is called twice."""
pass
@attr.s
class FailTracker:
"""Tracks which tests have failed since the last run.
Saves the failures to a file, and loads them into an instance variable to run
first in subsequent runs. The file gets cleared only once results start
flowing in, so if a run is canceled early, the fail cache will remain
unharmed.
"""
_fail_file_path = attr.ib()
_fail_file = attr.ib(default=None, type=TextIO)
_recent_fails = attr.ib(factory=set)
def __attrs_post_init__(self):
try:
with open(self._fail_file_path) as f:
self._recent_fails = set(f.read().splitlines())
except IOError:
self._recent_fails = set()
@property
def recent_fails(self):
"""Contains the contents of the .previous_fails file, which is a newline
separated list of recipe.test_case that failed last run."""
return self._recent_fails
def cache_recent_fails(self, test_name, test_result):
"""Caches recently failed test cases to a file
Args:
* test_name: Name of the test that just ran (str)
* test_result: The result of that test (Outcome.Results)
Returns whether the test failed. (bool)
"""
if FailTracker.test_failed(test_result):
if self._fail_file is None:
# This file is left open because we write to it in repeated
# calls to this function. We also don't want to open it prior to this
# time because we don't want to truncate the file before results start
# flowing in.
# self._fail_file is closed by Reporter.final_report() calling
# FailTracker.cleanup(). If the program terminates unexpectedly and it
# isn't closed, it shouldn't be the end of the world.
self._fail_file = open(self._fail_file_path, 'w')
self._fail_file.write('%s\n' % test_name)
self._fail_file.flush()
return True
return False
def cleanup(self):
"""Cleans up the dangling file pointer that this class uses.
This should be called once all test results have been streamed to the
FailTracker
"""
# The file is left open once results start streaming in, thus the 'if'
if isinstance(self._fail_file, ClosedFailFile):
raise FailFileAlreadyClosedException()
elif self._fail_file is not None:
self._fail_file.close()
self._fail_file = ClosedFailFile()
@staticmethod
def test_failed(test_result):
"""Returns whether a test failed."""
error_fields = set(('internal_error', 'bad_test', 'crash_mismatch', 'check',
'diff'))
result_fields = set(
descriptor.name for descriptor, value in test_result.ListFields())
return error_fields & result_fields