blob: 8161fdec7f6b7f41eed76f3fba8ffcee5935ad94 [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 ninjagraph
import (
"flag"
"fmt"
"os"
"path/filepath"
"strings"
"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 testDataFlag = flag.String("test_data_dir", "../test_data", "Path to ../test_data/; only used in GN build")
// This ordering is not perfect but good enough for these tests, since we always
// have unique Rule names in edges.
var orderEdgesByRule = cmpopts.SortSlices(func(x, y *Edge) bool { return x.Rule < y.Rule })
func TestFromDOT(t *testing.T) {
for _, tc := range []struct {
desc string
dot string
want Graph
}{
{
desc: "empty",
dot: `digraph ninja {}`,
want: Graph{},
},
{
desc: "no inputs single output",
// build some_file: touch
dot: `digraph ninja {
"0x13e8900" [label="some_file"]
"0x13e8850" [label="touch", shape=ellipse]
"0x13e8850" -> "0x13e8900"
}`,
want: Graph{
Nodes: map[int64]*Node{
0x13e8900: {
ID: 0x13e8900,
Path: "some_file",
In: &Edge{Outputs: []int64{0x13e8900}, Rule: "touch"},
},
},
Edges: []*Edge{{Outputs: []int64{0x13e8900}, Rule: "touch"}},
},
},
{
desc: "single input single output",
// build bar: cp foo
dot: `digraph ninja {
"0x1cc1a20" [label="bar"]
"0x1cc1ac0" -> "0x1cc1a20" [label=" cp"]
"0x1cc1ac0" [label="foo"]
}`,
want: Graph{
Nodes: map[int64]*Node{
0x1cc1ac0: {
ID: 0x1cc1ac0,
Path: "foo",
Outs: []*Edge{
{Inputs: []int64{0x1cc1ac0}, Outputs: []int64{0x1cc1a20}, Rule: "cp"},
},
},
0x1cc1a20: {
ID: 0x1cc1a20,
Path: "bar",
In: &Edge{Inputs: []int64{0x1cc1ac0}, Outputs: []int64{0x1cc1a20}, Rule: "cp"},
},
},
Edges: []*Edge{
{Inputs: []int64{0x1cc1ac0}, Outputs: []int64{0x1cc1a20}, Rule: "cp"},
},
},
},
{
desc: "multiple inputs multiple outputs",
// build d e: phony a b c
dot: `digraph ninja {
"0x1110870" [label="d"]
"0x11107e0" [label="phony", shape=ellipse]
"0x11107e0" -> "0x1110870"
"0x11107e0" -> "0x11108f0"
"0x11109a0" -> "0x11107e0" [arrowhead=none]
"0x1110a60" -> "0x11107e0" [arrowhead=none]
"0x1110b10" -> "0x11107e0" [arrowhead=none]
"0x11109a0" [label="a"]
"0x1110a60" [label="b"]
"0x1110b10" [label="c"]
"0x11108f0" [label="e"]
}`,
want: Graph{
Nodes: map[int64]*Node{
0x11109a0: {
ID: 0x11109a0,
Path: "a",
Outs: []*Edge{
{
Inputs: []int64{0x11109a0, 0x1110a60, 0x1110b10},
Outputs: []int64{0x1110870, 0x11108f0},
Rule: "phony",
},
},
},
0x1110a60: {
ID: 0x1110a60,
Path: "b",
Outs: []*Edge{
{
Inputs: []int64{0x11109a0, 0x1110a60, 0x1110b10},
Outputs: []int64{0x1110870, 0x11108f0},
Rule: "phony",
},
},
},
0x1110b10: {
ID: 0x1110b10,
Path: "c",
Outs: []*Edge{
{
Inputs: []int64{0x11109a0, 0x1110a60, 0x1110b10},
Outputs: []int64{0x1110870, 0x11108f0},
Rule: "phony",
},
},
},
0x1110870: {
ID: 0x1110870,
Path: "d",
In: &Edge{
Inputs: []int64{0x11109a0, 0x1110a60, 0x1110b10},
Outputs: []int64{0x1110870, 0x11108f0},
Rule: "phony",
},
},
0x11108f0: {
ID: 0x11108f0,
Path: "e",
In: &Edge{
Inputs: []int64{0x11109a0, 0x1110a60, 0x1110b10},
Outputs: []int64{0x1110870, 0x11108f0},
Rule: "phony",
},
},
},
Edges: []*Edge{
{
Inputs: []int64{0x11109a0, 0x1110a60, 0x1110b10},
Outputs: []int64{0x1110870, 0x11108f0},
Rule: "phony",
},
},
},
},
{
desc: "multiple builds",
// build baz: touch
// build bar: cp foo
dot: `digraph ninja {
"0x13c3c70" [label="bar"]
"0x13c3d10" -> "0x13c3c70" [label=" cp"]
"0x13c3d10" [label="foo"]
"0x13c3e30" [label="baz"]
"0x13c3dc0" [label="touch", shape=ellipse]
"0x13c3dc0" -> "0x13c3e30"
}`,
want: Graph{
Nodes: map[int64]*Node{
0x13c3d10: {
ID: 0x13c3d10,
Path: "foo",
Outs: []*Edge{
{Inputs: []int64{0x13c3d10}, Outputs: []int64{0x13c3c70}, Rule: "cp"},
},
},
0x13c3c70: {
ID: 0x13c3c70,
Path: "bar",
In: &Edge{Inputs: []int64{0x13c3d10}, Outputs: []int64{0x13c3c70}, Rule: "cp"},
},
0x13c3e30: {
ID: 0x13c3e30,
Path: "baz",
In: &Edge{Outputs: []int64{0x13c3e30}, Rule: "touch"},
},
},
Edges: []*Edge{
{Inputs: []int64{0x13c3d10}, Outputs: []int64{0x13c3c70}, Rule: "cp"},
{Outputs: []int64{0x13c3e30}, Rule: "touch"},
},
},
},
{
desc: "omit output not in build graph",
// build b: rule a
// Note in the graph "0x11108f0" doesn't exist, although `rule` claims to
// produce it.
dot: `digraph ninja {
"0x1110870" [label="b"]
"0x11107e0" [label="rule", shape=ellipse]
"0x11107e0" -> "0x1110870"
"0x11107e0" -> "0x11108f0"
"0x11109a0" -> "0x11107e0" [arrowhead=none]
"0x11109a0" [label="a"]
}`,
want: Graph{
Nodes: map[int64]*Node{
0x11109a0: {
ID: 0x11109a0,
Path: "a",
Outs: []*Edge{
{
Inputs: []int64{0x11109a0},
Outputs: []int64{0x1110870}, // Note 0x11108f0 is omitted.
Rule: "rule",
},
},
},
0x1110870: {
ID: 0x1110870,
Path: "b",
In: &Edge{
Inputs: []int64{0x11109a0},
Outputs: []int64{0x1110870}, // Note 0x11108f0 is omitted.
Rule: "rule",
},
},
},
Edges: []*Edge{
{
Inputs: []int64{0x11109a0},
Outputs: []int64{0x1110870}, // Note 0x11108f0 is omitted.
Rule: "rule",
},
},
},
},
} {
t.Run(tc.desc, func(t *testing.T) {
got, err := FromDOT(strings.NewReader(tc.dot))
if err != nil {
t.Errorf("FromDOT(%s) failed: %s", tc.dot, err)
}
opts := []cmp.Option{
cmpopts.EquateEmpty(),
cmpopts.IgnoreUnexported(Graph{}, Node{}, Edge{}),
orderEdgesByRule,
}
if diff := cmp.Diff(tc.want, got, opts...); diff != "" {
t.Errorf("FromDOT(%s) got diff (-want +got):\n%s", tc.dot, diff)
}
})
}
}
func TestFromDOTErrors(t *testing.T) {
for _, tc := range []struct {
desc string
dot string
}{
{
desc: "multiple edges pointing to the same output",
dot: `digraph ninja {
"0x13e8900" [label="some_file"]
"0x13e8850" [label="touch", shape=ellipse]
"0x13e8850" -> "0x13e8900"
"0x13e8851" [label="touch", shape=ellipse]
"0x13e8851" -> "0x13e8900"
}`,
},
} {
t.Run(tc.desc, func(t *testing.T) {
if got, err := FromDOT(strings.NewReader(tc.dot)); err == nil {
t.Errorf("FromDOT(%s) got no error, want error, parsed graph:\n%v", tc.dot, got)
}
})
}
}
func TestParsingFileFromRealBuild(t *testing.T) {
dotFilePath := filepath.Join(*testDataFlag, "graph.dot")
dotFile, err := os.Open(dotFilePath)
if err != nil {
t.Fatalf("Failed to open file %q: %s", dotFilePath, err)
}
defer dotFile.Close()
wantCXXEdge := Edge{
Inputs: []int64{0x17471d0},
Outputs: []int64{0x1747150},
Rule: "cxx",
}
wantLinkEdge := Edge{
Inputs: []int64{0x1747150, 0x1719e30, 0x1745bc0},
Outputs: []int64{0x1747530},
Rule: "link",
}
want := Graph{
Nodes: map[int64]*Node{
0x1747530: {ID: 0x1747530, Path: "gn", In: &wantLinkEdge},
0x1747150: {ID: 0x1747150, Path: "src/gn/gn_main.o", In: &wantCXXEdge, Outs: []*Edge{&wantLinkEdge}},
0x17471d0: {ID: 0x17471d0, Path: "../src/gn/gn_main.cc", Outs: []*Edge{&wantCXXEdge}},
0x1719e30: {ID: 0x1719e30, Path: "base.a", Outs: []*Edge{&wantLinkEdge}},
0x1745bc0: {ID: 0x1745bc0, Path: "gn_lib.a", Outs: []*Edge{&wantLinkEdge}},
},
Edges: []*Edge{&wantCXXEdge, &wantLinkEdge},
}
got, err := FromDOT(dotFile)
if err != nil {
t.Fatalf("FromDOT failed: %s", err)
}
opts := []cmp.Option{
cmpopts.IgnoreUnexported(Graph{}, Node{}, Edge{}),
orderEdgesByRule,
}
if diff := cmp.Diff(want, got, opts...); diff != "" {
t.Errorf("FromDOT(small gn build graph) =\n%#v\nwant:\n%#v\ndiff(-want +got):\n%s", got, want, diff)
}
}
func TestPopulateEdges(t *testing.T) {
for _, tc := range []struct {
desc string
steps []ninjalog.Step
graph Graph
wantGraph Graph
}{
{
desc: "empty graph",
},
{
desc: "phony edges are skipped",
graph: Graph{
Edges: []*Edge{
{Outputs: []int64{0, 1, 2}, Rule: "phony"},
},
},
wantGraph: Graph{
Edges: []*Edge{
{Outputs: []int64{0, 1, 2}, Rule: "phony"},
},
},
},
{
desc: "single output",
steps: []ninjalog.Step{
{Start: time.Millisecond, End: time.Second, Out: "foo"},
},
graph: Graph{
// `In` and `Outs` edges on nodes are omitted since they don't affect
// this test.
Nodes: map[int64]*Node{
0: {ID: 0, Path: "foo"},
},
Edges: []*Edge{
{Outputs: []int64{0}},
},
},
wantGraph: Graph{
Nodes: map[int64]*Node{
0: {ID: 0, Path: "foo"},
},
Edges: []*Edge{
{
Outputs: []int64{0},
Step: &ninjalog.Step{Start: time.Millisecond, End: time.Second, Out: "foo"},
},
},
},
},
{
desc: "multiple outputs",
steps: []ninjalog.Step{
{Out: "foo", Outs: []string{"bar", "baz"}},
},
graph: Graph{
// `In` and `Outs` edges on nodes are omitted since they don't affect
// this test.
Nodes: map[int64]*Node{
0: {ID: 0, Path: "foo"},
1: {ID: 1, Path: "bar"},
2: {ID: 2, Path: "baz"},
},
Edges: []*Edge{
{Outputs: []int64{0, 1, 2}},
},
},
wantGraph: Graph{
Nodes: map[int64]*Node{
0: {ID: 0, Path: "foo"},
1: {ID: 1, Path: "bar"},
2: {ID: 2, Path: "baz"},
},
Edges: []*Edge{
{
Outputs: []int64{0, 1, 2},
Step: &ninjalog.Step{Out: "foo", Outs: []string{"bar", "baz"}},
},
},
},
},
{
desc: "multiple edges",
steps: []ninjalog.Step{
{Out: "foo"},
{Out: "bar", Outs: []string{"baz"}},
},
graph: Graph{
// `In` and `Outs` edges on nodes are omitted since they don't affect
// this test.
Nodes: map[int64]*Node{
0: {ID: 0, Path: "foo"},
1: {ID: 1, Path: "bar"},
2: {ID: 2, Path: "baz"},
42: {ID: 42, Path: "truth"},
},
Edges: []*Edge{
{Outputs: []int64{0}},
{Outputs: []int64{1, 2}},
{Outputs: []int64{42}, Rule: "phony"},
},
},
wantGraph: Graph{
Nodes: map[int64]*Node{
0: {ID: 0, Path: "foo"},
1: {ID: 1, Path: "bar"},
2: {ID: 2, Path: "baz"},
42: {ID: 42, Path: "truth"},
},
Edges: []*Edge{
{
Outputs: []int64{0},
Step: &ninjalog.Step{Out: "foo"},
},
{
Outputs: []int64{1, 2},
Step: &ninjalog.Step{Out: "bar", Outs: []string{"baz"}},
},
{
Outputs: []int64{42},
Rule: "phony",
},
},
},
},
} {
t.Run(tc.desc, func(t *testing.T) {
if err := tc.graph.PopulateEdges(tc.steps); err != nil {
t.Errorf("PopulateEdges(%v) failed: %v", tc.steps, err)
}
if diff := cmp.Diff(tc.wantGraph, tc.graph, cmpopts.IgnoreUnexported(Graph{}, Node{}, Edge{})); diff != "" {
t.Errorf("PopulateEdges(%v) got graph diff (-want +got):\n%s", tc.steps, diff)
}
})
}
}
func TestPopulateEdgesErrors(t *testing.T) {
for _, tc := range []struct {
desc string
steps []ninjalog.Step
graph Graph
}{
{
desc: "steps with duplicate output",
steps: []ninjalog.Step{
{Out: "foo", Outs: []string{"bar"}},
{Out: "bar"},
},
},
{
desc: "missing step for edge",
graph: Graph{
// `In` and `Outs` edges on the Node are not spelled out since they
// don't affect this test.
Nodes: map[int64]*Node{
0: {ID: 0, Path: "foo"},
},
Edges: []*Edge{
{Outputs: []int64{0}},
},
},
},
{
desc: "multiple steps match the same edge",
steps: []ninjalog.Step{
{Out: "foo", CmdHash: 123},
{Out: "bar", CmdHash: 456},
},
graph: Graph{
// `In` and `Outs` edges on the Node are not spelled out since they
// don't affect this test.
Nodes: map[int64]*Node{
0: {ID: 0, Path: "foo"},
1: {ID: 1, Path: "bar"},
},
Edges: []*Edge{
{Outputs: []int64{0, 1}},
},
},
},
{
desc: "outputs in step don't match edge",
steps: []ninjalog.Step{
{Out: "foo", CmdHash: 123},
},
graph: Graph{
// `In` and `Outs` edges on the Node are not spelled out since they
// don't affect this test.
Nodes: map[int64]*Node{
0: {ID: 0, Path: "foo"},
1: {ID: 1, Path: "bar"},
},
Edges: []*Edge{
{Outputs: []int64{0, 1}},
},
},
},
} {
t.Run(tc.desc, func(t *testing.T) {
if err := tc.graph.PopulateEdges(tc.steps); err == nil {
t.Errorf("PopulateEdges(%v) got no errors, want error", tc.steps)
}
})
}
}
func readTestGraph(tb testing.TB) Graph {
ninjaLogPath := filepath.Join(*testDataFlag, "ninja_log")
ninjaLogFile, err := os.Open(ninjaLogPath)
if err != nil {
tb.Fatalf("Failed to open Ninja log with path %s: %v", ninjaLogPath, err)
}
defer ninjaLogFile.Close()
ninjaLog, err := ninjalog.Parse(ninjaLogPath, ninjaLogFile)
if err != nil {
tb.Fatalf("Failed to parse Ninja log from path %s: %v", ninjaLogPath, err)
}
steps := ninjalog.Dedup(ninjaLog.Steps)
compdbPath := filepath.Join(*testDataFlag, "compdb.json")
compdbFile, err := os.Open(compdbPath)
if err != nil {
tb.Fatalf("Failed to open compdb with path %s: %v", compdbPath, err)
}
defer compdbFile.Close()
commands, err := compdb.Parse(compdbFile)
if err != nil {
tb.Fatalf("Failed to parse compdb from path %s: %v", compdbPath, err)
}
steps = ninjalog.Populate(steps, commands)
dotFilePath := filepath.Join(*testDataFlag, "graph.dot")
dotFile, err := os.Open(dotFilePath)
if err != nil {
tb.Fatalf("Failed to open Ninja graph with path %s: %v", dotFilePath, err)
}
defer dotFile.Close()
graph, err := FromDOT(dotFile)
if err != nil {
tb.Fatalf("Failed to parse Ninja graph from path %s: %v", dotFilePath, err)
}
if err := graph.PopulateEdges(steps); err != nil {
tb.Fatalf("Failed to populate parsed Ninja graph with deduplicated steps: %v", err)
}
return graph
}
func TestPopulateEdgesWithRealBuild(t *testing.T) {
graph := readTestGraph(t)
for _, e := range graph.Edges {
if isPhony, gotNilStep := e.Rule == "phony", e.Step == nil; isPhony != gotNilStep {
var outputs []string
for _, output := range e.Outputs {
outputs = append(outputs, graph.Nodes[output].Path)
}
t.Errorf("Invalid edge after `PopulateEdges`, isPhony (%t) != gotNilStep (%t), outputs of this edge: %v", isPhony, gotNilStep, outputs)
}
}
}
func BenchmarkCriticalPath(b *testing.B) {
b.Run("CriticalPath", func(b *testing.B) {
for i := 0; i < b.N; i++ {
graph := readTestGraph(b)
if _, err := graph.CriticalPath(); err != nil {
b.Fatalf("CriticalPath() got error: %v", err)
}
}
})
b.Run("CriticalPathV2", func(b *testing.B) {
for i := 0; i < b.N; i++ {
graph := readTestGraph(b)
if _, err := graph.CriticalPathV2(); err != nil {
b.Fatalf("CriticalPathV2() got error: %v", err)
}
}
})
}
func TestCriticalPath(t *testing.T) {
step1 := ninjalog.Step{End: 5}
edge1 := &Edge{Outputs: []int64{1}, Step: &step1}
step2 := ninjalog.Step{Start: 5, End: 10, Out: "2"}
edge2 := &Edge{Inputs: []int64{1}, Outputs: []int64{2}, Step: &step2}
step3 := ninjalog.Step{Start: 42, End: 420, Out: "3"}
edge3 := &Edge{Inputs: []int64{2}, Outputs: []int64{3}, Step: &step3}
edge3Phony := &Edge{Inputs: []int64{2}, Outputs: []int64{3}, Rule: "phony"}
step4 := ninjalog.Step{End: 100, Out: "4"}
edge4 := &Edge{Inputs: []int64{3}, Outputs: []int64{4}, Step: &step4}
step4LateStart := ninjalog.Step{Start: 99, End: 100, Out: "4"}
edge4LateStart := &Edge{Inputs: []int64{3}, Outputs: []int64{4}, Step: &step4LateStart}
step5 := ninjalog.Step{Start: 100, End: 200, Out: "5"}
edge5 := &Edge{Inputs: []int64{2, 4}, Outputs: []int64{5}, Step: &step5}
clearEdgeMemoizations := func() {
for _, e := range []*Edge{edge1, edge2, edge3, edge4, edge4LateStart, edge5} {
e.earliestStart = nil
e.latestFinish = nil
}
}
for _, tc := range []struct {
desc string
graph Graph
want []ninjalog.Step
}{
{
desc: "empty graph",
},
{
// 1 -> 2 -> 3
desc: "single inputs",
graph: Graph{
Nodes: map[int64]*Node{
1: {ID: 1, Outs: []*Edge{edge2}},
2: {ID: 2, In: edge2, Outs: []*Edge{edge3}},
3: {ID: 3, In: edge3},
},
Edges: []*Edge{edge2, edge3},
},
want: []ninjalog.Step{step2, step3},
},
{
// 1 -> 2 --phony--> 3
desc: "phony edge",
graph: Graph{
Nodes: map[int64]*Node{
1: {ID: 1, Outs: []*Edge{edge2}},
2: {ID: 2, In: edge2, Outs: []*Edge{edge3Phony}},
3: {ID: 3, In: edge3Phony},
},
Edges: []*Edge{edge2, edge3Phony},
},
want: []ninjalog.Step{step2}, // Phony edge is not included.
},
{
// -> 1 -> 2 -> 3
desc: "edge with no input file",
graph: Graph{
Nodes: map[int64]*Node{
1: {ID: 1, In: edge1, Outs: []*Edge{edge2}},
2: {ID: 2, In: edge2, Outs: []*Edge{edge3}},
3: {ID: 3, In: edge3},
},
Edges: []*Edge{edge1, edge2, edge3},
},
want: []ninjalog.Step{step1, step2, step3},
},
{
// 1 -> 2 ---> 5
// /
// 3 -> 4 -
desc: "multiple inputs same start time",
graph: Graph{
Nodes: map[int64]*Node{
// `Outs` edges on nodes are omitted since they don't affect this test.
1: {ID: 1, Outs: []*Edge{edge2}},
2: {ID: 2, In: edge2, Outs: []*Edge{edge5}},
3: {ID: 3, Outs: []*Edge{edge4}},
4: {ID: 4, In: edge4, Outs: []*Edge{edge5}},
5: {ID: 5, In: edge5},
},
// `Edges` are omitted since they don't affect this test.
Edges: []*Edge{edge2, edge4, edge5},
},
want: []ninjalog.Step{step4, step5},
},
{
// 1 -> 2 ---> 5
// /
// 3 -> 4 -
desc: "multiple inputs different start times",
graph: Graph{
Nodes: map[int64]*Node{
// `Outs` edges on nodes are omitted since they don't affect this test.
1: {ID: 1, Outs: []*Edge{edge2}},
2: {ID: 2, In: edge2, Outs: []*Edge{edge5}},
3: {ID: 3, Outs: []*Edge{edge4LateStart}},
4: {ID: 4, In: edge4LateStart, Outs: []*Edge{edge5}},
5: {ID: 5, In: edge5},
},
Edges: []*Edge{edge2, edge4LateStart, edge5},
},
want: []ninjalog.Step{step2, step5},
},
} {
t.Run(tc.desc, func(t *testing.T) {
// Clear memoization fields because we are reusing pointers to edges.
defer clearEdgeMemoizations()
got, err := tc.graph.CriticalPath()
if err != nil {
t.Fatalf("CriticalPath() got error: %v", err)
}
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("CriticalPath() got diff (-want, +got):\n%s", diff)
}
got, err = tc.graph.CriticalPathV2()
if err != nil {
t.Fatalf("CriticalPathV2() got error: %v", err)
}
if diff := cmp.Diff(tc.want, got); diff != "" {
t.Errorf("CriticalPathV2() got diff (-want +got):\n%s", diff)
}
})
}
}
func TestCriticalPathErrors(t *testing.T) {
edge2 := &Edge{Inputs: []int64{1}, Outputs: []int64{2}, Step: &ninjalog.Step{End: 10, Out: "2"}}
edge2WithNoStep := &Edge{Inputs: []int64{1}, Outputs: []int64{2}}
for _, tc := range []struct {
desc string
graph Graph
}{
{
desc: "edge missing step",
graph: Graph{
Nodes: map[int64]*Node{
1: {ID: 1},
2: {ID: 2, In: edge2WithNoStep},
},
Edges: []*Edge{edge2WithNoStep},
},
},
{
desc: "missing input node",
graph: Graph{
Nodes: map[int64]*Node{
2: {ID: 2, In: edge2},
},
Edges: []*Edge{edge2},
},
},
} {
t.Run(tc.desc, func(t *testing.T) {
if _, err := tc.graph.CriticalPath(); err == nil {
t.Error("CriticalPath() got no error, want error")
}
})
}
}
func TestAddSink(t *testing.T) {
edge2 := &Edge{Inputs: []int64{1}, Outputs: []int64{2}}
edge3 := &Edge{Inputs: []int64{1}, Outputs: []int64{3}}
for _, v := range []struct {
desc string
graph Graph
want []*Edge
}{
{
desc: "empty graph",
},
{
// 1 -> 2
desc: "single pure output",
graph: Graph{
Nodes: map[int64]*Node{
1: {ID: 1, Outs: []*Edge{edge2}},
2: {ID: 2, In: edge2},
},
Edges: []*Edge{edge2},
},
want: []*Edge{
edge2,
{
Inputs: []int64{2},
Rule: ninjagoArtificialSinkRule,
Step: &ninjalog.Step{},
},
},
},
{
// 1 ----> 2
// \
// --> 3
desc: "multiple pure outputs",
graph: Graph{
Nodes: map[int64]*Node{
1: {ID: 1, Outs: []*Edge{edge2, edge3}},
2: {ID: 2, In: edge2},
3: {ID: 3, In: edge3},
},
Edges: []*Edge{edge2, edge3},
},
want: []*Edge{
edge2,
edge3,
{
Inputs: []int64{2, 3},
Rule: ninjagoArtificialSinkRule,
Step: &ninjalog.Step{},
},
},
},
} {
t.Run(v.desc, func(t *testing.T) {
if err := v.graph.addSink(); err != nil {
t.Fatalf("addSink() got error: %v", err)
}
opts := []cmp.Option{
cmpopts.IgnoreUnexported(Edge{}),
// Input IDs on sink edge has indeterministic order.
cmpopts.SortSlices(func(i, j int64) bool { return i < j }),
}
if diff := cmp.Diff(v.graph.Edges, v.want, opts...); diff != "" {
t.Errorf("After addSink(), got graph.Edges diff (-want, +got):\n%s", diff)
}
// addSink should be idempotent.
if err := v.graph.addSink(); err != nil {
t.Fatalf("addSink() got error: %v", err)
}
if diff := cmp.Diff(v.graph.Edges, v.want, opts...); diff != "" {
t.Errorf("After addSink(), got graph.Edges diff (-want, +got):\n%s", diff)
}
})
}
}
func TestTotalFloat(t *testing.T) {
step1 := ninjalog.Step{End: 10}
edge1 := &Edge{
Outputs: []int64{1},
Step: &step1,
}
step2 := ninjalog.Step{Start: 10, End: 30}
edge2 := &Edge{
Inputs: []int64{1},
Outputs: []int64{2},
Step: &step2,
}
step3 := ninjalog.Step{Start: 30, End: 35}
edge3 := &Edge{
Inputs: []int64{2},
Outputs: []int64{3},
Step: &step3,
}
step4 := ninjalog.Step{Start: 35, End: 45}
edge4 := &Edge{
Inputs: []int64{3},
Outputs: []int64{4},
Step: &step4,
}
step5 := ninjalog.Step{Start: 45, End: 65}
edge5 := &Edge{
Inputs: []int64{4, 7, 8},
Outputs: []int64{5},
Step: &step5,
}
step6 := ninjalog.Step{Start: 10, End: 25}
edge6 := &Edge{
Inputs: []int64{1},
Outputs: []int64{6},
Step: &step6,
}
step7 := ninjalog.Step{Start: 35, End: 40}
edge7 := &Edge{
Inputs: []int64{3, 6},
Outputs: []int64{7},
Step: &step7,
}
step8 := ninjalog.Step{Start: 10, End: 25}
edge8 := &Edge{
Inputs: []int64{1},
Outputs: []int64{8},
Step: &step8,
}
// Test graph based on https://en.wikipedia.org/wiki/Critical_path_drag#/media/File:SimpleAONwDrag3.png.
g := Graph{
Nodes: map[int64]*Node{
1: {ID: 1, In: edge1, Outs: []*Edge{edge2, edge6, edge8}},
2: {ID: 2, In: edge2, Outs: []*Edge{edge3}},
3: {ID: 3, In: edge3, Outs: []*Edge{edge4, edge7}},
4: {ID: 4, In: edge4, Outs: []*Edge{edge5}},
5: {ID: 5, In: edge5},
6: {ID: 6, In: edge6, Outs: []*Edge{edge7}},
7: {ID: 7, In: edge7, Outs: []*Edge{edge5}},
8: {ID: 8, In: edge8, Outs: []*Edge{edge5}},
},
Edges: []*Edge{edge1, edge2, edge3, edge4, edge5, edge6, edge7, edge8},
}
t.Log(`In graph:
-----> 6 ----------> 7 --
/ / \
/ / \
1 -----> 2 -----> 3 -----> 4 ----->5
\ /
\ /
-----------> 8 ----------`)
wantCriticalPath := []ninjalog.Step{step1, step2, step3, step4, step5}
got, err := g.CriticalPath()
if err != nil {
t.Fatalf("CriticalPath failed: %v", err)
}
if diff := cmp.Diff(wantCriticalPath, got); diff != "" {
t.Errorf("CriticalPath got: %v, want: %v, diff (-want +got):\n%s", got, wantCriticalPath, diff)
}
got, err = g.CriticalPathV2()
if err != nil {
t.Fatalf("CriticalPathV2 failed: %v", err)
}
if diff := cmp.Diff(wantCriticalPath, got); diff != "" {
t.Errorf("CriticalPathV2 got: %v, want: %v, diff (-want +got):\n%s", got, wantCriticalPath, diff)
}
for _, want := range []struct {
input *Edge
parallelizables []*Edge
}{
{input: edge1},
{input: edge2, parallelizables: []*Edge{edge6, edge8}},
{input: edge3, parallelizables: []*Edge{edge6, edge8}},
{input: edge4, parallelizables: []*Edge{edge7, edge8}},
{input: edge5},
} {
got, err := g.parallelizableEdges(want.input)
if err != nil {
t.Errorf("parallelizableEdges(edge outputting %v) error: %v", want.input.Outputs, err)
}
if !cmp.Equal(got, want.parallelizables, cmpopts.IgnoreUnexported(Edge{})) {
t.Errorf("parallelizableEdges(edge outputting %v) = %s, want: %s", want.input.Outputs, edgesToStr(got), edgesToStr(want.parallelizables))
}
}
for _, want := range []struct {
input *Edge
earliestStart time.Duration
earliestFinish time.Duration
latestStart time.Duration
latestFinish time.Duration
totalFloat time.Duration
}{
{
input: edge1,
earliestStart: 0,
earliestFinish: 10,
latestStart: 0,
latestFinish: 10,
totalFloat: 0,
},
{
input: edge2,
earliestStart: 10,
earliestFinish: 30,
latestStart: 10,
latestFinish: 30,
totalFloat: 0,
},
{
input: edge3,
earliestStart: 30,
earliestFinish: 35,
latestStart: 30,
latestFinish: 35,
totalFloat: 0,
},
{
input: edge4,
earliestStart: 35,
earliestFinish: 45,
latestStart: 35,
latestFinish: 45,
totalFloat: 0,
},
{
input: edge5,
earliestStart: 45,
earliestFinish: 65,
latestStart: 45,
latestFinish: 65,
totalFloat: 0,
},
{
input: edge6,
earliestStart: 10,
earliestFinish: 25,
latestStart: 25,
latestFinish: 40,
totalFloat: 15,
},
{
input: edge7,
earliestStart: 35,
earliestFinish: 40,
latestStart: 40,
latestFinish: 45,
totalFloat: 5,
},
{
input: edge8,
earliestStart: 10,
earliestFinish: 25,
latestStart: 30,
latestFinish: 45,
totalFloat: 20,
},
} {
for _, fn := range []struct {
name string
f func(*Edge) (time.Duration, error)
want time.Duration
}{
{name: "earliestStart", f: g.earliestStart, want: want.earliestStart},
{name: "earliestFinish", f: g.earliestFinish, want: want.earliestFinish},
{name: "latestStart", f: g.latestStart, want: want.latestStart},
{name: "latestFinish", f: g.latestFinish, want: want.latestFinish},
{name: "totalFloat", f: g.totalFloat, want: want.totalFloat},
} {
if got := mustDuration(t, want.input, fn.f); got != fn.want {
t.Errorf("%s(edge outputting %v) = %s, want: %s", fn.name, want.input.Outputs, got, fn.want)
}
}
}
for _, want := range []struct {
input *Edge
err bool
drag time.Duration
}{
{input: edge1, drag: 10},
{input: edge2, drag: 15},
{input: edge3, drag: 5},
{input: edge4, drag: 5},
{input: edge5, drag: 20},
{input: edge6, err: true},
{input: edge7, err: true},
{input: edge8, err: true},
} {
got, err := g.drag(want.input)
if (err != nil) != want.err {
t.Errorf("drag(edge outputting %v) got error: %v, want error: %t", want.input.Outputs, err, want.err)
}
if got != want.drag {
t.Errorf("drag(edge outputting %v) = %s, want: %s", want.input.Outputs, got, want.drag)
}
}
}
func edgesToStr(edges []*Edge) string {
var ss []string
for _, e := range edges {
ss = append(ss, fmt.Sprintf("edge outputting %v", e.Outputs))
}
return "[" + strings.Join(ss, ", ") + "]"
}
func mustDuration(t *testing.T, edge *Edge, f func(*Edge) (time.Duration, error)) time.Duration {
t.Helper()
d, err := f(edge)
if err != nil {
t.Fatalf("Failed to get duration: %v", err)
}
return d
}