| // Copyright 2026 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 |
| |
| import ( |
| "encoding/json" |
| "fmt" |
| "os" |
| "path/filepath" |
| "reflect" |
| "strings" |
| "testing" |
| ) |
| |
| func TestSplitCommand(t *testing.T) { |
| tests := []struct { |
| cmd string |
| expected []string |
| }{ |
| { |
| cmd: "clang++ -o obj/file.o -c file.cc", |
| expected: []string{"clang++", "-o", "obj/file.o", "-c", "file.cc"}, |
| }, |
| { |
| cmd: `clang++ "-DQUOTED_ARG" -o "obj/path with spaces.o"`, |
| expected: []string{"clang++", "-DQUOTED_ARG", "-o", "obj/path with spaces.o"}, |
| }, |
| { |
| cmd: `clang++ 'single quoted' -DFOO=\"bar\"`, |
| expected: []string{"clang++", "single quoted", "-DFOO=\"bar\""}, |
| }, |
| { |
| cmd: "arg1\\ with\\ space arg2", |
| expected: []string{"arg1 with space", "arg2"}, |
| }, |
| } |
| |
| for _, test := range tests { |
| result := splitCommand(test.cmd) |
| if !reflect.DeepEqual(result, test.expected) { |
| t.Errorf("splitCommand(%q) = %v; want %v", test.cmd, result, test.expected) |
| } |
| } |
| } |
| |
| func TestExtractOutput(t *testing.T) { |
| tests := []struct { |
| cmd CompileCommand |
| expected string |
| }{ |
| { |
| cmd: CompileCommand{ |
| Directory: "/build", |
| Arguments: []string{"clang++", "-o", "out.o", "in.cc"}, |
| }, |
| expected: "/build/out.o", |
| }, |
| { |
| cmd: CompileCommand{ |
| Directory: "/build", |
| Command: "clang++ -o /abs/out.o in.cc", |
| }, |
| expected: "/abs/out.o", |
| }, |
| } |
| |
| for _, test := range tests { |
| result, err := ExtractOutput(test.cmd) |
| if err != nil { |
| t.Errorf("ExtractOutput failed: %v", err) |
| } |
| if result != test.expected { |
| t.Errorf("ExtractOutput = %q; want %q", result, test.expected) |
| } |
| } |
| } |
| |
| func TestPopulateTargets(t *testing.T) { |
| // Create a temporary business logic context. |
| tmpDir, err := os.MkdirTemp("", "ide-query-test") |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer os.RemoveAll(tmpDir) |
| |
| fuchsiaDir := filepath.Join(tmpDir, "fuchsia") |
| buildDir := filepath.Join(fuchsiaDir, "out/default") |
| os.MkdirAll(buildDir, 0755) |
| |
| sourceFile := filepath.Join(fuchsiaDir, "src/main.cc") |
| os.MkdirAll(filepath.Dir(sourceFile), 0755) |
| os.WriteFile(sourceFile, []byte("void main() {}"), 0644) |
| |
| // Create a mock compile_commands.json |
| compDb := []CompileCommand{ |
| { |
| Directory: buildDir, |
| File: "../../src/main.cc", |
| Arguments: []string{"clang++", "-o", "obj/main.o", "-c", "../../src/main.cc"}, |
| }, |
| } |
| dbContent, _ := json.Marshal(compDb) |
| os.WriteFile(filepath.Join(buildDir, "compile_commands.json"), dbContent, 0644) |
| |
| // Mock the Build API resolver. |
| oldResolve := resolveNinjaPath |
| defer func() { resolveNinjaPath = oldResolve }() |
| resolveNinjaPath = func(ctx *WorkspaceContext, ninjaPath string) (string, error) { |
| if ninjaPath == "obj/main.o" { |
| return "//src:main_target", nil |
| } |
| return "", nil |
| } |
| |
| ctx := &WorkspaceContext{ |
| FuchsiaDir: fuchsiaDir, |
| BuildDir: buildDir, |
| Files: []FileEntry{ |
| { |
| AbsPath: sourceFile, |
| Status: StatusFound, |
| }, |
| }, |
| } |
| |
| if err := ctx.PopulateTargets(); err != nil { |
| t.Fatalf("PopulateTargets failed: %v", err) |
| } |
| |
| if len(ctx.Files[0].BuildTargets) != 1 || ctx.Files[0].BuildTargets[0] != "//src:main_target" { |
| t.Errorf("expected //src:main_target, got %v", ctx.Files[0].BuildTargets) |
| } |
| } |
| |
| func TestPopulateTargets_NestedHeader(t *testing.T) { |
| tmpDir, err := os.MkdirTemp("", "ide-query-test-nested") |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer os.RemoveAll(tmpDir) |
| |
| fuchsiaDir := filepath.Join(tmpDir, "fuchsia") |
| buildDir := filepath.Join(fuchsiaDir, "out/default") |
| os.MkdirAll(buildDir, 0755) |
| |
| sourceFile := filepath.Join(fuchsiaDir, "zircon/kernel/lib/arch/x86/standard-segments.cc") |
| headerFile := filepath.Join(fuchsiaDir, "zircon/kernel/lib/arch/x86/include/lib/arch/x86/standard-segments.h") |
| os.MkdirAll(filepath.Dir(sourceFile), 0755) |
| os.MkdirAll(filepath.Dir(headerFile), 0755) |
| os.WriteFile(sourceFile, []byte("void f() {}"), 0644) |
| os.WriteFile(headerFile, []byte("void f();"), 0644) |
| |
| compDb := []CompileCommand{ |
| { |
| Directory: buildDir, |
| File: "../../zircon/kernel/lib/arch/x86/standard-segments.cc", |
| Arguments: []string{"clang++", "-o", "obj/zircon/kernel/lib/arch/x86/standard-segments.o", "-c", "../../zircon/kernel/lib/arch/x86/standard-segments.cc"}, |
| }, |
| } |
| dbContent, _ := json.Marshal(compDb) |
| os.WriteFile(filepath.Join(buildDir, "compile_commands.json"), dbContent, 0644) |
| |
| oldResolve := resolveNinjaPath |
| defer func() { resolveNinjaPath = oldResolve }() |
| resolveNinjaPath = func(ctx *WorkspaceContext, ninjaPath string) (string, error) { |
| if ninjaPath == "obj/zircon/kernel/lib/arch/x86/standard-segments.o" { |
| return "//zircon/kernel/lib/arch/x86:x86", nil |
| } |
| return "", nil |
| } |
| |
| ctx := &WorkspaceContext{ |
| FuchsiaDir: fuchsiaDir, |
| BuildDir: buildDir, |
| Files: []FileEntry{ |
| { |
| AbsPath: headerFile, |
| Status: StatusFound, |
| }, |
| }, |
| } |
| |
| if err := ctx.PopulateTargets(); err != nil { |
| t.Fatalf("PopulateTargets failed: %v", err) |
| } |
| |
| if len(ctx.Files[0].BuildTargets) != 1 || ctx.Files[0].BuildTargets[0] != "//zircon/kernel/lib/arch/x86:x86" { |
| t.Errorf("expected //zircon/kernel/lib/arch/x86:x86, got %v", ctx.Files[0].BuildTargets) |
| } |
| } |
| func TestVerifyBuild_NoArgsGn(t *testing.T) { |
| tmpDir := t.TempDir() |
| fuchsiaDir := filepath.Join(tmpDir, "fuchsia") |
| buildDir := filepath.Join(fuchsiaDir, "out/default") |
| os.MkdirAll(buildDir, 0755) |
| |
| ctx := &WorkspaceContext{ |
| FuchsiaDir: fuchsiaDir, |
| BuildDir: buildDir, |
| Files: []FileEntry{ |
| {AbsPath: filepath.Join(fuchsiaDir, "src/main.cc"), Status: StatusFound}, |
| }, |
| } |
| |
| if err := ctx.VerifyBuild(); err != nil { |
| t.Fatalf("VerifyBuild failed: %v", err) |
| } |
| |
| if !strings.Contains(ctx.Files[0].AnalysisError, "args.gn missing") { |
| t.Errorf("expected AnalysisError about missing args.gn, got %q", ctx.Files[0].AnalysisError) |
| } |
| } |
| |
| func TestVerifyBuild_Success(t *testing.T) { |
| tmpDir := t.TempDir() |
| fuchsiaDir := filepath.Join(tmpDir, "fuchsia") |
| buildDir := filepath.Join(fuchsiaDir, "out/default") |
| os.MkdirAll(buildDir, 0755) |
| os.WriteFile(filepath.Join(buildDir, "args.gn"), []byte("foo=bar"), 0644) |
| |
| // Mock runFx |
| oldRunFx := runFx |
| defer func() { runFx = oldRunFx }() |
| runFxCalled := false |
| runFx = func(ctx *WorkspaceContext, dir string, args ...string) error { |
| if args[0] == "build" { |
| runFxCalled = true |
| } |
| return nil |
| } |
| |
| ctx := &WorkspaceContext{ |
| FuchsiaDir: fuchsiaDir, |
| BuildDir: buildDir, |
| Files: []FileEntry{ |
| { |
| AbsPath: filepath.Join(fuchsiaDir, "src/main.cc"), |
| Status: StatusFound, |
| BuildTargets: []string{"//src:main"}, |
| }, |
| }, |
| } |
| |
| if err := ctx.VerifyBuild(); err != nil { |
| t.Fatalf("VerifyBuild failed: %v", err) |
| } |
| |
| if !runFxCalled { |
| t.Error("expected runFx to be called for build") |
| } |
| |
| if ctx.Files[0].AnalysisResult == nil || ctx.Files[0].AnalysisResult.Status != AnalysisStatusOk { |
| t.Errorf("expected AnalysisStatusOk, got %+v", ctx.Files[0].AnalysisResult) |
| } |
| } |
| |
| func TestVerifyBuild_Failure(t *testing.T) { |
| tmpDir := t.TempDir() |
| fuchsiaDir := filepath.Join(tmpDir, "fuchsia") |
| buildDir := filepath.Join(fuchsiaDir, "out/default") |
| os.MkdirAll(buildDir, 0755) |
| os.WriteFile(filepath.Join(buildDir, "args.gn"), []byte("foo=bar"), 0644) |
| |
| // Mock runFx to fail on build |
| oldRunFx := runFx |
| defer func() { runFx = oldRunFx }() |
| runFx = func(ctx *WorkspaceContext, dir string, args ...string) error { |
| if args[0] == "build" { |
| return fmt.Errorf("build failed") |
| } |
| return nil |
| } |
| |
| ctx := &WorkspaceContext{ |
| FuchsiaDir: fuchsiaDir, |
| BuildDir: buildDir, |
| Files: []FileEntry{ |
| { |
| AbsPath: filepath.Join(fuchsiaDir, "src/main.cc"), |
| Status: StatusFound, |
| BuildTargets: []string{"//src:main"}, |
| }, |
| }, |
| } |
| |
| if err := ctx.VerifyBuild(); err != nil { |
| t.Fatalf("VerifyBuild failed: %v", err) |
| } |
| |
| if ctx.Files[0].AnalysisResult == nil || ctx.Files[0].AnalysisResult.Status != AnalysisStatusBuildFailed { |
| t.Errorf("expected AnalysisStatusBuildFailed, got %+v", ctx.Files[0].AnalysisResult) |
| } |
| if ctx.Files[0].AnalysisResult.Message != "File failed to build." { |
| t.Errorf("expected 'File failed to build.', got %q", ctx.Files[0].AnalysisResult.Message) |
| } |
| } |
| |
| func TestVerifyBuild_GenFailure(t *testing.T) { |
| tmpDir := t.TempDir() |
| fuchsiaDir := filepath.Join(tmpDir, "fuchsia") |
| buildDir := filepath.Join(fuchsiaDir, "out/default") |
| os.MkdirAll(buildDir, 0755) |
| os.WriteFile(filepath.Join(buildDir, "args.gn"), []byte("foo=bar"), 0644) |
| |
| // Mock runFx to fail on gen |
| oldRunFx := runFx |
| defer func() { runFx = oldRunFx }() |
| runFx = func(ctx *WorkspaceContext, dir string, args ...string) error { |
| if args[0] == "gen" { |
| return fmt.Errorf("gen failed") |
| } |
| return nil |
| } |
| |
| ctx := &WorkspaceContext{ |
| FuchsiaDir: fuchsiaDir, |
| BuildDir: buildDir, |
| Files: []FileEntry{ |
| { |
| AbsPath: filepath.Join(fuchsiaDir, "src/main.cc"), |
| Status: StatusFound, |
| }, |
| }, |
| } |
| |
| if err := ctx.VerifyBuild(); err != nil { |
| t.Fatalf("VerifyBuild failed: %v", err) |
| } |
| |
| if !strings.Contains(ctx.Files[0].AnalysisError, "fx gen failed") { |
| t.Errorf("expected AnalysisError about fx gen failure, got %q", ctx.Files[0].AnalysisError) |
| } |
| } |
| |
| func TestVerifyBuild_SyncFailure(t *testing.T) { |
| tmpDir := t.TempDir() |
| fuchsiaDir := filepath.Join(tmpDir, "fuchsia") |
| buildDir := filepath.Join(fuchsiaDir, "out/default") |
| os.MkdirAll(buildDir, 0755) |
| os.WriteFile(filepath.Join(buildDir, "args.gn"), []byte("foo=bar"), 0644) |
| |
| // Create a file where ide-analysis/args.gn should go, and make it a directory to cause WriteFile to fail. |
| ideAnalysisDir := filepath.Join(fuchsiaDir, "out", ".ide-analysis") |
| os.MkdirAll(filepath.Join(ideAnalysisDir, "args.gn"), 0755) |
| |
| ctx := &WorkspaceContext{ |
| FuchsiaDir: fuchsiaDir, |
| BuildDir: buildDir, |
| Files: []FileEntry{ |
| { |
| AbsPath: filepath.Join(fuchsiaDir, "src/main.cc"), |
| Status: StatusFound, |
| }, |
| }, |
| } |
| |
| if err := ctx.VerifyBuild(); err != nil { |
| t.Fatalf("VerifyBuild failed: %v", err) |
| } |
| |
| if !strings.Contains(ctx.Files[0].AnalysisError, "failed to sync args.gn") { |
| t.Errorf("expected AnalysisError about sync failure, got %q", ctx.Files[0].AnalysisError) |
| } |
| } |
| |
| func TestPopulateTargets_DeterministicNeighbor(t *testing.T) { |
| tmpDir, err := os.MkdirTemp("", "ide-query-test-determinism") |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer os.RemoveAll(tmpDir) |
| |
| fuchsiaDir := filepath.Join(tmpDir, "fuchsia") |
| buildDir := filepath.Join(fuchsiaDir, "out/default") |
| os.MkdirAll(buildDir, 0755) |
| |
| fileA := filepath.Join(fuchsiaDir, "src/a.cc") |
| fileB := filepath.Join(fuchsiaDir, "src/b.cc") |
| headerFile := filepath.Join(fuchsiaDir, "src/header.h") |
| os.MkdirAll(filepath.Dir(fileA), 0755) |
| os.WriteFile(fileA, []byte(""), 0644) |
| os.WriteFile(fileB, []byte(""), 0644) |
| os.WriteFile(headerFile, []byte(""), 0644) |
| |
| // In the compdb, A comes before B. |
| // dirToCmd["src"] should be A's command. |
| compDb := []CompileCommand{ |
| { |
| Directory: buildDir, |
| File: "../../src/a.cc", |
| Arguments: []string{"clang++", "-o", "obj/a.o", "-c", "../../src/a.cc"}, |
| }, |
| { |
| Directory: buildDir, |
| File: "../../src/b.cc", |
| Arguments: []string{"clang++", "-o", "obj/b.o", "-c", "../../src/b.cc"}, |
| }, |
| } |
| dbContent, _ := json.Marshal(compDb) |
| os.WriteFile(filepath.Join(buildDir, "compile_commands.json"), dbContent, 0644) |
| |
| oldResolve := resolveNinjaPath |
| defer func() { resolveNinjaPath = oldResolve }() |
| resolveNinjaPath = func(ctx *WorkspaceContext, ninjaPath string) (string, error) { |
| if ninjaPath == "obj/a.o" { |
| return "//src:a", nil |
| } |
| if ninjaPath == "obj/b.o" { |
| return "//src:b", nil |
| } |
| return "", nil |
| } |
| |
| ctx := &WorkspaceContext{ |
| FuchsiaDir: fuchsiaDir, |
| BuildDir: buildDir, |
| Files: []FileEntry{ |
| { |
| AbsPath: headerFile, |
| Status: StatusFound, |
| }, |
| }, |
| } |
| |
| if err := ctx.PopulateTargets(); err != nil { |
| t.Fatalf("PopulateTargets failed: %v", err) |
| } |
| |
| // Should favor the first one in the list (//src:a) because we only |
| // set the dirToCmd map if it's not already present. |
| if len(ctx.Files[0].BuildTargets) != 1 || ctx.Files[0].BuildTargets[0] != "//src:a" { |
| t.Errorf("expected //src:a, got %v", ctx.Files[0].BuildTargets) |
| } |
| } |
| |
| func TestPopulateTargets_MultipleTargets(t *testing.T) { |
| tmpDir, err := os.MkdirTemp("", "ide-query-test-multi") |
| if err != nil { |
| t.Fatal(err) |
| } |
| defer os.RemoveAll(tmpDir) |
| |
| fuchsiaDir := filepath.Join(tmpDir, "fuchsia") |
| buildDir := filepath.Join(fuchsiaDir, "out/default") |
| os.MkdirAll(buildDir, 0755) |
| |
| sourceFile := filepath.Join(fuchsiaDir, "src/main.cc") |
| os.MkdirAll(filepath.Dir(sourceFile), 0755) |
| os.WriteFile(sourceFile, []byte(""), 0644) |
| |
| // Create a mock compile_commands.json with TWO entries for the same file. |
| compDb := []CompileCommand{ |
| { |
| Directory: buildDir, |
| File: "../../src/main.cc", |
| Arguments: []string{"clang++", "-o", "obj/foo.o", "-c", "../../src/main.cc"}, |
| }, |
| { |
| Directory: buildDir, |
| File: "../../src/main.cc", |
| Arguments: []string{"clang++", "-o", "obj/bar.o", "-c", "../../src/main.cc"}, |
| }, |
| } |
| dbContent, _ := json.Marshal(compDb) |
| os.WriteFile(filepath.Join(buildDir, "compile_commands.json"), dbContent, 0644) |
| |
| oldResolve := resolveNinjaPath |
| defer func() { resolveNinjaPath = oldResolve }() |
| resolveNinjaPath = func(ctx *WorkspaceContext, ninjaPath string) (string, error) { |
| if ninjaPath == "obj/foo.o" { |
| return "//src:foo", nil |
| } |
| if ninjaPath == "obj/bar.o" { |
| return "//src:bar", nil |
| } |
| return "", nil |
| } |
| |
| ctx := &WorkspaceContext{ |
| FuchsiaDir: fuchsiaDir, |
| BuildDir: buildDir, |
| Files: []FileEntry{ |
| { |
| AbsPath: sourceFile, |
| Status: StatusFound, |
| }, |
| }, |
| } |
| |
| if err := ctx.PopulateTargets(); err != nil { |
| t.Fatalf("PopulateTargets failed: %v", err) |
| } |
| |
| // Should pick //src:bar because it's alphabetically before //src:foo |
| if len(ctx.Files[0].BuildTargets) != 1 || ctx.Files[0].BuildTargets[0] != "//src:bar" { |
| t.Errorf("expected //src:bar, got %v", ctx.Files[0].BuildTargets) |
| } |
| } |