diff --git a/.bazelrc b/.bazelrc index 7a0465a66a88..4b81aecb52df 100644 --- a/.bazelrc +++ b/.bazelrc @@ -108,6 +108,7 @@ common:ci --config=adms common:ci --config=lint common:ci --noexperimental_convenience_symlinks # not CI-suitable: "These symlinks are only for the user's convenience" common:ci --remote_download_regex=.*/test\.xml$ # force-download test.xml even under --remote_download_outputs=toplevel, so junit collection can read it locally +common:ci --remote_download_regex=.*/test\.log$ # force-download test.log for test2json postprocessing # Project/Language configs -------------------------------------------------------------------------------------- import %workspace%/bazel/configs/go_tests.bazelrc diff --git a/.gitlab/build/bazel/test.yml b/.gitlab/build/bazel/test.yml index 3c99961ca6e3..7f6bd43ac442 100644 --- a/.gitlab/build/bazel/test.yml +++ b/.gitlab/build/bazel/test.yml @@ -14,17 +14,19 @@ aud: ci-identities variables: EXTERNAL_LINKS_PATH: external_links_$CI_JOB_ID.json - FLAVOR: base - BEP_FILE: $CI_PROJECT_DIR/bazel-bep-$FLAVOR.json + BEP_FILE: $CI_PROJECT_DIR/bazel-bep.json + RESULT_JSON: test_output.json after_script: - - dda inv -- -e bazel.collect-junit --flavor=$FLAVOR --output-tgz=junit-${CI_JOB_NAME}.tgz --bep-file=$BEP_FILE || true + - dda inv -- -e bazel.process-test-results --bep-file=$BEP_FILE --junit-tar=junit-${CI_JOB_NAME}.tgz --result-json=$RESULT_JSON - !reference [.upload_junit_source] artifacts: expire_in: 2 weeks when: always paths: + - $RESULT_JSON + - test_output_unified.json - junit-*.tgz - - bazel-bep-*.json + - bazel-bep.json reports: annotations: - $EXTERNAL_LINKS_PATH @@ -42,8 +44,10 @@ bazel:coverage:linux-amd64: - cp "$(bazel info output_path)/_coverage/_coverage_report.dat" "$CI_PROJECT_DIR/coverage-bazel-$CI_JOB_NAME_SLUG.out" artifacts: paths: + - $RESULT_JSON + - test_output_unified.json - junit-*.tgz - - bazel-bep-*.json + - bazel-bep.json - coverage-bazel-*.out variables: KUBERNETES_CPU_REQUEST: 16 @@ -81,8 +85,10 @@ bazel:test:macos-arm64: - !reference [.bazel:test:reporting, after_script] artifacts: paths: + - $RESULT_JSON + - test_output_unified.json - junit-*.tgz - - bazel-bep-*.json + - bazel-bep.json - bazel-exec.log # Windows splits the build from the Go tests, unlike the other platforms: the diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv index aef6e42cfaf6..5ebd07ea3623 100644 --- a/LICENSE-3rdparty.csv +++ b/LICENSE-3rdparty.csv @@ -848,6 +848,9 @@ core,github.com/awslabs/operatorpkg/unstructured,Apache-2.0,"Copyright Amazon.co core,github.com/aymerick/raymond/lexer,MIT,Copyright (c) 2015 Aymerick JEHANNE core,github.com/bahlo/generic-list-go,BSD-3-Clause,Copyright (c) 2009 The Go Authors. All rights reserved core,github.com/bazelbuild/rules_go/go/runfiles,Apache-2.0,Benjamin Staffin | Brian Silverman | Damien Martin-Guillerez | David Chen | David Santiago | David Zbarsky | Fabian Meumertzheim | Google Inc. | Han-Wen Nienhuys | Ian Cottrell | Jake Voytko | Jay Conrod | Josh Powell | Justine Alexandra Roberts Tunney | Kristina Chodorow | Lukacs Berki | Tyler French | Yuki Yugui Sonoda +core,github.com/bazelbuild/rules_go/go/tools/bzltestutil,Apache-2.0,Benjamin Staffin | Brian Silverman | Damien Martin-Guillerez | David Chen | David Santiago | David Zbarsky | Fabian Meumertzheim | Google Inc. | Han-Wen Nienhuys | Ian Cottrell | Jake Voytko | Jay Conrod | Josh Powell | Justine Alexandra Roberts Tunney | Kristina Chodorow | Lukacs Berki | Tyler French | Yuki Yugui Sonoda +core,github.com/bazelbuild/rules_go/go/tools/bzltestutil/chdir,Apache-2.0,Benjamin Staffin | Brian Silverman | Damien Martin-Guillerez | David Chen | David Santiago | David Zbarsky | Fabian Meumertzheim | Google Inc. | Han-Wen Nienhuys | Ian Cottrell | Jake Voytko | Jay Conrod | Josh Powell | Justine Alexandra Roberts Tunney | Kristina Chodorow | Lukacs Berki | Tyler French | Yuki Yugui Sonoda +core,github.com/bazelbuild/rules_go/go/tools/coverdata,Apache-2.0,Benjamin Staffin | Brian Silverman | Damien Martin-Guillerez | David Chen | David Santiago | David Zbarsky | Fabian Meumertzheim | Google Inc. | Han-Wen Nienhuys | Ian Cottrell | Jake Voytko | Jay Conrod | Josh Powell | Justine Alexandra Roberts Tunney | Kristina Chodorow | Lukacs Berki | Tyler French | Yuki Yugui Sonoda core,github.com/bboreham/go-loser,Apache-2.0,bboreham core,github.com/beevik/ntp,BSD-2-Clause,Al Cutter (AlCutter) | Andrey Smirnov (smira) | Anton Tolchanov (knyar) | Ask Bjørn Hansen (abh) | Brett Vickers (beevik) | Christian Cedercrantz (chrisceder) | Christopher Batey (chbatey) | Copyright © 2015-2023 Brett Vickers. All rights reserved | Leonid Evdokimov (darkk) | Meng Zhuo (mengzhuo) | Mikhail Salosin (AlphaB) | Silves-Xiang (silves-xiang) core,github.com/benbjohnson/clock,MIT,Copyright (c) 2014 Ben Johnson diff --git a/bazel/tools/testlogs_to_json/BUILD.bazel b/bazel/tools/testlogs_to_json/BUILD.bazel new file mode 100644 index 000000000000..78c98691e012 --- /dev/null +++ b/bazel/tools/testlogs_to_json/BUILD.bazel @@ -0,0 +1,23 @@ +load("@rules_go//go:def.bzl", "go_binary", "go_library", "go_test") + +go_library( + name = "testlogs_to_json_lib", + srcs = ["testlogs_to_json.go"], + importpath = "github.com/DataDog/datadog-agent/bazel/tools/testlogs_to_json", + visibility = ["//visibility:private"], + deps = ["@rules_go//go/tools/bzltestutil"], +) + +go_binary( + name = "testlogs_to_json", + embed = [":testlogs_to_json_lib"], + visibility = ["//visibility:public"], +) + +# gazelle:dd_agent_go_test off +go_test( + name = "testlogs_to_json_test", + srcs = ["testlogs_to_json_test.go"], + embed = [":testlogs_to_json_lib"], + gotags = [], # keep +) diff --git a/bazel/tools/testlogs_to_json/testlogs_to_json.go b/bazel/tools/testlogs_to_json/testlogs_to_json.go new file mode 100644 index 000000000000..33281ff0cc64 --- /dev/null +++ b/bazel/tools/testlogs_to_json/testlogs_to_json.go @@ -0,0 +1,135 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2026-present Datadog, Inc. + +package main + +import ( + "bufio" + "errors" + "flag" + "fmt" + "io" + "os" + "strings" + + "github.com/bazelbuild/rules_go/go/tools/bzltestutil" +) + +type manifestEntry struct { + pkg string + logPath string +} + +type options struct { + manifestPath string + outputPath string +} + +func main() { + if err := run(os.Args[1:], os.Stdout, os.Stderr); err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(1) + } +} + +func run(args []string, stdout, stderr io.Writer) error { + opts, err := parseFlags(args) + if err != nil { + return err + } + + entries, err := readManifest(opts.manifestPath) + if err != nil { + return err + } + + var out io.Writer = stdout + var outFile *os.File + if opts.outputPath != "" && opts.outputPath != "-" { + outFile, err = os.Create(opts.outputPath) + if err != nil { + return fmt.Errorf("create output %q: %w", opts.outputPath, err) + } + defer outFile.Close() + out = outFile + } + + if err := convert(entries, out); err != nil { + return err + } + + fmt.Fprintf(stderr, "Converted %d Bazel test logs to test2json\n", len(entries)) + return nil +} + +func parseFlags(args []string) (options, error) { + var opts options + fs := flag.NewFlagSet("testlogs_to_json", flag.ContinueOnError) + fs.SetOutput(io.Discard) + fs.StringVar(&opts.manifestPath, "manifest", "", "Path to a tab-separated manifest: \\t") + fs.StringVar(&opts.outputPath, "output", "-", "Path to write test2json JSONL output, or '-' for stdout") + if err := fs.Parse(args); err != nil { + return opts, err + } + if opts.manifestPath == "" { + return opts, errors.New("missing required -manifest") + } + if fs.NArg() != 0 { + return opts, fmt.Errorf("unexpected positional arguments: %s", strings.Join(fs.Args(), " ")) + } + return opts, nil +} + +func readManifest(path string) ([]manifestEntry, error) { + f, err := os.Open(path) + if err != nil { + return nil, fmt.Errorf("open manifest %q: %w", path, err) + } + defer f.Close() + + var entries []manifestEntry + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := scanner.Text() + if strings.TrimSpace(line) == "" { + continue + } + pkg, logPath, ok := strings.Cut(line, "\t") + if !ok || pkg == "" || logPath == "" { + return nil, fmt.Errorf("invalid manifest line: expected \\t, got %q", line) + } + if strings.Contains(logPath, "\t") { + return nil, fmt.Errorf("invalid manifest line: too many tab-separated fields, got %q", line) + } + entries = append(entries, manifestEntry{pkg: pkg, logPath: logPath}) + } + if err := scanner.Err(); err != nil { + return nil, fmt.Errorf("read manifest %q: %w", path, err) + } + return entries, nil +} + +func convert(entries []manifestEntry, out io.Writer) error { + for _, entry := range entries { + f, err := os.Open(entry.logPath) + if err != nil { + return fmt.Errorf("open test log %q for package %s: %w", entry.logPath, entry.pkg, err) + } + + converter := bzltestutil.NewConverter(out, entry.pkg, bzltestutil.Timestamp) + _, copyErr := io.Copy(converter, f) + closeErr := f.Close() + if copyErr != nil { + return fmt.Errorf("convert test log %q for package %s: %w", entry.logPath, entry.pkg, copyErr) + } + if closeErr != nil { + return fmt.Errorf("close test log %q for package %s: %w", entry.logPath, entry.pkg, closeErr) + } + if err := converter.Close(); err != nil { + return fmt.Errorf("close converter for test log %q package %s: %w", entry.logPath, entry.pkg, err) + } + } + return nil +} diff --git a/bazel/tools/testlogs_to_json/testlogs_to_json_test.go b/bazel/tools/testlogs_to_json/testlogs_to_json_test.go new file mode 100644 index 000000000000..6147870d176e --- /dev/null +++ b/bazel/tools/testlogs_to_json/testlogs_to_json_test.go @@ -0,0 +1,111 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2026-present Datadog, Inc. + +package main + +import ( + "bytes" + "encoding/json" + "os" + "path/filepath" + "strings" + "testing" +) + +func TestReadManifest(t *testing.T) { + dir := t.TempDir() + manifestPath := filepath.Join(dir, "manifest.tsv") + if err := os.WriteFile(manifestPath, []byte(strings.Join([]string{ + "", + "github.com/DataDog/datadog-agent/pkg/foo\t/path/to/foo.log", + "github.com/DataDog/datadog-agent/pkg/bar\t/path with spaces/bar.log", + }, "\n")), 0o644); err != nil { + t.Fatal(err) + } + + entries, err := readManifest(manifestPath) + if err != nil { + t.Fatal(err) + } + + want := []manifestEntry{ + {pkg: "github.com/DataDog/datadog-agent/pkg/foo", logPath: "/path/to/foo.log"}, + {pkg: "github.com/DataDog/datadog-agent/pkg/bar", logPath: "/path with spaces/bar.log"}, + } + if len(entries) != len(want) { + t.Fatalf("got %d entries, want %d: %#v", len(entries), len(want), entries) + } + for i := range want { + if entries[i] != want[i] { + t.Fatalf("entry %d = %#v, want %#v", i, entries[i], want[i]) + } + } +} + +func TestReadManifestRejectsInvalidLine(t *testing.T) { + dir := t.TempDir() + manifestPath := filepath.Join(dir, "manifest.tsv") + if err := os.WriteFile(manifestPath, []byte("github.com/DataDog/datadog-agent/pkg/foo /path/to/foo.log\n"), 0o644); err != nil { + t.Fatal(err) + } + + _, err := readManifest(manifestPath) + if err == nil || !strings.Contains(err.Error(), "invalid manifest line") { + t.Fatalf("expected invalid manifest error, got %v", err) + } +} + +func TestConvertMultipleLogs(t *testing.T) { + dir := t.TempDir() + fooLog := filepath.Join(dir, "foo.log") + barLog := filepath.Join(dir, "bar.log") + if err := os.WriteFile(fooLog, []byte("=== RUN TestFoo\n--- PASS: TestFoo (0.01s)\nPASS\n"), 0o644); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(barLog, []byte("=== RUN TestBar\n bar_test.go:12: boom\n--- FAIL: TestBar (0.02s)\nFAIL\n"), 0o644); err != nil { + t.Fatal(err) + } + + var out bytes.Buffer + err := convert([]manifestEntry{ + {pkg: "github.com/DataDog/datadog-agent/pkg/foo", logPath: fooLog}, + {pkg: "github.com/DataDog/datadog-agent/pkg/bar", logPath: barLog}, + }, &out) + if err != nil { + t.Fatal(err) + } + + var events []map[string]any + for _, line := range strings.Split(strings.TrimSpace(out.String()), "\n") { + var event map[string]any + if err := json.Unmarshal([]byte(line), &event); err != nil { + t.Fatalf("invalid JSON line %q: %v", line, err) + } + events = append(events, event) + } + + assertEvent(t, events, "github.com/DataDog/datadog-agent/pkg/foo", "TestFoo", "pass") + assertEvent(t, events, "github.com/DataDog/datadog-agent/pkg/foo", "", "pass") + assertEvent(t, events, "github.com/DataDog/datadog-agent/pkg/bar", "TestBar", "fail") + assertEvent(t, events, "github.com/DataDog/datadog-agent/pkg/bar", "", "fail") +} + +func assertEvent(t *testing.T, events []map[string]any, pkg, testName, action string) { + t.Helper() + for _, event := range events { + if event["Package"] == pkg && event["Action"] == action { + if testName == "" { + if _, ok := event["Test"]; !ok { + return + } + continue + } + if event["Test"] == testName { + return + } + } + } + t.Fatalf("did not find event package=%q test=%q action=%q in %#v", pkg, testName, action, events) +} diff --git a/tasks/bazel.py b/tasks/bazel.py index d733d43b2718..f14cebc9b7ba 100644 --- a/tasks/bazel.py +++ b/tasks/bazel.py @@ -9,6 +9,7 @@ import xml.etree.ElementTree as ET from datetime import datetime from pathlib import Path +from typing import TypedDict from invoke import task @@ -100,27 +101,28 @@ def _label_to_import_path(label: str) -> str: return _IMPORT_PREFIX if not pkg_part else f"{_IMPORT_PREFIX}/{pkg_part}" -def _test_xml_candidates( +def _test_output_candidates( label: str, uri: str, cfg_id: str, local_exec_root: str | None, config_testlogs: dict[str, Path], + output_name: str, ) -> list[Path]: - """Candidate paths for test.xml, in priority order. + """Candidate paths for a Bazel test output, in priority order. BEP URIs are file:// for local actions and bytestream:// for remote-cache - hits; for the latter Bazel still materializes test.xml on disk at - //