blob: 5941b794f76c86ce588870fa015ec98b849d8a3c [file] [log] [blame]
// Copyright 2024 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package query
import (
"context"
"os"
"path/filepath"
"sort"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/testing/protocmp"
"infra/build/siso/hashfs"
fspb "infra/build/siso/hashfs/proto"
"infra/build/siso/reapi/digest"
pb "infra/build/siso/toolsupport/ciderutil/proto"
"infra/build/siso/toolsupport/ninjautil"
)
func TestIDEAnalysis(t *testing.T) {
ctx := context.Background()
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
topDir := t.TempDir()
err = os.Chdir(topDir)
if err != nil {
t.Fatal(err)
}
defer func() {
err = os.Chdir(wd)
if err != nil {
t.Error(err)
}
}()
dir := filepath.Join(topDir, "out/siso")
err = os.MkdirAll(dir, 0755)
if err != nil {
t.Fatal(err)
}
buildNinjaFilename := filepath.Join(dir, "build.ninja")
setupBuildNinja(t, buildNinjaFilename)
fsStateFilename := filepath.Join(dir, ".siso_fs_state")
barH := `// generated file`
testPBH := `// Generated by the protocol buffer compiler. DO NOT EDIT!`
setupFileState(t, topDir, fsStateFilename, map[string]fileState{
filepath.Join(topDir, "base/test/test.proto"): {
content: "test.proto",
},
filepath.Join(topDir, "foo/foo.cc"): {
content: `
#include "bar/bar.h"
#include "base/test/test.pb.h"
#include "foo.h"
`,
},
filepath.Join(topDir, "foo/foo.h"): {
content: "// foo.h",
},
filepath.Join(topDir, "out/siso/build.ninja.d"): {
content: `build.ninja.stamp: ../../.gn ../../BUILD.gn ../../foo/BUILD.gn ../../foo/foo.gni ../../chrome/VERSION gen/foo/foo.build_metadata
`,
},
filepath.Join(topDir, "out/siso/foo"): {
content: "foo binary",
mtime: 3 * time.Millisecond,
generated: true,
},
filepath.Join(topDir, "out/siso/gen/bar/bar.h"): {
content: barH,
mtime: 1 * time.Millisecond,
generated: true,
},
filepath.Join(topDir, "out/siso/gen/base/test/test.pb.h"): {
content: testPBH,
mtime: 1 * time.Millisecond,
generated: true,
},
filepath.Join(topDir, "out/siso/obj/foo.o"): {
content: "foo obj",
mtime: 2 * time.Millisecond,
generated: true,
},
filepath.Join(topDir, "out/siso/obj/protoc.o"): {
content: "protoc obj",
mtime: 1 * time.Millisecond,
generated: true,
},
filepath.Join(topDir, "out/siso/protoc"): {
content: "protoc",
mtime: 1 * time.Millisecond,
generated: true,
},
filepath.Join(topDir, "protoc/protoc.cc"): {
content: "// protoc.cc",
},
filepath.Join(topDir, "tools/protoc_wrapper/protoc_wrapper.py"): {
content: "protoc_wrapper.py",
},
})
// This c.analysis call is equivalent with invocation of
// ./build/util/ide_query --out-dir out/siso --source foo/foo.cc
c := &ideAnalysisRun{}
c.init()
c.dir = "out/siso"
got, err := c.analyze(ctx, []string{"../../foo/foo.cc^"})
if err != nil {
t.Errorf(`analyze(ctx, "../../foo/foo.cc^")=%v, %v; want nil er`, got, err)
}
t.Logf("got=%s", got)
want := &pb.IdeAnalysis{
BuildOutDir: "out/siso",
WorkingDir: "out/siso",
Results: []*pb.AnalysisResult{
{
SourceFilePath: "../../foo/foo.cc",
Status: &pb.AnalysisResult_Status{
Code: pb.AnalysisResult_Status_CODE_OK,
},
UnitId: "obj/foo.o",
Invalidation: &pb.Invalidation{
FilePaths: []string{
"chrome/VERSION",
},
Wildcards: []*pb.Invalidation_Wildcard{
{
Suffix: proto.String(".gn"),
CanCrossFolder: proto.Bool(true),
},
{
Suffix: proto.String(".gni"),
CanCrossFolder: proto.Bool(true),
},
},
},
},
},
Units: []*pb.BuildableUnit{
{
Id: "gen/bar/bar.h",
SourceFilePaths: []string{
"../../bar/bar.in",
},
CompilerArguments: []string{
"python3",
"gen.py",
"../../bar/bar.in",
"gen/bar/bar.h",
},
GeneratedFiles: []*pb.GeneratedFile{
{
Path: "gen/bar/bar.h",
Contents: []byte(barH),
},
},
},
{
Id: "gen/base/test/test.pb.h",
SourceFilePaths: []string{
"../../tools/protoc_wrapper/protoc_wrapper.py",
"../../base/test/test.proto",
},
CompilerArguments: []string{
"python3",
"../../tools/protoc_wrapper/protoc_wrapper.py",
"test.proto",
"--protoc",
"./protoc",
"--proto-in-dir",
"../../base/test",
"--cc-out-dir",
"gen/base/test",
},
GeneratedFiles: []*pb.GeneratedFile{
{
Path: "gen/base/test/test.pb.h",
Contents: []byte(testPBH),
},
},
},
{
Id: "obj/foo.o",
Language: pb.Language_LANGUAGE_CPP,
SourceFilePaths: []string{
"../../foo/foo.cc",
},
CompilerArguments: []string{
"../../toolchain/clang++",
"-MMD",
"-MF",
"obj/foo.o.d",
"-Igen",
"--sysroot=../../sysroot",
"-c",
"../../foo/foo.cc",
"-o",
"obj/foo.o",
},
DependencyIds: []string{
"gen/bar/bar.h",
"gen/base/test/test.pb.h",
},
},
},
}
if diff := cmp.Diff(want, got, protocmp.Transform()); diff != "" {
t.Errorf(`analysis(ctx, "../../foo/foo.cc^") diff -want +got:\n%s`, diff)
}
}
func setupBuildNinja(t *testing.T, fname string) {
t.Helper()
err := os.WriteFile(fname, []byte(`
cflags = -Igen --sysroot=../../sysroot
rule stamp
command = touch ${out}
rule gen
command = python3 gen.py ${in} ${out}
rule proto_gen_test_proto
command = python3 ../../tools/protoc_wrapper/protoc_wrapper.py test.proto --protoc ./protoc --proto-in-dir ../../base/test --cc-out-dir gen/base/test
rule cxx
command = ../../toolchain/clang++ -MMD -MF ${out}.d ${cflags} -c ${in} -o ${out}
dep = gcc
depfile = ${out}.d
rule link
command = clang++ @${rspfile} -o ${out}
rspfile = ${out}.rsp
rspfile_content = ${in}
build obj/protoc.o: cxx ../../protoc/protoc.cc
build protoc: link obj/protoc.o
build gen/bar/bar.h: gen ../../bar/bar.in
build gen/base/test/test.pb.h: proto_gen_test_proto | ../../tools/protoc_wrapper/protoc_wrapper.py protoc ../../base/test/test.proto
build obj/foo.inputdeps.stamp: stamp gen/bar/bar.h gen/base/test/test.pb.h
build obj/foo.o: cxx ../../foo/foo.cc | obj/foo.inputdeps.stamp
build foo: link obj/foo.o
`), 0644)
if err != nil {
t.Fatal(err)
}
}
func setupDepsLog(t *testing.T, fname string) {
t.Helper()
ctx := context.Background()
depsLog, err := ninjautil.NewDepsLog(ctx, fname)
if err != nil {
t.Fatal(err)
}
_, err = depsLog.Record(ctx, "obj/foo.o", time.Now(), []string{"../../foo/foo.cc", "../../foo/foo.h", "gen/bar/bar.h", "gen/base/test/test.pb.h"})
if err != nil {
depsLog.Close()
t.Fatal(err)
}
err = depsLog.Close()
if err != nil {
t.Fatal(err)
}
}
type fileState struct {
content string
mtime time.Duration
generated bool
}
func setupFileState(t *testing.T, topdir, fname string, files map[string]fileState) {
t.Helper()
ctx := context.Background()
srcMtime := time.Now().Add(-10 * time.Second)
cmdHash := []byte("someCommandHash")
state := &fspb.State{
Entries: []*fspb.Entry{
{
Id: &fspb.FileID{
ModTime: srcMtime.UnixNano(),
},
Name: filepath.ToSlash(topdir),
},
},
}
seen := map[string]bool{
topdir: true,
}
var mkdirAll func(dirname string, mtime time.Time)
mkdirAll = func(dirname string, mtime time.Time) {
err := os.MkdirAll(dirname, 0755)
if err != nil {
t.Fatal(err)
}
if seen[dirname] {
return
}
seen[dirname] = true
state.Entries = append(state.Entries, &fspb.Entry{
Id: &fspb.FileID{
ModTime: mtime.UnixNano(),
},
Name: dirname,
})
mkdirAll(filepath.Dir(dirname), mtime)
}
writeFile := func(fname, content string, mtime time.Time, generated bool) {
mkdirAll(filepath.Dir(fname), mtime)
d := digest.FromBytes(fname, []byte(content))
ent := &fspb.Entry{
Id: &fspb.FileID{
ModTime: mtime.UnixNano(),
},
Name: fname,
Digest: &fspb.Digest{
Hash: d.Digest().Hash,
SizeBytes: d.Digest().SizeBytes,
},
}
if generated {
ent.CmdHash = cmdHash
ent.UpdatedTime = mtime.UnixNano()
}
state.Entries = append(state.Entries, ent)
err := os.WriteFile(fname, []byte(content), 0644)
if err != nil {
t.Fatal(err)
}
}
for fname, fstate := range files {
mtime := srcMtime.Add(fstate.mtime)
writeFile(fname, fstate.content, mtime, fstate.generated)
}
sort.Slice(state.Entries, func(i, j int) bool {
return state.Entries[i].Name < state.Entries[j].Name
})
err := hashfs.Save(ctx, fname, state)
if err != nil {
t.Fatal(err)
}
}