Skip to content

Commit 237de89

Browse files
Liviu RauV8 LUCI CQ
authored andcommitted
[resultdb] Add ResultDB indicator
Adds a new indicator that will send every result to ResultDB (and ultimately in a bq table; to be configured later). If we are not running in a ResultDB context we introduce only a minimal overhead by exiting early from indicator. To test these changes in a luci context with ResultDB we activated resultdb feature flag via V8-Recipe-Flags. This feature got implemented in https://crrev.com/c/3925576 . V8-Recipe-Flags: resultdb Bug: v8:13316 Change-Id: I5d98e8f27531b536686a8d63b993313b9d6f62c5 Reviewed-on: https://chromium-review.googlesource.com/c/v8/v8/+/3905385 Commit-Queue: Liviu Rau <[email protected]> Reviewed-by: Alexander Schulze <[email protected]> Cr-Commit-Position: refs/heads/main@{#83672}
1 parent fb3321e commit 237de89

7 files changed

Lines changed: 191 additions & 46 deletions

File tree

.vpython3

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,8 @@ wheel: <
7474
name: "infra/python/wheels/protobuf-py3"
7575
version: "version:3.19.3"
7676
>
77+
78+
wheel: <
79+
name: "infra/python/wheels/requests-py2_py3"
80+
version: "version:2.13.0"
81+
>

tools/testrunner/objects/testcase.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -447,9 +447,13 @@ def cmp(x, y):
447447
(other.suite.name, other.name, other.variant)
448448
)
449449

450-
def __str__(self):
450+
@property
451+
def full_name(self):
451452
return self.suite.name + '/' + self.name
452453

454+
def __str__(self):
455+
return self.full_name
456+
453457

454458
class D8TestCase(TestCase):
455459
def get_shell(self):

tools/testrunner/testproc/indicators.py

Lines changed: 38 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515

1616
def print_failure_header(test, is_flaky=False):
17-
text = [str(test)]
17+
text = [test.full_name]
1818
if test.output_proc.negative:
1919
text.append('[negative]')
2020
if is_flaky:
@@ -24,6 +24,23 @@ def print_failure_header(test, is_flaky=False):
2424
print(output.encode(encoding, errors='replace').decode(encoding))
2525

2626

27+
def formatted_result_output(result):
28+
lines = []
29+
if result.output.stderr:
30+
lines.append("--- stderr ---")
31+
lines.append(result.output.stderr.strip())
32+
if result.output.stdout:
33+
lines.append("--- stdout ---")
34+
lines.append(result.output.stdout.strip())
35+
lines.append("Command: %s" % result.cmd.to_string())
36+
if result.output.HasCrashed():
37+
lines.append("exit code: %s" % result.output.exit_code_string)
38+
lines.append("--- CRASHED ---")
39+
if result.output.HasTimedOut():
40+
lines.append("--- TIMEOUT ---")
41+
return '\n'.join(lines)
42+
43+
2744
class ProgressIndicator():
2845

2946
def __init__(self, context, options, test_count):
@@ -68,19 +85,7 @@ def finished(self):
6885
for test, result, is_flaky in self._failed:
6986
flaky += int(is_flaky)
7087
print_failure_header(test, is_flaky=is_flaky)
71-
if result.output.stderr:
72-
print("--- stderr ---")
73-
print(result.output.stderr.strip())
74-
if result.output.stdout:
75-
print("--- stdout ---")
76-
print(result.output.stdout.strip())
77-
print("Command: %s" % result.cmd.to_string())
78-
if result.output.HasCrashed():
79-
print("exit code: %s" % result.output.exit_code_string)
80-
print("--- CRASHED ---")
81-
crashed += 1
82-
if result.output.HasTimedOut():
83-
print("--- TIMEOUT ---")
88+
print(formatted_result_output(result))
8489
if len(self._failed) == 0:
8590
print("===")
8691
print("=== All tests succeeded")
@@ -230,7 +235,7 @@ def on_test_result(self, test, result):
230235
else:
231236
self._passed += 1
232237

233-
self._print_progress(str(test))
238+
self._print_progress(test.full_name)
234239
if result.has_unexpected_output:
235240
output = result.output
236241
stdout = output.stdout.strip()
@@ -358,10 +363,7 @@ def __init__(self, context, options, test_count, framework_name):
358363
self.test_count = 0
359364

360365
def on_test_result(self, test, result):
361-
if result.is_rerun:
362-
self.process_results(test, result.results)
363-
else:
364-
self.process_results(test, [result])
366+
self.process_results(test, result.as_list)
365367

366368
def process_results(self, test, results):
367369
for run, result in enumerate(results):
@@ -376,7 +378,7 @@ def process_results(self, test, results):
376378
if not result.has_unexpected_output and run == 0:
377379
continue
378380

379-
record = self._test_record(test, result, output, run)
381+
record = self._test_record(test, result, run)
380382
record.update({
381383
"result": test.output_proc.get_outcome(output),
382384
"stdout": output.stdout,
@@ -392,41 +394,33 @@ def result_value(test, result, output):
392394
return ""
393395
return test.output_proc.get_outcome(output)
394396

395-
record = self._test_record(test, result, output, run)
396-
record.update({
397-
"result": result_value(test, result, output),
398-
"marked_slow": test.is_slow,
399-
})
397+
record = self._test_record(test, result, run)
398+
record.update(
399+
result=result_value(test, result, output),
400+
marked_slow=test.is_slow,
401+
)
400402
self.tests.add(record)
401403
self.duration_sum += record['duration']
402404
self.test_count += 1
403405

404-
def _test_record(self, test, result, output, run):
405-
return {
406-
"name": str(test),
407-
"flags": result.cmd.args,
408-
"command": result.cmd.to_string(relative=True),
409-
"run": run + 1,
410-
"exit_code": output.exit_code,
411-
"expected": test.expected_outcomes,
412-
"duration": output.duration,
413-
"random_seed": test.random_seed,
414-
"target_name": test.get_shell(),
415-
"variant": test.variant,
416-
"variant_flags": test.variant_flags,
417-
"framework_name": self.framework_name,
418-
}
406+
def _test_record(self, test, result, run):
407+
record = util.base_test_record(test, result, run)
408+
record.update(
409+
framework_name=self.framework_name,
410+
command=result.cmd.to_string(relative=True),
411+
)
412+
return record
419413

420414
def finished(self):
421415
duration_mean = None
422416
if self.test_count:
423417
duration_mean = self.duration_sum / self.test_count
424418

425419
result = {
426-
"results": self.results,
427-
"slowest_tests": self.tests.as_list(),
428-
"duration_mean": duration_mean,
429-
"test_total": self.test_count,
420+
'results': self.results,
421+
'slowest_tests': self.tests.as_list(),
422+
'duration_mean': duration_mean,
423+
'test_total': self.test_count,
430424
}
431425

432426
with open(self.options.json_test_results, "w") as f:

tools/testrunner/testproc/progress.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from . import base
77
from testrunner.local import utils
88
from testrunner.testproc.indicators import JsonTestProgressIndicator, PROGRESS_INDICATORS
9+
from testrunner.testproc.resultdb import ResultDBIndicator
910

1011

1112
class ResultsTracker(base.TestProcObserver):
@@ -66,7 +67,7 @@ def __init__(self, context, options, framework_name, test_count):
6667
0,
6768
JsonTestProgressIndicator(context, options, test_count,
6869
framework_name))
69-
70+
self.procs.append(ResultDBIndicator(context, options, test_count))
7071
self._requirement = max(proc._requirement for proc in self.procs)
7172

7273
def _on_result_for(self, test, result):

tools/testrunner/testproc/result.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ def is_grouped(self):
1616
def is_rerun(self):
1717
return False
1818

19+
@property
20+
def as_list(self):
21+
return [self]
22+
1923

2024
class Result(ResultBase):
2125
"""Result created by the output processor."""
@@ -112,5 +116,9 @@ def __init__(self, results):
112116
def is_rerun(self):
113117
return True
114118

119+
@property
120+
def as_list(self):
121+
return self.results
122+
115123
def status(self):
116124
return ' '.join(r.status() for r in self.results)
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
# Copyright 2022 the V8 project authors. All rights reserved.
2+
# Use of this source code is governed by a BSD-style license that can be
3+
# found in the LICENSE file.
4+
5+
import json
6+
import logging
7+
import pprint
8+
import requests
9+
import os
10+
11+
from . import base
12+
from .indicators import (
13+
formatted_result_output,
14+
ProgressIndicator,
15+
)
16+
from .util import (
17+
base_test_record,
18+
extract_tags,
19+
strip_ascii_control_characters,
20+
)
21+
22+
23+
class ResultDBIndicator(ProgressIndicator):
24+
25+
def __init__(self, context, options, test_count):
26+
super(ResultDBIndicator, self).__init__(context, options, test_count)
27+
self._requirement = base.DROP_PASS_OUTPUT
28+
self.rpc = ResultDB_RPC()
29+
30+
def on_test_result(self, test, result):
31+
for run, sub_result in enumerate(result.as_list):
32+
self.send_result(test, sub_result, run)
33+
34+
def send_result(self, test, result, run):
35+
# We need to recalculate the observed (but lost) test behaviour.
36+
# `result.has_unexpected_output` indicates that the run behaviour of the
37+
# test matches the expected behaviour irrespective of passing or failing.
38+
result_expected = not result.has_unexpected_output
39+
test_should_pass = not test.is_fail
40+
run_passed = (result_expected == test_should_pass)
41+
rdb_result = {
42+
'testId': strip_ascii_control_characters(test.full_name),
43+
'status': 'PASS' if run_passed else 'FAIL',
44+
'expected': result_expected,
45+
}
46+
47+
if result.output and result.output.duration:
48+
rdb_result.update(duration=f'{result.output.duration}ms')
49+
if result.has_unexpected_output:
50+
formated_output = formatted_result_output(result)
51+
sanitized = strip_ascii_control_characters(formated_output)
52+
# TODO(liviurau): do we have a better presentation data for this?
53+
# Protobuf strings can have len == 2**32.
54+
rdb_result.update(summaryHtml=f'<pre>{sanitized}</pre>')
55+
record = base_test_record(test, result, run)
56+
rdb_result.update(tags=extract_tags(record))
57+
self.rpc.send(rdb_result)
58+
59+
60+
class ResultDB_RPC:
61+
62+
def __init__(self):
63+
self.session = None
64+
luci_context = os.environ.get('LUCI_CONTEXT')
65+
# TODO(liviurau): use a factory method and return None in absence of
66+
# necessary context.
67+
if not luci_context:
68+
logging.warning(
69+
f'No LUCI_CONTEXT found. No results will be sent to ResutDB.')
70+
return
71+
with open(luci_context, mode="r", encoding="utf-8") as f:
72+
config = json.load(f)
73+
sink = config.get('result_sink', None)
74+
if not sink:
75+
logging.warning(
76+
f'No ResultDB sink found. No results will be sent to ResutDB.')
77+
return
78+
self.session = requests.Session()
79+
self.session.headers = {
80+
'Authorization': f'ResultSink {sink.get("auth_token")}',
81+
}
82+
self.url = f'http://{sink.get("address")}/prpc/luci.resultsink.v1.Sink/ReportTestResults'
83+
84+
def send(self, result):
85+
if self.session:
86+
payload = dict(testResults=[result])
87+
try:
88+
self.session.post(self.url, json=payload).raise_for_status()
89+
except Exception as e:
90+
logging.error(f'Request failed: {payload}')
91+
raise e
92+
93+
def __del__(self):
94+
if self.session:
95+
self.session.close()

tools/testrunner/testproc/util.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import logging
88
import os
99
import platform
10+
import re
1011
import signal
1112
import subprocess
1213

@@ -53,6 +54,43 @@ def kill_processes_linux():
5354
logging.exception('Failed to kill process')
5455

5556

57+
def strip_ascii_control_characters(unicode_string):
58+
return re.sub(r'[^\x20-\x7E]', '?', str(unicode_string))
59+
60+
61+
def base_test_record(test, result, run):
62+
record = {
63+
'name': test.full_name,
64+
'flags': result.cmd.args,
65+
'run': run + 1,
66+
'expected': test.expected_outcomes,
67+
'random_seed': test.random_seed,
68+
'target_name': test.get_shell(),
69+
'variant': test.variant,
70+
'variant_flags': test.variant_flags,
71+
}
72+
if result.output:
73+
record.update(
74+
exit_code=result.output.exit_code,
75+
duration=result.output.duration,
76+
)
77+
return record
78+
79+
80+
def extract_tags(record):
81+
tags = []
82+
for k, v in record.items():
83+
if type(v) == list:
84+
tags += [sanitized_kv_dict(k, e) for e in v]
85+
else:
86+
tags.append(sanitized_kv_dict(k, v))
87+
return tags
88+
89+
90+
def sanitized_kv_dict(k, v):
91+
return dict(key=k, value=strip_ascii_control_characters(v))
92+
93+
5694
class FixedSizeTopList():
5795
"""Utility collection for gathering a fixed number of elements with the
5896
biggest value for the given key. It employs a heap from which we pop the

0 commit comments

Comments
 (0)