add --flaky_test_attempts=3, fail the pipeline when a test is flaky:
diff --git a/buildkite/bazelci.py b/buildkite/bazelci.py index dede4dc..1f59ea1 100644 --- a/buildkite/bazelci.py +++ b/buildkite/bazelci.py
@@ -273,7 +273,10 @@ bep_file = os.path.join(tmpdir, "build_event_json_file.json") exit_code = execute_bazel_test(bazel_binary, config.get("test_flags", []), config.get("test_targets", None), bep_file) - upload_failed_test_logs(bep_file, tmpdir) + # Fail the pipeline if there were any flaky tests. + if has_flaky_tests() and exit_code == 0: + exit_code = 1 + upload_test_logs(bep_file, tmpdir) finally: if tmpdir: shutil.rmtree(tmpdir) @@ -282,6 +285,10 @@ exit(exit_code) +def has_flaky_tests(bep_file): + return len(test_logs_for_status(bep_file, status="FLAKY")) > 0 + + def print_bazel_version_info(bazel_binary): print_collapsed_group("Bazel Info") fail_if_nonzero(execute_command([bazel_binary, "version"])) @@ -380,8 +387,9 @@ print_expanded_group("Test") num_jobs = str(multiprocessing.cpu_count()) common_flags = ["--color=yes", "--keep_going", "--verbose_failures", - "--build_tests_only", "--jobs=" + num_jobs, - "--local_test_jobs=" + num_jobs, "--build_event_json_file=" + bep_file] + "--flaky_test_attempts=3", "--build_tests_only", + "--jobs=" + num_jobs, "--local_test_jobs=" + num_jobs, + "--build_event_json_file=" + bep_file] return execute_command([bazel_binary, "test"] + common_flags + flags + targets) @@ -390,19 +398,16 @@ exit(exitcode) -def upload_failed_test_logs(bep_file, tmpdir): +def upload_test_logs(bep_file, tmpdir): if not os.path.exists(bep_file): return - failed_tests = failed_test_logs(bep_file, tmpdir) - if failed_tests: + test_logs = test_logs_to_upload(bep_file, tmpdir) + if test_logs: cwd = os.getcwd() try: os.chdir(tmpdir) - print_expanded_group("Failed Tests") - for label, _ in failed_tests: - print(label) - print_collapsed_group("Uploading logs of failed tests") - for _, logfile in failed_tests: + print_collapsed_group("Uploading test logs") + for logfile in test_logs: relative_path = os.path.relpath(logfile, tmpdir) fail_if_nonzero(execute_command(["buildkite-agent", "artifact", "upload", relative_path])) @@ -410,27 +415,38 @@ os.chdir(cwd) -def failed_test_logs(bep_file, tmpdir): - failed_tests = test_targets_with_status(bep_file, status="FAILED") - timeout_tests = test_targets_with_status(bep_file, status="TIMEOUT") - test_logs = [] - for label, test_log in (failed_tests + timeout_tests): - new_path = test_label_to_path(tmpdir, label) - os.makedirs(os.path.dirname(new_path), exist_ok=True) - copyfile(test_log, new_path) - test_logs.append((label, new_path)) - return test_logs +def test_logs_to_upload(bep_file, tmpdir): + failed = test_logs_for_status(bep_file, status="FAILED") + timed_out = test_logs_for_status(bep_file, status="TIMEOUT") + flaky = test_logs_for_status(bep_file, status="FLAKY") + # Rename the test.log files to the target that created them + # so that it's easy to associate test.log and target. + new_paths = [] + for label, test_logs in (failed + timed_out + flaky): + attempt = 0 + if len(test_logs) > 1: + attempt = 1 + for test_log in test_logs: + new_path = test_label_to_path(tmpdir, label, attempt) + os.makedirs(os.path.dirname(new_path), exist_ok=True) + copyfile(test_logs, new_path) + new_paths.append(new_path) + attempt = attempt + 1 + return new_paths -def test_label_to_path(tmpdir, label): +def test_label_to_path(tmpdir, label, attempt): # remove leading // path = label[2:] path = path.replace(":", "/") - path = os.path.join(path, "test.log") + if attempt == 0: + path = os.path.join(path, "test.log") + else: + path = os.path.join(path, "attempt_" + str(attempt) + ".log") return os.path.join(tmpdir, path) -def test_targets_with_status(bep_file, status): +def test_logs_for_status(bep_file, status): targets = [] raw_data = "" with open(bep_file) as f: @@ -441,13 +457,16 @@ while pos < len(raw_data): bep_obj, size = decoder.raw_decode(raw_data[pos:]) if "testResult" in bep_obj: + test_target = bep_obj["id"]["testResult"]["label"] test_result = bep_obj["testResult"] if test_result["status"] == status: outputs = test_result["testActionOutput"] + test_logs = [] for output in outputs: if output["name"] == "test.log": - targets.append((bep_obj["id"]["testResult"] - ["label"], urlparse(output["uri"]).path)) + test_logs.append(urlparse(output["uri"]).path) + if test_logs: + targets.append((test_target, test_logs)) pos += size + 1 return targets