| // 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) |
| } |
| } |