blob: 08c95b12a937a0bce5bb87235b982e218c209693 [file] [log] [blame]
// Copyright 2023 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 coverage_verifier
import (
"context"
"encoding/json"
"flag"
"io"
"net"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"go.fuchsia.dev/fuchsia/tools/botanist/targets"
"go.fuchsia.dev/fuchsia/tools/build"
"go.fuchsia.dev/fuchsia/tools/debug/covargs/api/llvm"
"go.fuchsia.dev/fuchsia/tools/emulator"
"go.fuchsia.dev/fuchsia/tools/emulator/emulatortest"
"go.fuchsia.dev/fuchsia/tools/integration/testsharder"
"go.fuchsia.dev/fuchsia/tools/lib/ffxutil"
"go.fuchsia.dev/fuchsia/tools/testing/runtests"
"go.fuchsia.dev/fuchsia/tools/testing/tap"
"go.fuchsia.dev/fuchsia/tools/testing/testrunner"
fvdpb "go.fuchsia.dev/fuchsia/tools/virtual_device/proto"
)
var (
configPath = flag.String("config", "", "Path to the configuration file.")
)
const defaultNodename = "fuchsia-5254-0012-3456"
type Binary struct {
Ffx string `json:"ffx"`
LlvmCov string `json:"llvm_cov"`
LlvmProfdata string `json:"llvm_profdata"`
LlvmCxxFilt string `json:"llvm_cxxfilt"`
Fvm string `json:"fvm"`
}
type Function struct {
Name string `json:"name"`
Count int `json:"count"`
}
type Expectations struct {
Source string `json:"source"`
Functions []Function `json:"functions"`
}
type TestInfo struct {
Path string `json:"path"`
Name string `json:"name"`
ZbiImage string `json:"zbi_image"`
BlockImage string `json:"block_image"`
SshKeyFile string `json:"ssh_key,omitempty"`
}
type Config struct {
Bin Binary
Test TestInfo
Expectations []Expectations
}
func execDir(t *testing.T) string {
ex, err := os.Executable()
if err != nil {
t.Fatal(err)
}
return filepath.Dir(ex)
}
func ParseConfiguration(t *testing.T) Config {
data, err := os.ReadFile(*configPath)
if err != nil {
t.Fatalf("Failed to read configuration file at '%s'", *configPath)
}
var config Config
err = json.Unmarshal(data, &config)
if err != nil {
t.Fatalf("Failed to parse JSON config. Reason: %s", err)
}
return config
}
func GetCoverageDataFromTest(t *testing.T, outDir string, config *Config) []string {
exDir := execDir(t)
distro := emulatortest.UnpackFrom(t, filepath.Join(exDir, "test_data"), emulator.DistributionParams{
Emulator: emulator.Qemu,
})
arch := distro.TargetCPU()
device := emulator.DefaultVirtualDevice(string(arch))
// Resize image to avoid problems due to trimmed FVM if FVM.
resizeImage := distro.ResizeRawImage(config.Test.BlockImage, config.Bin.Fvm)
if len(resizeImage) == 0 {
t.Fatalf("Failed to resize image.")
}
// FVM/FXFS path
device.Drive = &fvdpb.Drive{
Id: "disk00",
Image: resizeImage,
IsFilename: true,
Device: &fvdpb.Device{Model: "virtio-blk-pci"},
}
// ZBI path
device.Initrd = config.Test.ZbiImage
// Network
device.Hw.NetworkDevices = append(device.Hw.NetworkDevices, &fvdpb.Netdev{
Id: "qemu",
Kind: "tap",
Device: &fvdpb.Device{Model: "virtio-net-pci"},
})
device.KernelArgs = append(device.KernelArgs, "zircon.nodename="+defaultNodename)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
i := distro.CreateContext(ctx, device)
i.Start()
i.WaitForLogMessage("initializing platform")
// Component manager starts up.
i.WaitForLogMessage("[component_manager] INFO: Component manager is starting up...")
// Netstack is up. This would contain the line number of the log message. Lets avoid it.
i.WaitForLogMessage("[netstack] INFO: main.go")
// Resolve node context.
t.Log("Resolving target IP.")
_, ipv6, err := targets.ResolveIP(context.Background(), defaultNodename)
if err != nil {
t.Fatalf("Failed to resolved node IP address. Reason: %s\n", err)
}
// Create an ssh tester.
var shard testsharder.Test
var sshRunner testrunner.Tester
shard = testsharder.Test{
Test: build.Test{
Name: config.Test.Name,
PackageURL: config.Test.Name,
},
RunAlgorithm: testsharder.StopOnFailure,
Runs: 1,
}
var address net.IPAddr = ipv6
t.Log("Establishing SSH Session.")
runnerCtx := context.Background()
sshRunner, err = testrunner.NewFuchsiaSSHTester(runnerCtx, address, config.Test.SshKeyFile, outDir, "")
if err != nil {
t.Fatalf("Error initializing Fuchsia SSH Test. Reason: %s", err)
}
defer sshRunner.Close()
// Create a new fuchsia tester that is responsible for executing the test.
// This should match what is currently used by the coverage builders. They
// currently use an FFXTester which defaults to the SSHTester if the ffx
// experiment level does not enable using `ffx test`. When updating the ffx
// experiment level on the coverage builders, the level should be updated
// here as well.
ffxExperimentLevel := 1
ffxInstance, err := ffxutil.NewFFXInstance(runnerCtx, config.Bin.Ffx, "", os.Environ(), defaultNodename, config.Test.SshKeyFile, outDir)
if err != nil {
t.Fatalf("Cannot create Ffx instance. Reason: %s", err)
}
ffxRunner, err := testrunner.NewFFXTester(runnerCtx, ffxInstance, sshRunner, outDir, ffxExperimentLevel)
if err != nil {
t.Fatalf("Cannot create Ffx Tester. Reason: %s", err)
}
defer ffxRunner.Close()
testCtx := context.Background()
t.Logf("Running Test: %s", config.Test.Name)
testResult, err := ffxRunner.Test(testCtx, shard, os.Stdout, os.Stdout, "")
testResult, err = ffxRunner.ProcessResult(testCtx, shard, "", testResult, err)
if err != nil {
t.Fatalf("Test execution failed. Reason: %s", err)
}
t.Logf("Retrieving test Artifacts. Output dir : %s\n", outDir)
outputs, err := testrunner.CreateTestOutputs(tap.NewProducer(io.Discard), outDir)
if err != nil {
t.Fatalf("Test output creation failed. Reason: %s", err)
}
if err := outputs.Record(ctx, *testResult); err != nil {
t.Fatalf("Failed to record data sinks. Reason: %s", err)
}
var sinks []runtests.DataSinkReference
err = ffxRunner.EnsureSinks(ctx, sinks, outputs)
if err != nil {
t.Fatalf("Error collecting data sinks from fuchsia device. Reason: %s", err)
}
if err := outputs.Close(); err != nil {
t.Fatalf("Failed to save test outputs. Reason: %s", err)
}
t.Log("Processing retrieved profiles.")
t.Logf("Test count: %d", len(outputs.Summary.Tests))
var rawProfiles []string
for curr, test := range outputs.Summary.Tests {
t.Logf("Test[%d] Name %s Sink count: %d", curr, test.Name, len(test.DataSinks))
for _, sinks := range test.DataSinks {
for _, sink := range sinks {
pathComponents := strings.Split(sink.Name, string(os.PathSeparator))
if pathComponents[0] == "llvm-profile" {
rawProfiles = append(rawProfiles, filepath.Join(outDir, sink.File))
} else {
t.Logf("Unexpected data sink: %s", pathComponents[0])
}
}
}
}
return rawProfiles
}
func GetMergedProfiles(t *testing.T, config *Config, profRaws []string, outDir string) string {
if len(profRaws) < 1 {
t.Fatal("Cannot execute llvm-profdata. No profraw files provided.")
}
profRawPaths :=
strings.Join(profRaws, " ")
validateCmd := []string{
"show",
"-binary-ids",
profRawPaths,
}
showCmd := exec.Command(config.Bin.LlvmProfdata, validateCmd...)
t.Logf("Preparing llvm-profraw validation: %s", showCmd.String())
showCmdOutput, err := showCmd.CombinedOutput()
if err != nil {
if _, ok := err.(*exec.ExitError); ok {
t.Fatalf("Failed to read profraw file. Reason: %s", string(showCmdOutput))
} else {
t.Fatalf("Failed to execute %q. Reason: %s", showCmd, err)
}
}
t.Logf("Data Validated.")
mergeOutput := filepath.Join(outDir, "coverage.profdata")
mergeCmds := []string{
"merge",
profRawPaths,
"-o",
mergeOutput,
}
mergeCmd := exec.Command(config.Bin.LlvmProfdata, mergeCmds...)
t.Logf("Merging profiles: %s", mergeCmd.String())
if mergeCmdOutput, err := mergeCmd.CombinedOutput(); err != nil {
if _, ok := err.(*exec.ExitError); ok {
t.Fatalf("Failed to merge profraw files. Reason: %s", string(mergeCmdOutput))
} else {
t.Fatalf("Failed to execute %q. Reason: %s", mergeCmd, err)
}
}
t.Logf("Profiles merged successfully. Merge output in %s", mergeOutput)
return mergeOutput
}
func ConvertMergedProfilesToJson(t *testing.T, mergedOutput string, config *Config, outDir string) llvm.Export {
exportCmds := []string{
"export",
"-format=text",
"-instr-profile", mergedOutput,
config.Test.Path,
}
// Append sources to filter output only touching the files we care about.
exportCmds = append(exportCmds, "-sources")
for _, exp := range config.Expectations {
exportCmds = append(exportCmds, exp.Source)
}
exportCmd := exec.Command(config.Bin.LlvmCov, exportCmds...)
t.Logf("Exporting coverage data as JSON: %s", exportCmd.String())
exportCmdOutput, err := exportCmd.CombinedOutput()
if err != nil {
if _, ok := err.(*exec.ExitError); ok {
t.Fatalf("Failed to export merged profiles. Reason: %s", string(exportCmdOutput))
} else {
t.Fatalf("Failed to execute %q. Reason: %s", exportCmd, err)
}
}
var coverageJson llvm.Export
if err = json.Unmarshal(exportCmdOutput, &coverageJson); err != nil {
t.Fatalf("Failed to parse generated coverage json. Reason: %s", err)
}
return coverageJson
}
func demangleName(t *testing.T, config *Config, name *string) string {
demangleCmd := exec.Command(config.Bin.LlvmCxxFilt, *name)
demangledOutput, err := demangleCmd.Output()
if err != nil {
t.Fatalf("Failed to demangle name. Reason: %s", err)
}
return strings.TrimSpace(string(demangledOutput))
}
func TestCoverage(t *testing.T) {
outDir := t.TempDir()
cfg := ParseConfiguration(t)
//profraws
profrawPaths := GetCoverageDataFromTest(t, outDir, &cfg)
mergedProfiles := GetMergedProfiles(t, &cfg, profrawPaths, outDir)
coverageJson := ConvertMergedProfilesToJson(t, mergedProfiles, &cfg, outDir)
type ExpectCheck struct {
count int
actualCount int
found bool
}
sourceToFunction := map[string]map[string]ExpectCheck{}
for _, expectation := range cfg.Expectations {
for _, functionExpectation := range expectation.Functions {
var expectedFunction ExpectCheck
expectedFunction.count = functionExpectation.Count
expectedFunction.actualCount = 0
expectedFunction.found = false
if _, exists := sourceToFunction[expectation.Source]; !exists {
sourceToFunction[expectation.Source] = map[string]ExpectCheck{}
}
sourceToFunction[expectation.Source][functionExpectation.Name] = expectedFunction
}
}
for _, data := range coverageJson.Data {
for _, function := range data.Functions {
for _, filename := range function.Filenames {
if symbolToCount, contained := sourceToFunction[filename]; contained {
demangledName := demangleName(t, &cfg, &function.Name)
if expectedFunction, contained := symbolToCount[demangledName]; contained {
expectedFunction.found = true
expectedFunction.actualCount = function.Count
sourceToFunction[filename][demangledName] = expectedFunction
}
}
}
}
}
failures := 0
for filename, symbolNameToExpectation := range sourceToFunction {
for symbolName, expectedFunction := range symbolNameToExpectation {
if !expectedFunction.found {
t.Logf("Symbol %s in file %s was not found in coverage data.", symbolName, filename)
failures++
continue
}
if expectedFunction.actualCount != expectedFunction.count {
t.Logf("Symbol %s in file %s actualCount %d and expectedCount of %d.", symbolName, filename, expectedFunction.actualCount, expectedFunction.count)
failures++
continue
}
}
}
if failures > 0 {
t.Fatal("Failed to meet all expectations.")
}
}