blob: 12318e25d3d604ba765deddeb88d2b5c2b66e89f [file] [log] [blame]
// Copyright 2020 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
package main
import (
"bytes"
"compress/gzip"
"encoding/json"
"errors"
"flag"
"os"
"path/filepath"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"go.fuchsia.dev/fuchsia/tools/build/ninjago/compdb"
"go.fuchsia.dev/fuchsia/tools/build/ninjago/ninjalog"
)
var testDataDir = flag.String("test_data_dir", "../test_data", "Path to ../test_data/; only used in GN build")
func readAndUnzip(t *testing.T, path string) *gzip.Reader {
f, err := os.Open(path)
if err != nil {
t.Fatalf("Failed to read %q: %v", path, err)
}
t.Cleanup(func() { f.Close() })
unzipped, err := gzip.NewReader(f)
if err != nil {
t.Fatalf("Failed to unzip %q: %v", path, err)
}
t.Cleanup(func() { unzipped.Close() })
return unzipped
}
func TestExtractAndSerializeBuildStats(t *testing.T) {
graph, err := constructGraph(inputs{
ninjalog: readAndUnzip(t, filepath.Join(*testDataDir, "ninja_log.gz")),
compdb: readAndUnzip(t, filepath.Join(*testDataDir, "compdb.json.gz")),
graph: readAndUnzip(t, filepath.Join(*testDataDir, "graph.dot.gz")),
})
if err != nil {
t.Fatalf("Failed to construct graph: %v", err)
}
stats, err := extractBuildStats(&graph, 0)
if err != nil {
t.Fatalf("Failed to extract build stats: %v", err)
}
if len(stats.CriticalPath) == 0 {
t.Errorf("Critical path in stats is emtpy, expect non-empty")
}
if len(stats.Slowests) == 0 {
t.Errorf("Slowest builds in stats is empty, expect non-empty")
}
if len(stats.CatBuildTimes) == 0 {
t.Errorf("Build times by category in stats is empty, expect non-empty")
}
if len(stats.All) == 0 {
t.Errorf("All in stats is empty, expect non-empty")
}
buffer := new(bytes.Buffer)
if err := serializeBuildStats(stats, buffer); err != nil {
t.Fatalf("Failed to serialize build stats: %v", err)
}
var gotStats buildStats
if err := json.NewDecoder(buffer).Decode(&gotStats); err != nil {
t.Fatalf("Failed to deserialize build stats: %v", err)
}
if diff := cmp.Diff(stats, gotStats); diff != "" {
t.Errorf("build stats diff after deserialization (-want, +got):\n%s", diff)
}
}
type stubGraph struct {
steps []ninjalog.Step
err error
}
func (g *stubGraph) PopulatedSteps() ([]ninjalog.Step, error) {
return g.steps, g.err
}
func TestExtractStats(t *testing.T) {
for _, v := range []struct {
name string
minActionBuildTime time.Duration
g stubGraph
want buildStats
}{
{
name: "empty steps",
},
{
name: "successfully extract stats",
minActionBuildTime: 0,
g: stubGraph{
steps: []ninjalog.Step{
{
CmdHash: 1,
Out: "a.o",
Outs: []string{"aa.o", "aaa.o"},
End: 3 * time.Second,
Command: &compdb.Command{Command: "gomacc a.cc"},
OnCriticalPath: true,
Drag: 123 * time.Second,
},
{
CmdHash: 2,
Out: "b.o",
Start: 3 * time.Second,
End: 5 * time.Second,
Command: &compdb.Command{Command: "rustc b.rs"},
OnCriticalPath: true,
Drag: 321 * time.Second,
},
{
CmdHash: 3,
Out: "c.o",
Start: 9 * time.Second,
End: 10 * time.Second,
Command: &compdb.Command{Command: "gomacc c.cc"},
TotalFloat: 789 * time.Second,
},
},
},
want: buildStats{
CriticalPath: []action{
{
Command: "gomacc a.cc",
Outputs: []string{"aa.o", "aaa.o", "a.o"},
End: 3 * time.Second,
Category: "gomacc",
Drag: 123 * time.Second,
},
{
Command: "rustc b.rs",
Outputs: []string{"b.o"},
Start: 3 * time.Second,
End: 5 * time.Second,
Category: "rustc",
Drag: 321 * time.Second,
},
},
Slowests: []action{
{
Command: "gomacc a.cc",
Outputs: []string{"aa.o", "aaa.o", "a.o"},
End: 3 * time.Second,
Category: "gomacc",
Drag: 123 * time.Second,
},
{
Command: "rustc b.rs",
Outputs: []string{"b.o"},
Start: 3 * time.Second,
End: 5 * time.Second,
Category: "rustc",
Drag: 321 * time.Second,
},
{
Command: "gomacc c.cc",
Outputs: []string{"c.o"},
Start: 9 * time.Second,
End: 10 * time.Second,
Category: "gomacc",
TotalFloat: 789 * time.Second,
},
},
CatBuildTimes: []catBuildTime{
{
Category: "gomacc",
Count: 2,
BuildTime: 4 * time.Second,
MinBuildTime: time.Second,
MaxBuildTime: 3 * time.Second,
},
{
Category: "rustc",
Count: 1,
BuildTime: 2 * time.Second,
MinBuildTime: 2 * time.Second,
MaxBuildTime: 2 * time.Second,
},
},
All: []action{
{
Command: "gomacc a.cc",
Outputs: []string{"aa.o", "aaa.o", "a.o"},
End: 3 * time.Second,
Category: "gomacc",
Drag: 123 * time.Second,
},
{
Command: "rustc b.rs",
Outputs: []string{"b.o"},
Start: 3 * time.Second,
End: 5 * time.Second,
Category: "rustc",
Drag: 321 * time.Second,
},
{
Command: "gomacc c.cc",
Outputs: []string{"c.o"},
Start: 9 * time.Second,
End: 10 * time.Second,
Category: "gomacc",
TotalFloat: 789 * time.Second,
},
},
Actions: []action{
{
Command: "gomacc a.cc",
Outputs: []string{"aa.o", "aaa.o", "a.o"},
End: 3 * time.Second,
Category: "gomacc",
Drag: 123 * time.Second,
},
{
Command: "rustc b.rs",
Outputs: []string{"b.o"},
Start: 3 * time.Second,
End: 5 * time.Second,
Category: "rustc",
Drag: 321 * time.Second,
},
{
Command: "gomacc c.cc",
Outputs: []string{"c.o"},
Start: 9 * time.Second,
End: 10 * time.Second,
Category: "gomacc",
TotalFloat: 789 * time.Second,
},
},
TotalBuildTime: 6 * time.Second,
BuildDuration: 10 * time.Second,
},
},
{
name: "filter short actions",
minActionBuildTime: time.Minute,
g: stubGraph{
steps: []ninjalog.Step{
{CmdHash: 1, Out: "1", End: time.Second},
{CmdHash: 2, Out: "2", End: time.Minute},
{CmdHash: 3, Out: "3", End: 2 * time.Minute},
},
},
want: buildStats{
Slowests: []action{
{Outputs: []string{"3"}, Category: "unknown", End: 2 * time.Minute},
{Outputs: []string{"2"}, Category: "unknown", End: time.Minute},
{Outputs: []string{"1"}, Category: "unknown", End: time.Second},
},
CatBuildTimes: []catBuildTime{
{
Category: "unknown",
Count: 3,
BuildTime: 3*time.Minute + time.Second,
MinBuildTime: time.Second,
MaxBuildTime: 2 * time.Minute,
},
},
All: []action{
{Outputs: []string{"2"}, Category: "unknown", End: time.Minute},
{Outputs: []string{"3"}, Category: "unknown", End: 2 * time.Minute},
},
Actions: []action{
{Outputs: []string{"2"}, Category: "unknown", End: time.Minute},
{Outputs: []string{"3"}, Category: "unknown", End: 2 * time.Minute},
},
TotalBuildTime: 3*time.Minute + time.Second,
BuildDuration: 2 * time.Minute,
},
},
} {
t.Run(v.name, func(t *testing.T) {
gotStats, err := extractBuildStats(&v.g, v.minActionBuildTime)
if err != nil {
t.Fatalf("extractBuildStats(%#v, %s) got error: %v", v.g, v.minActionBuildTime, err)
}
if diff := cmp.Diff(v.want, gotStats, cmpopts.EquateEmpty()); diff != "" {
t.Errorf("extractBuildStats(%#v, %s) got stats diff (-want +got):\n%s", v.g, v.minActionBuildTime, diff)
}
})
}
}
func TestExtractStatsError(t *testing.T) {
g := stubGraph{err: errors.New("test critical path error")}
if _, err := extractBuildStats(&g, 0); err == nil {
t.Errorf("extractBuildStats(%#v, nil) got no error, want error", g)
}
}