blob: b87135b49154ad825445ba087b871a3a69dbf13a [file]
// Copyright 2026 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package build
import (
"errors"
"io/fs"
"os"
"path/filepath"
"slices"
"testing"
"github.com/golang/glog"
"google.golang.org/protobuf/types/known/anypb"
rpb "go.chromium.org/build/remote-apis/build/bazel/remote/execution/v2"
"go.chromium.org/build/siso/execute"
pb "go.chromium.org/build/siso/execute/proto"
"go.chromium.org/build/siso/hashfs"
"go.chromium.org/build/siso/path"
)
func init() {
glog.Infof("create log file before setting TMPDIR")
}
func setTapResult(cmd *execute.Cmd, reads, writes, deletes []string) error {
tapData := &pb.TapResult{
Reads: reads,
Writes: writes,
Deletes: deletes,
}
any, err := anypb.New(tapData)
if err != nil {
return err
}
res := &rpb.ActionResult{
ExecutionMetadata: &rpb.ExecutedActionMetadata{
AuxiliaryMetadata: []*anypb.Any{any},
},
}
cmd.SetActionResult(res, false)
return nil
}
func setupDirForTapTest(t *testing.T) (string, string) {
tmpdir := t.TempDir()
dir := t.TempDir()
dir, err := filepath.EvalSymlinks(dir)
if err != nil {
t.Fatal(err)
}
t.Setenv("TMPDIR", tmpdir)
t.Setenv("TMP", tmpdir)
if path.New(dir).HasPrefix(path.New(tmpdir)) {
t.Fatalf("dir=%q under TMPDIR=%q", dir, tmpdir)
}
wsDir := filepath.Join(dir, "workspace")
if err := os.MkdirAll(wsDir, 0755); err != nil {
t.Fatal(err)
}
customTmpDir := filepath.Join(dir, "workspace/out/soong/.temp")
if err := os.MkdirAll(customTmpDir, 0755); err != nil {
t.Fatal(err)
}
return dir, tmpdir
}
func TestTapCanonicalizeCmd_NoTapData(t *testing.T) {
ctx := t.Context()
dir, _ := setupDirForTapTest(t)
wsDir := filepath.Join(dir, "workspace")
hfs, err := hashfs.New(ctx, hashfs.Option{})
if err != nil {
t.Fatal(err)
}
defer hfs.Close(ctx)
err = hfs.WaitReady(ctx)
if err != nil {
t.Fatal(err)
}
b := &Builder{
hashFS: hfs,
path: NewPath(wsDir, "out/siso"),
}
cmd := &execute.Cmd{
WorkspaceRoot: wsDir,
}
err = b.tapCanonicalizeCmd(ctx, cmd)
if err == nil {
t.Errorf("tapCanonicalizeCmd(cmd) = nil; want error")
}
}
func TestTapCanonicalizeCmd(t *testing.T) {
ctx := t.Context()
dir, tmpDir := setupDirForTapTest(t)
wsDir := filepath.Join(dir, "workspace")
hfs, err := hashfs.New(ctx, hashfs.Option{})
if err != nil {
t.Fatal(err)
}
defer hfs.Close(ctx)
if err := hfs.WaitReady(ctx); err != nil {
t.Fatal(err)
}
setupFile := func(root, rel string) string {
t.Helper()
abs := filepath.Join(root, rel)
if err := os.MkdirAll(filepath.Dir(abs), 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(abs, []byte("content"), 0644); err != nil {
t.Fatal(err)
}
return abs
}
setupDir := func(root, rel string) string {
t.Helper()
abs := filepath.Join(root, rel)
if err := os.MkdirAll(abs, 0755); err != nil {
t.Fatal(err)
}
return abs
}
b := &Builder{
hashFS: hfs,
path: NewPath(wsDir, "out/siso"),
}
existingInput := setupFile(wsDir, "input_existing.h")
newInput := setupFile(wsDir, "input_new.h")
nonExistentInput := filepath.Join(wsDir, "input_nonexistent.h")
outsideInput := setupFile(tmpDir, "outside_input.h")
existingOutput := setupFile(wsDir, "out/siso/output_existing.o")
newOutput := setupFile(wsDir, "out/siso/output_new.o")
dirOutput := setupDir(wsDir, "out/siso/output_dir")
toDeleteFile := setupFile(wsDir, "to_delete.txt")
// Cache toDeleteFile in hashFS first
_, err = hfs.Stat(ctx, wsDir, path.New("to_delete.txt"))
if err != nil {
t.Fatalf("hfs.Stat to_delete.txt failed: %v", err)
}
// Remove toDeleteFile from disk to simulate action deletion
if err := os.Remove(toDeleteFile); err != nil {
t.Fatal(err)
}
cmd := &execute.Cmd{
WorkspaceRoot: wsDir,
Inputs: []path.Path{path.New("input_existing.h")},
Outputs: []path.Path{path.New("out/siso/output_existing.o")},
}
err = setTapResult(cmd,
[]string{existingInput, newInput, nonExistentInput, outsideInput},
[]string{existingOutput, newOutput, dirOutput},
[]string{toDeleteFile},
)
if err != nil {
t.Fatal(err)
}
err = b.tapCanonicalizeCmd(ctx, cmd)
if err != nil {
t.Fatalf("tapCanonicalizeCmd returned error: %v", err)
}
if !cmd.Pure {
t.Errorf("cmd.Pure = false; want true")
}
wantInputs := []path.Path{path.New("input_existing.h"), path.New("input_new.h")}
if !slices.Equal(cmd.Inputs, wantInputs) {
t.Errorf("cmd.Inputs = %v; want %v", cmd.Inputs, wantInputs)
}
wantOutputs := []path.Path{path.New("out/siso/output_existing.o"), path.New("out/siso/output_new.o")}
if !slices.Equal(cmd.Outputs, wantOutputs) {
t.Errorf("cmd.Outputs = %v; want %v", cmd.Outputs, wantOutputs)
}
// Verify to_delete.txt is forgotten from hashFS and returns ErrNotExist
_, err = hfs.Stat(ctx, wsDir, path.New("to_delete.txt"))
if !errors.Is(err, fs.ErrNotExist) {
t.Errorf("hfs.Stat(to_delete.txt) = %v; want %v after forget", err, fs.ErrNotExist)
}
}
func TestTapCanonicalizeCmd_IgnoreOSTMPDIR(t *testing.T) {
ctx := t.Context()
dir, _ := setupDirForTapTest(t)
wsDir := filepath.Join(dir, "workspace")
customTmpDir := filepath.Join(dir, "workspace/out/soong/.temp")
t.Setenv("TMPDIR", customTmpDir)
t.Setenv("TMP", customTmpDir)
hfs, err := hashfs.New(ctx, hashfs.Option{})
if err != nil {
t.Fatal(err)
}
defer hfs.Close(ctx)
err = hfs.WaitReady(ctx)
if err != nil {
t.Fatal(err)
}
setupFile := func(abs string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(abs), 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(abs, []byte("content"), 0644); err != nil {
t.Fatal(err)
}
}
wsInput := filepath.Join(wsDir, "input.h")
setupFile(wsInput)
tmpRead := filepath.Join(os.TempDir(), "tmp_read.h")
setupFile(tmpRead)
defer os.Remove(tmpRead)
customTmpRead := filepath.Join(customTmpDir, "custom_tmp_read.h")
setupFile(customTmpRead)
customTmpWrite := filepath.Join(customTmpDir, "custom_tmp_write.o")
setupFile(customTmpWrite)
b := &Builder{
hashFS: hfs,
path: NewPath(wsDir, "out/siso"),
}
cmd := &execute.Cmd{
WorkspaceRoot: wsDir,
}
err = setTapResult(cmd,
[]string{wsInput, tmpRead, customTmpRead},
[]string{customTmpWrite},
nil,
)
if err != nil {
t.Fatal(err)
}
err = b.tapCanonicalizeCmd(ctx, cmd)
if err != nil {
t.Fatalf("tapCanonicalizeCmd returned error: %v", err)
}
wantInputs := []path.Path{path.New("input.h")}
if !slices.Equal(cmd.Inputs, wantInputs) {
t.Errorf("cmd.Inputs = %v; want %v (tmp reads should be ignored)", cmd.Inputs, wantInputs)
}
if len(cmd.Outputs) != 0 {
t.Errorf("cmd.Outputs = %v; want empty (tmp writes should be ignored)", cmd.Outputs)
}
}
func TestTapCanonicalizeCmd_IgnoreCmdEnvTMPDIR(t *testing.T) {
ctx := t.Context()
dir, _ := setupDirForTapTest(t)
wsDir := filepath.Join(dir, "workspace")
customTmpDir := filepath.Join(dir, "workspace/out/soong/.temp")
hfs, err := hashfs.New(ctx, hashfs.Option{})
if err != nil {
t.Fatal(err)
}
defer hfs.Close(ctx)
err = hfs.WaitReady(ctx)
if err != nil {
t.Fatal(err)
}
setupFile := func(abs string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(abs), 0755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(abs, []byte("content"), 0644); err != nil {
t.Fatal(err)
}
}
wsInput := filepath.Join(wsDir, "input.h")
setupFile(wsInput)
tmpRead := filepath.Join(os.TempDir(), "tmp_read.h")
setupFile(tmpRead)
defer os.Remove(tmpRead)
customTmpRead := filepath.Join(customTmpDir, "custom_tmp_read.h")
setupFile(customTmpRead)
customTmpWrite := filepath.Join(customTmpDir, "custom_tmp_write.o")
setupFile(customTmpWrite)
b := &Builder{
hashFS: hfs,
path: NewPath(wsDir, "out/siso"),
}
cmd := &execute.Cmd{
WorkspaceRoot: wsDir,
Env: []string{"TMPDIR=" + customTmpDir},
}
err = setTapResult(cmd,
[]string{wsInput, tmpRead, customTmpRead},
[]string{customTmpWrite},
nil,
)
if err != nil {
t.Fatal(err)
}
err = b.tapCanonicalizeCmd(ctx, cmd)
if err != nil {
t.Fatalf("tapCanonicalizeCmd returned error: %v", err)
}
wantInputs := []path.Path{path.New("input.h")}
if !slices.Equal(cmd.Inputs, wantInputs) {
t.Errorf("cmd.Inputs = %v; want %v (tmp reads should be ignored)", cmd.Inputs, wantInputs)
}
if len(cmd.Outputs) != 0 {
t.Errorf("cmd.Outputs = %v; want empty (tmp writes should be ignored)", cmd.Outputs)
}
}