blob: 71e90df326d1dce321e5cb0f1926e23d8b073e5d [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package main
import (
"context"
"encoding/hex"
"encoding/json"
"flag"
"io"
"net"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"go.fuchsia.dev/fuchsia/tools/botanist/constants"
"go.fuchsia.dev/fuchsia/tools/build"
"go.fuchsia.dev/fuchsia/tools/debug/covargs/api/llvm"
"go.fuchsia.dev/fuchsia/tools/integration/testsharder"
"go.fuchsia.dev/fuchsia/tools/lib/osmisc"
"go.fuchsia.dev/fuchsia/tools/lib/retry"
"go.fuchsia.dev/fuchsia/tools/testing/runtests"
"go.fuchsia.dev/fuchsia/tools/testing/tap"
"go.fuchsia.dev/fuchsia/tools/testing/testrunner"
)
var (
covargs = flag.String("covargs", "", "Path to covargs binary")
coverageTestBinary = flag.String("coverage-test-binary", "", "Path to instrumented coverage test binary")
coverageTestName = flag.String("coverage-test-name", "", "Name of coverage test")
goldenCoverageFile = flag.String("golden-coverage", "", "Path to golden coverage file")
llvmProfData = flag.String("llvm-profdata", "", "Path to version of llvm-profdata tool")
llvmCov = flag.String("llvm-cov", "", "Path to llvm-cov tool")
)
func TestCoverage(t *testing.T) {
ctx := context.Background()
var addr net.IPAddr
if deviceAddr, ok := os.LookupEnv(constants.DeviceAddrEnvKey); ok {
addrPtr, err := net.ResolveIPAddr("ip", deviceAddr)
if err != nil {
t.Fatalf("failed to parse device address: %s", deviceAddr)
}
addr = *addrPtr
}
// Read SSH key which is required to run a test.
sshKeyFile := os.Getenv(constants.SSHKeyEnvKey)
testOutDir := t.TempDir()
// Create a new fuchsia tester that is responsible for executing the test.
// This is a v2 test, and it uses run-test-suite instead of runtests, so runtests=false.
// TODO(fxbug.dev/77634): When we start treating profiles as artifacts, start using ffx
// with testrunner.NewFFXTester().
tester, err := testrunner.NewFuchsiaSSHTester(ctx, addr, sshKeyFile, testOutDir, "", false)
if err != nil {
t.Fatalf("failed to initialize fuchsia tester: %s", err)
}
defer tester.Close()
test := testsharder.Test{
Test: build.Test{
Name: *coverageTestName,
PackageURL: *coverageTestName,
},
RunAlgorithm: testsharder.StopOnFailure,
Runs: 1,
}
// Run the test over SSH.
result, err := tester.Test(ctx, test, os.Stdout, os.Stdout, "unused-out-dir")
if err != nil {
t.Fatalf("failed to run the test: %s", err)
}
// Create a test outputs object, responsible for producing TAP output,
// and recording data sinks.
outputs, err := testrunner.CreateTestOutputs(tap.NewProducer(io.Discard), testOutDir)
if err != nil {
t.Fatalf("failed to create test outputs: %s", err)
}
// Record data sinks.
if err := outputs.Record(ctx, *result); err != nil {
t.Fatalf("failed to record data sinks: %s", err)
}
// Copy profiles to the host. There might be a delay between when the test finishes and
// data sinks including profiles are available on the target to copy. When that's the case,
// EnsureSinks() does not return an error, and it logs the message.
// Therefore, check whether v2 sinks directory exists to ensure copying is successful.
// When there is a delay, retry.
// TODO(fxbug.dev/77634): When we start treating profiles as artifacts, remove retry.
var sinks []runtests.DataSinkReference
err = retry.Retry(ctx, retry.NewConstantBackoff(5*time.Second), func() error {
if err := tester.EnsureSinks(ctx, sinks, outputs); err != nil {
return err
}
v2SinksDir := filepath.Join(testOutDir, "v2")
_, err := os.ReadDir(v2SinksDir)
if err != nil {
return err
}
return nil
}, nil)
if err != nil {
t.Fatalf("failed to collect data sinks: %s", err)
}
// Close recording of test outputs.
if err := outputs.Close(); err != nil {
t.Fatalf("failed to save test outputs: %s", err)
}
// Find the raw profile that corresponds to the given coverage test name.
rawProfile := ""
if len(outputs.Summary.Tests) != 1 {
t.Fatalf("failed to find the test in the outputs")
}
outputTest := outputs.Summary.Tests[0]
for _, sinks := range outputTest.DataSinks {
// There should be one sink per test.
if len(sinks) != 1 {
t.Fatalf("there should be one sink per test")
}
rawProfile = filepath.Join(testOutDir, sinks[0].File)
}
if rawProfile == "" {
t.Fatalf("failed to find a raw profile")
}
// Prepare llvm-profdata arguments.
args := []string{
"show",
"-binary-ids",
rawProfile,
}
// Read the raw profile using llvm-profdata show command to ensure that it's valid.
showCmd := exec.Command(*llvmProfData, args...)
showCmdOutput, err := showCmd.CombinedOutput()
if err != nil {
if _, ok := err.(*exec.ExitError); ok {
t.Fatalf("cannot read raw profile %s: %s", rawProfile, string(showCmdOutput))
} else {
t.Fatalf("cannot execute %q: %s", showCmd, err)
}
}
// Prepare llvm-profdata arguments.
indexedProfile := filepath.Join(testOutDir, "coverage.profdata")
args = []string{
"merge",
rawProfile,
"-o",
indexedProfile,
}
// Generate an indexed profile using llvm-profdata merge command.
mergeCmd := exec.Command(*llvmProfData, args...)
if mergeCmdOutput, err := mergeCmd.CombinedOutput(); err != nil {
if _, ok := err.(*exec.ExitError); ok {
t.Fatalf("cannot create an indexed profile: %s", string(mergeCmdOutput))
} else {
t.Fatalf("cannot execute %q: %s", mergeCmd, err)
}
}
// Prepare llvm-cov arguments.
args = []string{
"export",
"-summary-only",
"-format=text",
"-instr-profile", indexedProfile,
*coverageTestBinary,
}
// Generate a coverage report via using llvm-cov export command.
exportCmd := exec.Command(*llvmCov, args...)
generatedCoverageOutput, err := exportCmd.CombinedOutput()
if err != nil {
if _, ok := err.(*exec.ExitError); ok {
t.Fatalf("cannot export coverage: %s", string(generatedCoverageOutput))
} else {
t.Fatalf("cannot execute %q: %s", exportCmd, err)
}
}
var generatedCoverageExport llvm.Export
if err := json.Unmarshal(generatedCoverageOutput, &generatedCoverageExport); err != nil {
t.Fatalf("cannot unmarshal generated coverage: %s", err)
}
// Read golden coverage.
goldenCoverage, err := os.ReadFile(*goldenCoverageFile)
if err != nil {
t.Fatalf("cannot find golden coverage file: %s", err)
}
var goldenCoverageExport llvm.Export
if err := json.Unmarshal(goldenCoverage, &goldenCoverageExport); err != nil {
t.Fatalf("cannot unmarshal golden coverage: %s", err)
}
// Compare the generated coverage with a golden coverage.
diff := cmp.Diff(goldenCoverageExport, generatedCoverageExport)
if diff != "" {
t.Fatalf("unexpected coverage (-golden-coverage +generated-coverage): %s", diff)
}
summaryFile := filepath.Join(testOutDir, "summary.json")
if _, err := os.Stat(summaryFile); err != nil {
t.Fatalf("failed to find summary.json: %s", err)
}
splittedOutput := strings.Split((string(showCmdOutput)), "\n")
if len(splittedOutput) < 2 {
t.Fatalf("invalid build id in profile %q: %s", rawProfile, err)
}
embeddedBuildId := splittedOutput[len(splittedOutput)-2]
// Check if embedded build id consists of hex characters.
if _, err = hex.DecodeString(embeddedBuildId); err != nil {
t.Fatalf("invalid build id in profile %q: %s", rawProfile, err)
}
// Create a debug file in the format of xx/yyyyyyyy.debug for covargs.
debugFile := filepath.Join(testOutDir, embeddedBuildId[:2], embeddedBuildId[2:]+".debug")
if err := osmisc.CopyFile(*coverageTestBinary, debugFile); err != nil {
t.Fatalf("failed to create a debug file: %s", err)
}
// Prepare covargs arguments.
args = []string{
"-build-id-dir", testOutDir,
"-llvm-cov", *llvmCov,
"-llvm-profdata", *llvmProfData,
"-output-dir", testOutDir,
"-coverage-report=false",
"-report-dir", testOutDir,
"-save-temps", testOutDir,
"-summary", summaryFile,
}
// Invoke covargs.
covargsCmd := exec.Command(*covargs, args...)
if covargsOutput, err := covargsCmd.CombinedOutput(); err != nil {
if _, ok := err.(*exec.ExitError); ok {
t.Fatalf("failed to run covargs: %s", string(covargsOutput))
} else {
t.Fatalf("cannot execute %q: %s", covargsCmd, err)
}
}
// Read generated coverage.
coverageFile := filepath.Join(testOutDir, "coverage.json")
generatedCoverage, err := os.ReadFile(coverageFile)
if err != nil {
t.Fatalf("cannot read coverage.json: %s", err)
}
if err := json.Unmarshal(generatedCoverage, &generatedCoverageExport); err != nil {
t.Fatalf("cannot unmarshal generated coverage: %s", err)
}
if len(goldenCoverageExport.Data) != 1 || len(generatedCoverageExport.Data) != 1 {
t.Fatalf("failed to export data")
}
// Compare the covargs generated coverage summary section with a golden coverage.
diff = cmp.Diff(goldenCoverageExport.Data[0].Totals, generatedCoverageExport.Data[0].Totals)
if diff != "" {
t.Fatalf("unexpected coverage (-golden-coverage +generated-coverage): %s", diff)
}
}