Bug 1552914 - [mozlog] Update formatters to reflect new known_intermittent field. r=jgraham

The in-tree log formatters have been updated to reflect the new `known_intermittent`
field, ensuring that a status matching a `known_intermittent` status  is not logged as an
unexpected failure. A message is printed when there is a test status that matches this.
A test for known intermittents has been added to the test_formatters, following the
same testing style for pass or fail.

Differential Revision: https://phabricator.services.mozilla.com/D32174

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Nikki S 2019-05-24 13:30:13 +00:00
Родитель 4eddbb64f5
Коммит 7e9e422495
6 изменённых файлов: 222 добавлений и 54 удалений

Просмотреть файл

@ -19,6 +19,7 @@ color_dict = {
'log_process_output': 'blue',
'log_test_status_pass': 'green',
'log_test_status_unexpected_fail': 'red',
'log_test_status_known_intermittent': 'orange',
'time': 'cyan',
'action': 'yellow',
'pid': 'cyan',
@ -136,22 +137,28 @@ class MachFormatter(base.BaseFormatter):
self.summary.current))
return "\n".join(rv)
def _format_expected(self, status, expected):
def _format_expected(self, status, expected, known_intermittent=[]):
if status == expected:
color = self.color_formatter.log_test_status_pass
if expected not in ("PASS", "OK"):
color = self.color_formatter.log_test_status_fail
status = "EXPECTED-%s" % status
else:
color = self.color_formatter.log_test_status_fail
if status in ("PASS", "OK"):
status = "UNEXPECTED-%s" % status
if status in known_intermittent:
color = self.color_formatter.log_test_status_known_intermittent
status = "KNOWN-INTERMITTENT-%s" % status
else:
color = self.color_formatter.log_test_status_fail
if status in ("PASS", "OK"):
status = "UNEXPECTED-%s" % status
return color(status)
def _format_status(self, test, data):
name = data.get("subtest", test)
rv = "%s %s" % (self._format_expected(
data["status"], data.get("expected", data["status"])), name)
rv = "%s %s" % (self._format_expected(data["status"],
data.get("expected", data["status"]),
data.get("known_intermittent", [])),
name)
if "message" in data:
rv += " - %s" % data["message"]
if "stack" in data:
@ -164,6 +171,7 @@ class MachFormatter(base.BaseFormatter):
def _format_suite_summary(self, suite, summary):
count = summary['counts']
logs = summary['unexpected_logs']
intermittent_logs = summary['intermittent_logs']
rv = [
"",
@ -178,7 +186,11 @@ class MachFormatter(base.BaseFormatter):
# Format expected counts
checks = self.summary.aggregate('expected', count, include_skip=False)
rv.append("Expected results: {}".format(sum(checks.values())))
intermittent_checks = self.summary.aggregate('known_intermittent',
count, include_skip=False)
intermittents = sum(intermittent_checks.values())
known = " ({} known intermittents)".format(intermittents) if intermittents else ""
rv.append("Expected results: {}{}".format(sum(checks.values()), known))
# Format skip counts
skip_tests = count["test"]["expected"]["skip"]
@ -192,8 +204,8 @@ class MachFormatter(base.BaseFormatter):
# Format unexpected counts
checks = self.summary.aggregate('unexpected', count)
unexpected_count = sum(checks.values())
rv.append("Unexpected results: {}".format(unexpected_count))
if unexpected_count:
rv.append("Unexpected results: {}".format(unexpected_count))
for key in ('test', 'subtest', 'assert'):
if not count[key]['unexpected']:
continue
@ -203,6 +215,24 @@ class MachFormatter(base.BaseFormatter):
key, sum(count[key]['unexpected'].values()), status_str))
# Format status
if intermittents > 0:
heading = "Known Intermittent Results"
rv.extend(["", self.color_formatter.heading(heading),
self.color_formatter.heading("-" * len(heading))])
if count['subtest']['count']:
for test_id, results in intermittent_logs.items():
test = self._get_file_name(test_id)
rv.append(self.color_formatter.bold(test))
for data in results:
rv.append(" %s" % self._format_status(test, data).rstrip())
else:
for test_id, results in intermittent_logs.items():
test = self._get_file_name(test_id)
assert len(results) == 1
data = results[0]
assert "subtest" not in data
rv.append(self._format_status(test, data).rstrip())
if not any(count[key]["unexpected"] for key in ('test', 'subtest', 'assert')):
rv.append(self.color_formatter.log_test_status_pass("OK"))
else:
@ -232,7 +262,8 @@ class MachFormatter(base.BaseFormatter):
def test_end(self, data):
subtests = self._get_subtest_data(data)
if "expected" in data:
if ("expected" in data and
data["status"] not in data.get("known_intermittent", [])):
parent_unexpected = True
expected_str = ", expected %s" % data["expected"]
else:
@ -267,6 +298,12 @@ class MachFormatter(base.BaseFormatter):
for d in unexpected:
rv += self._format_status(data['test'], d)
intermittents = self.summary.current["intermittent_logs"].get(data["test"])
if intermittents:
rv += "\n"
for d in intermittents:
rv += self._format_status(data['test'], d)
if "expected" not in data and not bool(subtests['unexpected']):
color = self.color_formatter.log_test_status_pass
else:
@ -357,8 +394,10 @@ class MachFormatter(base.BaseFormatter):
if data["status"] == "PASS":
self.status_buffer[test]["pass"] += 1
if 'expected' in data:
if ('expected' in data and
data["status"] not in data.get("known_intermittent", [])):
self.status_buffer[test]["unexpected"] += 1
if self.verbose:
return self._format_status(test, data).rstrip('\n')

Просмотреть файл

@ -180,19 +180,24 @@ class TbplFormatter(BaseFormatter):
if message and message[-1] == "\n":
message = message[:-1]
status = data["status"]
if "expected" in data:
if not message:
message = "- expected %s" % data["expected"]
failure_line = "TEST-UNEXPECTED-%s | %s | %s %s\n" % (
data["status"], data["test"], data["subtest"],
message)
if data["expected"] != "PASS":
info_line = "TEST-INFO | expected %s\n" % data["expected"]
return failure_line + info_line
return failure_line
if status in data.get("known_intermittent", []):
status = "TEST-KNOWN-INTERMITTENT-%s" % status
else:
if not message:
message = "- expected %s" % data["expected"]
failure_line = "TEST-UNEXPECTED-%s | %s | %s %s\n" % (
status, data["test"], data["subtest"],
message)
if data["expected"] != "PASS":
info_line = "TEST-INFO | expected %s\n" % data["expected"]
return failure_line + info_line
return failure_line
return "TEST-%s | %s | %s %s\n" % (
data["status"], data["test"], data["subtest"],
status, data["test"], data["subtest"],
message)
def test_end(self, data):
@ -226,29 +231,34 @@ class TbplFormatter(BaseFormatter):
screenshot_msg = ("\nREFTEST IMAGE: data:image/png;base64,%s" %
screenshots[0]["screenshot"])
status = data['status']
if "expected" in data:
message = data.get("message", "")
if not message:
message = "expected %s" % data["expected"]
if "stack" in data:
message += "\n%s" % data["stack"]
if message and message[-1] == "\n":
message = message[:-1]
message += screenshot_msg
failure_line = "TEST-UNEXPECTED-%s | %s | %s\n" % (
data["status"], test_id, message)
if data["expected"] not in ("PASS", "OK"):
expected_msg = "expected %s | " % data["expected"]
if status in data.get("known_intermittent", []):
status = "TEST-KNOWN-INTERMITTENT-%s" % status
else:
expected_msg = ""
info_line = "TEST-INFO %s%s\n" % (expected_msg, duration_msg)
message = data.get("message", "")
if not message:
message = "expected %s" % data["expected"]
if "stack" in data:
message += "\n%s" % data["stack"]
if message and message[-1] == "\n":
message = message[:-1]
return failure_line + info_line
message += screenshot_msg
sections = ["TEST-%s" % data['status'], test_id]
failure_line = "TEST-UNEXPECTED-%s | %s | %s\n" % (
data["status"], test_id, message)
if data["expected"] not in ("PASS", "OK"):
expected_msg = "expected %s | " % data["expected"]
else:
expected_msg = ""
info_line = "TEST-INFO %s%s\n" % (expected_msg, duration_msg)
return failure_line + info_line
sections = ["TEST-%s" % status, test_id]
if duration_msg:
sections.append(duration_msg)
rv.append(' | '.join(sections) + '\n')

Просмотреть файл

@ -48,10 +48,16 @@ class SummaryHandler(LogHandler):
'unexpected': {
'<status>': int,
},
'known_intermittent': {
'<status>': int,
},
},
},
'unexpected_logs': {
'<test>': [<data>]
},
'intermittent_logs': {
'<test>': [<data>]
}
}
@ -60,6 +66,11 @@ class SummaryHandler(LogHandler):
<test> key is the id as logged by `test_start`. Finally the <data>
field is the log data from any `test_end` or `test_status` log messages
that have an unexpected result.
Mozlog's structuredlog has a `known_intermittent` field indicating if a
`test` and `subtest` <status> are expected to arise intermittently.
Known intermittent results are logged as both as `expected` and
`known_intermittent`.
"""
return self.summary[suite]
@ -75,7 +86,7 @@ class SummaryHandler(LogHandler):
@classmethod
def aggregate(cls, key, counts, include_skip=True):
"""Helper method for aggregating count data by 'key' instead of by 'check'."""
assert key in ('count', 'expected', 'unexpected')
assert key in ('count', 'expected', 'unexpected', 'known_intermittent')
res = defaultdict(int)
for check, val in counts.items():
@ -98,19 +109,23 @@ class SummaryHandler(LogHandler):
'count': 0,
'expected': defaultdict(int),
'unexpected': defaultdict(int),
'known_intermittent': defaultdict(int),
},
'subtest': {
'count': 0,
'expected': defaultdict(int),
'unexpected': defaultdict(int),
'known_intermittent': defaultdict(int),
},
'assert': {
'count': 0,
'expected': defaultdict(int),
'unexpected': defaultdict(int),
'known_intermittent': defaultdict(int),
}
},
'unexpected_logs': OrderedDict(),
'intermittent_logs': OrderedDict(),
}
def test_start(self, data):
@ -118,25 +133,41 @@ class SummaryHandler(LogHandler):
def test_status(self, data):
logs = self.current['unexpected_logs']
intermittent_logs = self.current['intermittent_logs']
count = self.current['counts']
count['subtest']['count'] += 1
if 'expected' in data:
count['subtest']['unexpected'][data['status'].lower()] += 1
if data['test'] not in logs:
logs[data['test']] = []
logs[data['test']].append(data)
if data['status'] not in data.get('known_intermittent', []):
count['subtest']['unexpected'][data['status'].lower()] += 1
if data['test'] not in logs:
logs[data['test']] = []
logs[data['test']].append(data)
else:
count['subtest']['expected'][data['status'].lower()] += 1
count['subtest']['known_intermittent'][data['status'].lower()] += 1
if data['test'] not in intermittent_logs:
intermittent_logs[data['test']] = []
intermittent_logs[data['test']].append(data)
else:
count['subtest']['expected'][data['status'].lower()] += 1
def test_end(self, data):
logs = self.current['unexpected_logs']
intermittent_logs = self.current['intermittent_logs']
count = self.current['counts']
if 'expected' in data:
count['test']['unexpected'][data['status'].lower()] += 1
if data['test'] not in logs:
logs[data['test']] = []
logs[data['test']].append(data)
if data['status'] not in data.get('known_intermittent', []):
count['test']['unexpected'][data['status'].lower()] += 1
if data['test'] not in logs:
logs[data['test']] = []
logs[data['test']].append(data)
else:
count['test']['expected'][data['status'].lower()] += 1
count['test']['known_intermittent'][data['status'].lower()] += 1
if data['test'] not in intermittent_logs:
intermittent_logs[data['test']] = []
intermittent_logs[data['test']].append(data)
else:
count['test']['expected'][data['status'].lower()] += 1

Просмотреть файл

@ -36,6 +36,8 @@ Allowed actions, and subfields:
expected [As for status] - Status that the test was expected to get,
or absent if the test got the expected status
extra - Dictionary of harness-specific extra information e.g. debug info
known_intermittent - List of known intermittent statuses that should
not fail a test. eg. ['FAIL', 'TIMEOUT']
test_status
test - ID for the test
@ -43,6 +45,8 @@ Allowed actions, and subfields:
status [PASS | FAIL | TIMEOUT | NOTRUN | SKIP] - test status
expected [As for status] - Status that the subtest was expected to get,
or absent if the subtest got the expected status
known_intermittent - List of known intermittent statuses that should
not fail a test. eg. ['FAIL', 'TIMEOUT']
process_output
process - PID of the process
@ -356,7 +360,7 @@ class StructuredLogger(object):
Unicode("message", default=None, optional=True),
Unicode("stack", default=None, optional=True),
Dict(Any, "extra", default=None, optional=True),
List(SubStatus, "expected_intermittent", default=None,
List(SubStatus, "known_intermittent", default=None,
optional=True))
def test_status(self, data):
"""
@ -389,7 +393,7 @@ class StructuredLogger(object):
Unicode("message", default=None, optional=True),
Unicode("stack", default=None, optional=True),
Dict(Any, "extra", default=None, optional=True),
List(Status, "expected_intermittent", default=None,
List(Status, "known_intermittent", default=None,
optional=True))
def test_end(self, data):
"""

Просмотреть файл

@ -35,6 +35,7 @@ suite 1
~~~~~~~
Ran 4 checks (1 subtests, 3 tests)
Expected results: 4
Unexpected results: 0
OK
""".lstrip(b'\n')),
('mach', {'verbose': True}, b"""
@ -52,6 +53,7 @@ suite 1
~~~~~~~
Ran 4 checks (1 subtests, 3 tests)
Expected results: 4
Unexpected results: 0
OK
""".lstrip(b'\n')),
],
@ -124,6 +126,62 @@ test_bar
TIMEOUT another subtest
test_baz
UNEXPECTED-PASS test_baz
""".lstrip(b'\n')),
],
'KNOWN-INTERMITTENT': [
('mach', {}, b"""
0:00.00 SUITE_START: running 3 tests
0:00.00 TEST_START: test_foo
0:00.00 TEST_END: FAIL
KNOWN-INTERMITTENT-FAIL test_foo
0:00.00 TEST_START: test_bar
0:00.00 TEST_END: Test OK. Subtests passed 1/1. Unexpected 0
KNOWN-INTERMITTENT-PASS a subtest
0:00.00 TEST_START: test_baz
0:00.00 TEST_END: FAIL
0:00.00 SUITE_END
suite 1
~~~~~~~
Ran 4 checks (1 subtests, 3 tests)
Expected results: 4 (2 known intermittents)
Unexpected results: 0
Known Intermittent Results
--------------------------
test_foo
KNOWN-INTERMITTENT-FAIL test_foo
test_bar
KNOWN-INTERMITTENT-PASS a subtest
OK
""".lstrip(b'\n')),
('mach', {'verbose': True}, b"""
0:00.00 SUITE_START: running 3 tests
0:00.00 TEST_START: test_foo
0:00.00 TEST_END: FAIL
KNOWN-INTERMITTENT-FAIL test_foo
0:00.00 TEST_START: test_bar
0:00.00 KNOWN-INTERMITTENT-PASS a subtest
0:00.00 TEST_END: Test OK. Subtests passed 1/1. Unexpected 0
KNOWN-INTERMITTENT-PASS a subtest
0:00.00 TEST_START: test_baz
0:00.00 TEST_END: FAIL
0:00.00 SUITE_END
suite 1
~~~~~~~
Ran 4 checks (1 subtests, 3 tests)
Expected results: 4 (2 known intermittents)
Unexpected results: 0
Known Intermittent Results
--------------------------
test_foo
KNOWN-INTERMITTENT-FAIL test_foo
test_bar
KNOWN-INTERMITTENT-PASS a subtest
OK
""".lstrip(b'\n')),
],
}
@ -201,6 +259,31 @@ def test_fail(name, opts, expected):
print(result)
assert result == expected
@pytest.mark.parametrize("name,opts,expected", FORMATS['KNOWN-INTERMITTENT'],
ids=ids('KNOWN-INTERMITTENT'))
def test_known_intermittent(name, opts, expected):
buf = BytesIO()
fmt = formatters[name](**opts)
logger = StructuredLogger('test_logger')
logger.add_handler(StreamHandler(buf, fmt))
logger.suite_start(['test_foo', 'test_bar', 'test_baz'])
logger.test_start('test_foo')
logger.test_end('test_foo', 'FAIL', 'PASS', known_intermittent=['FAIL'])
logger.test_start('test_bar')
logger.test_status('test_bar', 'a subtest', 'PASS', 'FAIL',
known_intermittent=['PASS'])
logger.test_end('test_bar', 'OK')
logger.test_start('test_baz')
logger.test_end('test_baz', 'FAIL', 'FAIL', 'expected 0 got 1',
known_intermittent=['PASS'])
logger.suite_end()
result = buf.getvalue()
print("Dumping result for copy/paste:")
print(result)
assert result == expected
if __name__ == '__main__':
mozunit.main()

Просмотреть файл

@ -239,15 +239,16 @@ class TestStructuredLog(BaseStructuredTest):
self.logger.test_end("test1", "OK")
self.logger.suite_end()
def test_status_expected_intermittent(self):
def test_status_known_intermittent(self):
self.logger.suite_start([])
self.logger.test_start("test1")
self.logger.test_status("test1", "subtest name", "fail", expected_intermittent=["FAIL"])
self.logger.test_status("test1", "subtest name", "fail",
known_intermittent=["FAIL"])
self.assert_log_equals({"action": "test_status",
"subtest": "subtest name",
"status": "FAIL",
"expected": "PASS",
"expected_intermittent": ["FAIL"],
"known_intermittent": ["FAIL"],
"test": "test1"})
self.logger.test_end("test1", "OK")
self.logger.suite_end()
@ -874,7 +875,7 @@ Unexpected results: 3
self.assertIn("OK", self.loglines)
self.assertIn("Expected results: 5", self.loglines)
self.assertNotIn("Unexpected results: 0", self.loglines)
self.assertIn("Unexpected results: 0", self.loglines)
def test_process_start(self):
self.logger.process_start(1234)