package builder_test

import (
	"context"
	"os"
	"syscall"
	"testing"

	remoteexecution "github.com/bazelbuild/remote-apis/build/bazel/remote/execution/v2"
	"github.com/buildbarn/bb-remote-execution/internal/mock"
	"github.com/buildbarn/bb-remote-execution/pkg/builder"
	"github.com/buildbarn/bb-storage/pkg/blobstore/buffer"
	"github.com/buildbarn/bb-storage/pkg/digest"
	"github.com/buildbarn/bb-storage/pkg/filesystem"
	"github.com/buildbarn/bb-storage/pkg/filesystem/path"
	"github.com/buildbarn/bb-storage/pkg/testutil"
	"github.com/stretchr/testify/require"

	"google.golang.org/grpc/codes"
	"google.golang.org/grpc/status"

	"go.uber.org/mock/gomock"
)

func TestOutputHierarchyCreation(t *testing.T) {
	t.Run("AbsoluteWorkingDirectory", func(t *testing.T) {
		_, err := builder.NewOutputHierarchy(&remoteexecution.Command{
			WorkingDirectory: "/tmp/hello/../..",
		}, false)
		testutil.RequireEqualStatus(t, status.Error(codes.InvalidArgument, "Invalid working directory: Path is absolute, while a relative path was expected"), err)
	})

	t.Run("InvalidWorkingDirectory", func(t *testing.T) {
		_, err := builder.NewOutputHierarchy(&remoteexecution.Command{
			WorkingDirectory: "hello/../..",
		}, false)
		testutil.RequireEqualStatus(t, status.Error(codes.InvalidArgument, "Invalid working directory: Path resolves to a location outside the input root directory"), err)
	})

	t.Run("AbsoluteOutputDirectory", func(t *testing.T) {
		_, err := builder.NewOutputHierarchy(&remoteexecution.Command{
			WorkingDirectory:  ".",
			OutputDirectories: []string{"/etc/passwd"},
		}, true)
		testutil.RequireEqualStatus(t, status.Error(codes.InvalidArgument, "Invalid output directory \"/etc/passwd\": Path is absolute, while a relative path was expected"), err)
	})

	t.Run("InvalidOutputDirectory", func(t *testing.T) {
		_, err := builder.NewOutputHierarchy(&remoteexecution.Command{
			WorkingDirectory:  "hello",
			OutputDirectories: []string{"../.."},
		}, true)
		testutil.RequireEqualStatus(t, status.Error(codes.InvalidArgument, "Invalid output directory \"../..\": Path resolves to a location outside the input root directory"), err)
	})

	t.Run("InvalidOutputFile", func(t *testing.T) {
		_, err := builder.NewOutputHierarchy(&remoteexecution.Command{
			WorkingDirectory: "hello",
			OutputFiles:      []string{".."},
		}, true)
		testutil.RequireEqualStatus(t, status.Error(codes.InvalidArgument, "Output file \"..\" resolves to the input root directory"), err)
	})
}

func TestOutputHierarchyCreateParentDirectories(t *testing.T) {
	ctrl := gomock.NewController(t)

	root := mock.NewMockParentPopulatableDirectory(ctrl)

	t.Run("Noop", func(t *testing.T) {
		// No parent directories should be created.
		oh, err := builder.NewOutputHierarchy(&remoteexecution.Command{
			WorkingDirectory: ".",
		}, false)
		require.NoError(t, err)
		require.NoError(t, oh.CreateParentDirectories(root))
	})

	t.Run("WorkingDirectory", func(t *testing.T) {
		// REv2 explicitly states that the working directory
		// must be a directory that exists in the input root.
		// Using a subdirectory as a working directory should
		// not cause any Mkdir() calls.
		oh, err := builder.NewOutputHierarchy(&remoteexecution.Command{
			WorkingDirectory: "foo/bar",
		}, true)
		require.NoError(t, err)
		require.NoError(t, oh.CreateParentDirectories(root))
	})

	t.Run("StillNoop", func(t *testing.T) {
		// All of the provided paths expand to (locations under)
		// the root directory. There is thus no need to create
		// any output directories.
		oh, err := builder.NewOutputHierarchy(&remoteexecution.Command{
			WorkingDirectory:  "foo",
			OutputDirectories: []string{".."},
			OutputFiles:       []string{"../file"},
			OutputPaths:       []string{"../path"},
		}, false)
		require.NoError(t, err)
		require.NoError(t, oh.CreateParentDirectories(root))
	})

	t.Run("Success", func(t *testing.T) {
		// Create /foo/bar/baz.
		root.EXPECT().Mkdir(path.MustNewComponent("foo"), os.FileMode(0o777))
		foo := mock.NewMockParentPopulatableDirectory(ctrl)
		root.EXPECT().EnterParentPopulatableDirectory(path.MustNewComponent("foo")).Return(foo, nil)
		foo.EXPECT().Mkdir(path.MustNewComponent("bar"), os.FileMode(0o777))
		bar := mock.NewMockParentPopulatableDirectory(ctrl)
		foo.EXPECT().EnterParentPopulatableDirectory(path.MustNewComponent("bar")).Return(bar, nil)
		bar.EXPECT().Mkdir(path.MustNewComponent("baz"), os.FileMode(0o777))
		bar.EXPECT().Close()
		// Create /foo/qux.
		foo.EXPECT().Mkdir(path.MustNewComponent("qux"), os.FileMode(0o777))
		foo.EXPECT().Close()

		oh, err := builder.NewOutputHierarchy(&remoteexecution.Command{
			WorkingDirectory:  "foo",
			OutputDirectories: []string{"bar/baz"},
			OutputFiles:       []string{"../foo/qux/xyzzy"},
		}, true)
		require.NoError(t, err)
		require.NoError(t, oh.CreateParentDirectories(root))
	})

	t.Run("SuccessPaths", func(t *testing.T) {
		// No /foo/bar/baz since OutputPaths is set.
		// Create /alice.
		root.EXPECT().Mkdir(path.MustNewComponent("alice"), os.FileMode(0o777))

		oh, err := builder.NewOutputHierarchy(&remoteexecution.Command{
			WorkingDirectory:  "foo",
			OutputDirectories: []string{"bar/baz"},
			OutputFiles:       []string{"../foo/qux/xyzzy"},
			OutputPaths:       []string{"../alice/bob"},
		}, false)
		require.NoError(t, err)
		require.NoError(t, oh.CreateParentDirectories(root))
	})

	t.Run("MkdirFailureParent", func(t *testing.T) {
		// Failure to create the parent directory of a location
		// where an output file is expected.
		root.EXPECT().Mkdir(path.MustNewComponent("foo"), os.FileMode(0o777))
		foo := mock.NewMockParentPopulatableDirectory(ctrl)
		root.EXPECT().EnterParentPopulatableDirectory(path.MustNewComponent("foo")).Return(foo, nil)
		foo.EXPECT().Mkdir(path.MustNewComponent("bar"), os.FileMode(0o777)).Return(status.Error(codes.Internal, "I/O error"))
		foo.EXPECT().Close()

		oh, err := builder.NewOutputHierarchy(&remoteexecution.Command{
			WorkingDirectory: "foo",
			OutputFiles:      []string{"bar/baz"},
		}, true)
		require.NoError(t, err)
		testutil.RequireEqualStatus(
			t,
			status.Error(codes.Internal, "Failed to create output parent directory \"foo/bar\": I/O error"),
			oh.CreateParentDirectories(root))
	})

	t.Run("MkdirFailureParentExists", func(t *testing.T) {
		// This test is identical to the previous, except that
		// the error is EEXIST. This should not cause a hard
		// failure.
		root.EXPECT().Mkdir(path.MustNewComponent("foo"), os.FileMode(0o777))
		foo := mock.NewMockParentPopulatableDirectory(ctrl)
		root.EXPECT().EnterParentPopulatableDirectory(path.MustNewComponent("foo")).Return(foo, nil)
		foo.EXPECT().Mkdir(path.MustNewComponent("bar"), os.FileMode(0o777)).Return(os.ErrExist)
		foo.EXPECT().Close()

		oh, err := builder.NewOutputHierarchy(&remoteexecution.Command{
			WorkingDirectory: "foo",
			OutputFiles:      []string{"bar/baz"},
		}, true)
		require.NoError(t, err)
		require.NoError(t, oh.CreateParentDirectories(root))
	})

	t.Run("MkdirFailureOutput", func(t *testing.T) {
		// Failure to create a location where an output
		// directory is expected.
		root.EXPECT().Mkdir(path.MustNewComponent("foo"), os.FileMode(0o777))
		foo := mock.NewMockParentPopulatableDirectory(ctrl)
		root.EXPECT().EnterParentPopulatableDirectory(path.MustNewComponent("foo")).Return(foo, nil)
		foo.EXPECT().Mkdir(path.MustNewComponent("bar"), os.FileMode(0o777)).Return(status.Error(codes.Internal, "I/O error"))
		foo.EXPECT().Close()

		oh, err := builder.NewOutputHierarchy(&remoteexecution.Command{
			WorkingDirectory:  "foo",
			OutputDirectories: []string{"bar"},
		}, true)
		require.NoError(t, err)
		testutil.RequireEqualStatus(
			t,
			status.Error(codes.Internal, "Failed to create output directory \"foo/bar\": I/O error"),
			oh.CreateParentDirectories(root))
	})

	t.Run("MkdirFailureOutputExists", func(t *testing.T) {
		// This test is identical to the previous, except that
		// the error is EEXIST. This should not cause a hard
		// failure.
		root.EXPECT().Mkdir(path.MustNewComponent("foo"), os.FileMode(0o777))
		foo := mock.NewMockParentPopulatableDirectory(ctrl)
		root.EXPECT().EnterParentPopulatableDirectory(path.MustNewComponent("foo")).Return(foo, nil)
		foo.EXPECT().Mkdir(path.MustNewComponent("bar"), os.FileMode(0o777)).Return(os.ErrExist)
		foo.EXPECT().Close()

		oh, err := builder.NewOutputHierarchy(&remoteexecution.Command{
			WorkingDirectory:  "foo",
			OutputDirectories: []string{"bar"},
		}, true)
		require.NoError(t, err)
		require.NoError(t, oh.CreateParentDirectories(root))
	})

	t.Run("EnterFailure", func(t *testing.T) {
		root.EXPECT().Mkdir(path.MustNewComponent("foo"), os.FileMode(0o777))
		foo := mock.NewMockParentPopulatableDirectory(ctrl)
		root.EXPECT().EnterParentPopulatableDirectory(path.MustNewComponent("foo")).Return(foo, nil)
		foo.EXPECT().Mkdir(path.MustNewComponent("bar"), os.FileMode(0o777))
		foo.EXPECT().EnterParentPopulatableDirectory(path.MustNewComponent("bar")).Return(nil, status.Error(codes.Internal, "I/O error"))
		foo.EXPECT().Close()

		oh, err := builder.NewOutputHierarchy(&remoteexecution.Command{
			WorkingDirectory:  "foo",
			OutputDirectories: []string{"bar/baz"},
		}, true)
		require.NoError(t, err)
		testutil.RequireEqualStatus(
			t,
			status.Error(codes.Internal, "Failed to enter output parent directory \"foo/bar\": I/O error"),
			oh.CreateParentDirectories(root))
	})
}

func TestOutputHierarchyUploadOutputs(t *testing.T) {
	ctrl, ctx := gomock.WithContext(context.Background(), t)

	root := mock.NewMockUploadableDirectory(ctrl)
	contentAddressableStorage := mock.NewMockBlobAccess(ctrl)
	digestFunction := digest.MustNewDigest("example", remoteexecution.DigestFunction_MD5, "8b1a9953c4611296a827abf8c47804d7", 5).GetDigestFunction()
	writableFileUploadDelay := make(chan struct{})

	t.Run("Noop", func(t *testing.T) {
		// Uploading of a build action with no declared outputs
		// should not trigger any I/O.
		oh, err := builder.NewOutputHierarchy(&remoteexecution.Command{
			WorkingDirectory: ".",
		}, false)
		require.NoError(t, err)
		var actionResult remoteexecution.ActionResult
		require.NoError(
			t,
			oh.UploadOutputs(
				ctx,
				root,
				contentAddressableStorage,
				digestFunction,
				writableFileUploadDelay,
				&actionResult,
				/* forceUploadTreesAndDirectories = */ false))
		require.Equal(t, remoteexecution.ActionResult{}, actionResult)
	})

	testSuccess := func(t *testing.T, command *remoteexecution.Command, expectedResult remoteexecution.ActionResult) {
		// Declare output directories, files and paths. For each
		// of these output types, let them match one of the
		// valid file types.
		foo := mock.NewMockUploadableDirectory(ctrl)
		root.EXPECT().EnterUploadableDirectory(path.MustNewComponent("foo")).Return(foo, nil)

		// Calls triggered to obtain the file type of the outputs.
		foo.EXPECT().Lstat(path.MustNewComponent("directory-directory")).Return(filesystem.NewFileInfo(path.MustNewComponent("directory-directory"), filesystem.FileTypeDirectory, false), nil)
		foo.EXPECT().Lstat(path.MustNewComponent("directory-symlink")).Return(filesystem.NewFileInfo(path.MustNewComponent("directory-symlink"), filesystem.FileTypeSymlink, false), nil)
		foo.EXPECT().Lstat(path.MustNewComponent("directory-enoent")).Return(filesystem.FileInfo{}, syscall.ENOENT)
		foo.EXPECT().Lstat(path.MustNewComponent("file-regular")).Return(filesystem.NewFileInfo(path.MustNewComponent("file-regular"), filesystem.FileTypeRegularFile, false), nil)
		foo.EXPECT().Lstat(path.MustNewComponent("file-executable")).Return(filesystem.NewFileInfo(path.MustNewComponent("file-executable"), filesystem.FileTypeRegularFile, true), nil)
		foo.EXPECT().Lstat(path.MustNewComponent("file-symlink")).Return(filesystem.NewFileInfo(path.MustNewComponent("file-symlink"), filesystem.FileTypeSymlink, false), nil)
		foo.EXPECT().Lstat(path.MustNewComponent("file-enoent")).Return(filesystem.FileInfo{}, syscall.ENOENT)
		foo.EXPECT().Lstat(path.MustNewComponent("path-regular")).Return(filesystem.NewFileInfo(path.MustNewComponent("path-regular"), filesystem.FileTypeRegularFile, false), nil)
		foo.EXPECT().Lstat(path.MustNewComponent("path-executable")).Return(filesystem.NewFileInfo(path.MustNewComponent("path-executable"), filesystem.FileTypeRegularFile, true), nil)
		foo.EXPECT().Lstat(path.MustNewComponent("path-directory")).Return(filesystem.NewFileInfo(path.MustNewComponent("path-directory"), filesystem.FileTypeDirectory, false), nil)
		foo.EXPECT().Lstat(path.MustNewComponent("path-symlink")).Return(filesystem.NewFileInfo(path.MustNewComponent("path-symlink"), filesystem.FileTypeSymlink, false), nil)
		foo.EXPECT().Lstat(path.MustNewComponent("path-enoent")).Return(filesystem.FileInfo{}, syscall.ENOENT)

		// Inspection/uploading of all non-directory outputs.
		foo.EXPECT().Readlink(path.MustNewComponent("directory-symlink")).Return(path.UNIXFormat.NewParser("directory-symlink-target"), nil)
		foo.EXPECT().UploadFile(ctx, path.MustNewComponent("file-regular"), gomock.Any(), writableFileUploadDelay).
			Return(digest.MustNewDigest("example", remoteexecution.DigestFunction_MD5, "a58c2f2281011ca2e631b39baa1ab657", 12), nil)
		foo.EXPECT().UploadFile(ctx, path.MustNewComponent("file-executable"), gomock.Any(), writableFileUploadDelay).
			Return(digest.MustNewDigest("example", remoteexecution.DigestFunction_MD5, "7590e1b46240ecb5ea65a80db7ee6fae", 15), nil)
		foo.EXPECT().Readlink(path.MustNewComponent("file-symlink")).Return(path.UNIXFormat.NewParser("file-symlink-target"), nil)
		foo.EXPECT().UploadFile(ctx, path.MustNewComponent("path-regular"), gomock.Any(), writableFileUploadDelay).
			Return(digest.MustNewDigest("example", remoteexecution.DigestFunction_MD5, "44206648b7bb2f3b0d2ed0c52ad2e269", 12), nil)
		foo.EXPECT().UploadFile(ctx, path.MustNewComponent("path-executable"), gomock.Any(), writableFileUploadDelay).
			Return(digest.MustNewDigest("example", remoteexecution.DigestFunction_MD5, "87729325cd08d300fb0e238a3a8da443", 15), nil)
		foo.EXPECT().Readlink(path.MustNewComponent("path-symlink")).Return(path.UNIXFormat.NewParser("path-symlink-target"), nil)

		// Uploading of /foo/directory-directory. Files with an
		// unknown type (UNIX sockets, FIFOs) should be ignored.
		// Returning a hard error makes debugging harder (e.g.,
		// in case the full input root is declared as an output).
		directoryDirectory := mock.NewMockUploadableDirectory(ctrl)
		foo.EXPECT().EnterUploadableDirectory(path.MustNewComponent("directory-directory")).Return(directoryDirectory, nil)
		directoryDirectory.EXPECT().ReadDir().Return([]filesystem.FileInfo{
			filesystem.NewFileInfo(path.MustNewComponent("directory"), filesystem.FileTypeDirectory, false),
			filesystem.NewFileInfo(path.MustNewComponent("executable"), filesystem.FileTypeRegularFile, true),
			filesystem.NewFileInfo(path.MustNewComponent("other"), filesystem.FileTypeOther, false),
			filesystem.NewFileInfo(path.MustNewComponent("regular"), filesystem.FileTypeRegularFile, false),
			filesystem.NewFileInfo(path.MustNewComponent("symlink"), filesystem.FileTypeSymlink, false),
		}, nil)
		directoryDirectoryDirectory := mock.NewMockUploadableDirectory(ctrl)
		directoryDirectory.EXPECT().EnterUploadableDirectory(path.MustNewComponent("directory")).Return(directoryDirectoryDirectory, nil)
		directoryDirectoryDirectory.EXPECT().ReadDir().Return(nil, nil)
		directoryDirectoryDirectory.EXPECT().Close()
		directoryDirectory.EXPECT().UploadFile(ctx, path.MustNewComponent("executable"), gomock.Any(), writableFileUploadDelay).
			Return(digest.MustNewDigest("example", remoteexecution.DigestFunction_MD5, "ee7004c7949d83f130592f15d98ca343", 10), nil)
		directoryDirectory.EXPECT().UploadFile(ctx, path.MustNewComponent("regular"), gomock.Any(), writableFileUploadDelay).
			Return(digest.MustNewDigest("example", remoteexecution.DigestFunction_MD5, "af37d08ae228a87dc6b265fd1019c97d", 7), nil)
		directoryDirectory.EXPECT().Readlink(path.MustNewComponent("symlink")).Return(path.UNIXFormat.NewParser("symlink-target"), nil)
		directoryDirectory.EXPECT().Close()
		contentAddressableStorage.EXPECT().Put(
			ctx,
			digest.MustNewDigest("example", remoteexecution.DigestFunction_MD5, "55aed4acf40a28132fb2d2de2b5962f0", 184),
			gomock.Any()).
			DoAndReturn(func(ctx context.Context, digest digest.Digest, b buffer.Buffer) error {
				m, err := b.ToProto(&remoteexecution.Tree{}, 10000)
				require.NoError(t, err)
				testutil.RequireEqualProto(t, &remoteexecution.Tree{
					Root: &remoteexecution.Directory{
						Files: []*remoteexecution.FileNode{
							{
								Name: "executable",
								Digest: &remoteexecution.Digest{
									Hash:      "ee7004c7949d83f130592f15d98ca343",
									SizeBytes: 10,
								},
								IsExecutable: true,
							},
							{
								Name: "regular",
								Digest: &remoteexecution.Digest{
									Hash:      "af37d08ae228a87dc6b265fd1019c97d",
									SizeBytes: 7,
								},
							},
						},
						Directories: []*remoteexecution.DirectoryNode{
							{
								Name: "directory",
								Digest: &remoteexecution.Digest{
									Hash:      "d41d8cd98f00b204e9800998ecf8427e",
									SizeBytes: 0,
								},
							},
						},
						Symlinks: []*remoteexecution.SymlinkNode{
							{
								Name:   "symlink",
								Target: "symlink-target",
							},
						},
					},
					Children: []*remoteexecution.Directory{
						{},
					},
				}, m)
				return nil
			})

		// Uploading of /foo/path-directory.
		pathDirectory := mock.NewMockUploadableDirectory(ctrl)
		foo.EXPECT().EnterUploadableDirectory(path.MustNewComponent("path-directory")).Return(pathDirectory, nil)
		pathDirectory.EXPECT().ReadDir().Return(nil, nil)
		pathDirectory.EXPECT().Close()
		contentAddressableStorage.EXPECT().Put(
			ctx,
			digest.MustNewDigest("example", remoteexecution.DigestFunction_MD5, "9dd94c5a4b02914af42e8e6372e0b709", 2),
			gomock.Any()).
			DoAndReturn(func(ctx context.Context, digest digest.Digest, b buffer.Buffer) error {
				m, err := b.ToProto(&remoteexecution.Tree{}, 10000)
				require.NoError(t, err)
				testutil.RequireEqualProto(t, &remoteexecution.Tree{
					Root: &remoteexecution.Directory{},
				}, m)
				return nil
			})

		foo.EXPECT().Close()

		oh, err := builder.NewOutputHierarchy(command, true)
		require.NoError(t, err)
		var actionResult remoteexecution.ActionResult
		require.NoError(
			t,
			oh.UploadOutputs(
				ctx,
				root,
				contentAddressableStorage,
				digestFunction,
				writableFileUploadDelay,
				&actionResult,
				/* forceUploadTreesAndDirectories = */ false))
		require.Equal(t, expectedResult, actionResult)
	}

	t.Run("Success", func(t *testing.T) {
		t.Run("FilesAndDirectories", func(t *testing.T) {
			testSuccess(t, &remoteexecution.Command{
				WorkingDirectory: "foo",
				OutputDirectories: []string{
					"directory-directory",
					"../foo/directory-directory",
					"directory-symlink",
					"../foo/directory-symlink",
					"directory-enoent",
					"../foo/directory-enoent",
					"path-directory",
					"../foo/path-directory",
				},
				OutputFiles: []string{
					"file-regular",
					"../foo/file-regular",
					"file-executable",
					"../foo/file-executable",
					"file-symlink",
					"../foo/file-symlink",
					"file-enoent",
					"../foo/file-enoent",
					"path-regular",
					"../foo/path-regular",
					"path-executable",
					"../foo/path-executable",
					"path-symlink",
					"../foo/path-symlink",
					"path-enoent",
					"../foo/path-enoent",
				},
			}, remoteexecution.ActionResult{
				OutputDirectories: []*remoteexecution.OutputDirectory{
					{
						Path: "directory-directory",
						TreeDigest: &remoteexecution.Digest{
							Hash:      "55aed4acf40a28132fb2d2de2b5962f0",
							SizeBytes: 184,
						},
						IsTopologicallySorted: true,
					},
					{
						Path: "../foo/directory-directory",
						TreeDigest: &remoteexecution.Digest{
							Hash:      "55aed4acf40a28132fb2d2de2b5962f0",
							SizeBytes: 184,
						},
						IsTopologicallySorted: true,
					},
					{
						Path: "path-directory",
						TreeDigest: &remoteexecution.Digest{
							Hash:      "9dd94c5a4b02914af42e8e6372e0b709",
							SizeBytes: 2,
						},
						IsTopologicallySorted: true,
					},
					{
						Path: "../foo/path-directory",
						TreeDigest: &remoteexecution.Digest{
							Hash:      "9dd94c5a4b02914af42e8e6372e0b709",
							SizeBytes: 2,
						},
						IsTopologicallySorted: true,
					},
				},
				OutputDirectorySymlinks: []*remoteexecution.OutputSymlink{
					{
						Path:   "directory-symlink",
						Target: "directory-symlink-target",
					},
					{
						Path:   "../foo/directory-symlink",
						Target: "directory-symlink-target",
					},
				},
				OutputFiles: []*remoteexecution.OutputFile{
					{
						Path: "file-executable",
						Digest: &remoteexecution.Digest{
							Hash:      "7590e1b46240ecb5ea65a80db7ee6fae",
							SizeBytes: 15,
						},
						IsExecutable: true,
					},
					{
						Path: "../foo/file-executable",
						Digest: &remoteexecution.Digest{
							Hash:      "7590e1b46240ecb5ea65a80db7ee6fae",
							SizeBytes: 15,
						},
						IsExecutable: true,
					},
					{
						Path: "file-regular",
						Digest: &remoteexecution.Digest{
							Hash:      "a58c2f2281011ca2e631b39baa1ab657",
							SizeBytes: 12,
						},
					},
					{
						Path: "../foo/file-regular",
						Digest: &remoteexecution.Digest{
							Hash:      "a58c2f2281011ca2e631b39baa1ab657",
							SizeBytes: 12,
						},
					},
					{
						Path: "path-executable",
						Digest: &remoteexecution.Digest{
							Hash:      "87729325cd08d300fb0e238a3a8da443",
							SizeBytes: 15,
						},
						IsExecutable: true,
					},
					{
						Path: "../foo/path-executable",
						Digest: &remoteexecution.Digest{
							Hash:      "87729325cd08d300fb0e238a3a8da443",
							SizeBytes: 15,
						},
						IsExecutable: true,
					},
					{
						Path: "path-regular",
						Digest: &remoteexecution.Digest{
							Hash:      "44206648b7bb2f3b0d2ed0c52ad2e269",
							SizeBytes: 12,
						},
					},
					{
						Path: "../foo/path-regular",
						Digest: &remoteexecution.Digest{
							Hash:      "44206648b7bb2f3b0d2ed0c52ad2e269",
							SizeBytes: 12,
						},
					},
				},
				OutputFileSymlinks: []*remoteexecution.OutputSymlink{
					{
						Path:   "file-symlink",
						Target: "file-symlink-target",
					},
					{
						Path:   "../foo/file-symlink",
						Target: "file-symlink-target",
					},
					{
						Path:   "path-symlink",
						Target: "path-symlink-target",
					},
					{
						Path:   "../foo/path-symlink",
						Target: "path-symlink-target",
					},
				},
			})
		})
		t.Run("Paths", func(t *testing.T) {
			testSuccess(t, &remoteexecution.Command{
				WorkingDirectory: "foo",
				OutputPaths: []string{
					"file-regular",
					"../foo/file-regular",
					"file-executable",
					"../foo/file-executable",
					"file-symlink",
					"../foo/file-symlink",
					"file-enoent",
					"../foo/file-enoent",
					"directory-directory",
					"../foo/directory-directory",
					"directory-symlink",
					"../foo/directory-symlink",
					"directory-enoent",
					"../foo/directory-enoent",
					"path-directory",
					"../foo/path-directory",
					"path-regular",
					"../foo/path-regular",
					"path-executable",
					"../foo/path-executable",
					"path-symlink",
					"../foo/path-symlink",
					"path-enoent",
					"../foo/path-enoent",
				},
			}, remoteexecution.ActionResult{
				OutputDirectories: []*remoteexecution.OutputDirectory{
					{
						Path: "directory-directory",
						TreeDigest: &remoteexecution.Digest{
							Hash:      "55aed4acf40a28132fb2d2de2b5962f0",
							SizeBytes: 184,
						},
						IsTopologicallySorted: true,
					},
					{
						Path: "../foo/directory-directory",
						TreeDigest: &remoteexecution.Digest{
							Hash:      "55aed4acf40a28132fb2d2de2b5962f0",
							SizeBytes: 184,
						},
						IsTopologicallySorted: true,
					},
					{
						Path: "path-directory",
						TreeDigest: &remoteexecution.Digest{
							Hash:      "9dd94c5a4b02914af42e8e6372e0b709",
							SizeBytes: 2,
						},
						IsTopologicallySorted: true,
					},
					{
						Path: "../foo/path-directory",
						TreeDigest: &remoteexecution.Digest{
							Hash:      "9dd94c5a4b02914af42e8e6372e0b709",
							SizeBytes: 2,
						},
						IsTopologicallySorted: true,
					},
				},
				OutputFiles: []*remoteexecution.OutputFile{
					{
						Path: "file-executable",
						Digest: &remoteexecution.Digest{
							Hash:      "7590e1b46240ecb5ea65a80db7ee6fae",
							SizeBytes: 15,
						},
						IsExecutable: true,
					},
					{
						Path: "../foo/file-executable",
						Digest: &remoteexecution.Digest{
							Hash:      "7590e1b46240ecb5ea65a80db7ee6fae",
							SizeBytes: 15,
						},
						IsExecutable: true,
					},
					{
						Path: "file-regular",
						Digest: &remoteexecution.Digest{
							Hash:      "a58c2f2281011ca2e631b39baa1ab657",
							SizeBytes: 12,
						},
					},
					{
						Path: "../foo/file-regular",
						Digest: &remoteexecution.Digest{
							Hash:      "a58c2f2281011ca2e631b39baa1ab657",
							SizeBytes: 12,
						},
					},
					{
						Path: "path-executable",
						Digest: &remoteexecution.Digest{
							Hash:      "87729325cd08d300fb0e238a3a8da443",
							SizeBytes: 15,
						},
						IsExecutable: true,
					},
					{
						Path: "../foo/path-executable",
						Digest: &remoteexecution.Digest{
							Hash:      "87729325cd08d300fb0e238a3a8da443",
							SizeBytes: 15,
						},
						IsExecutable: true,
					},
					{
						Path: "path-regular",
						Digest: &remoteexecution.Digest{
							Hash:      "44206648b7bb2f3b0d2ed0c52ad2e269",
							SizeBytes: 12,
						},
					},
					{
						Path: "../foo/path-regular",
						Digest: &remoteexecution.Digest{
							Hash:      "44206648b7bb2f3b0d2ed0c52ad2e269",
							SizeBytes: 12,
						},
					},
				},
				OutputSymlinks: []*remoteexecution.OutputSymlink{
					{
						Path:   "directory-symlink",
						Target: "directory-symlink-target",
					},
					{
						Path:   "../foo/directory-symlink",
						Target: "directory-symlink-target",
					},
					{
						Path:   "file-symlink",
						Target: "file-symlink-target",
					},
					{
						Path:   "../foo/file-symlink",
						Target: "file-symlink-target",
					},
					{
						Path:   "path-symlink",
						Target: "path-symlink-target",
					},
					{
						Path:   "../foo/path-symlink",
						Target: "path-symlink-target",
					},
				},
			})
		})
	})

	t.Run("RootDirectory", func(t *testing.T) {
		// Special case: it is also permitted to add the root
		// directory as an REv2.0 output directory. This
		// shouldn't cause any Lstat() calls, as the root
		// directory always exists. It is also impossible to
		// call Lstat() on it, as that would require us to
		// traverse upwards.
		root.EXPECT().ReadDir().Return(nil, nil)
		contentAddressableStorage.EXPECT().Put(
			ctx,
			digest.MustNewDigest("example", remoteexecution.DigestFunction_MD5, "9dd94c5a4b02914af42e8e6372e0b709", 2),
			gomock.Any()).
			DoAndReturn(func(ctx context.Context, digest digest.Digest, b buffer.Buffer) error {
				m, err := b.ToProto(&remoteexecution.Tree{}, 10000)
				require.NoError(t, err)
				testutil.RequireEqualProto(t, &remoteexecution.Tree{
					Root: &remoteexecution.Directory{},
				}, m)
				return nil
			})

		oh, err := builder.NewOutputHierarchy(&remoteexecution.Command{
			WorkingDirectory:  "foo",
			OutputDirectories: []string{".."},
		}, true)
		require.NoError(t, err)
		var actionResult remoteexecution.ActionResult
		require.NoError(
			t,
			oh.UploadOutputs(
				ctx,
				root,
				contentAddressableStorage,
				digestFunction,
				writableFileUploadDelay,
				&actionResult,
				/* forceUploadTreesAndDirectories = */ false))
		require.Equal(t, remoteexecution.ActionResult{
			OutputDirectories: []*remoteexecution.OutputDirectory{
				{
					Path: "..",
					TreeDigest: &remoteexecution.Digest{
						Hash:      "9dd94c5a4b02914af42e8e6372e0b709",
						SizeBytes: 2,
					},
					IsTopologicallySorted: true,
				},
			},
		}, actionResult)
	})

	t.Run("RootPath", func(t *testing.T) {
		// Similar to the previous test, it is also permitted to
		// add the root directory as an REv2.1 output path.
		root.EXPECT().ReadDir().Return(nil, nil)
		contentAddressableStorage.EXPECT().Put(
			ctx,
			digest.MustNewDigest("example", remoteexecution.DigestFunction_MD5, "9dd94c5a4b02914af42e8e6372e0b709", 2),
			gomock.Any()).
			DoAndReturn(func(ctx context.Context, digest digest.Digest, b buffer.Buffer) error {
				m, err := b.ToProto(&remoteexecution.Tree{}, 10000)
				require.NoError(t, err)
				testutil.RequireEqualProto(t, &remoteexecution.Tree{
					Root: &remoteexecution.Directory{},
				}, m)
				return nil
			})

		oh, err := builder.NewOutputHierarchy(&remoteexecution.Command{
			WorkingDirectory: "foo",
			OutputPaths:      []string{".."},
		}, false)
		require.NoError(t, err)
		var actionResult remoteexecution.ActionResult
		require.NoError(
			t,
			oh.UploadOutputs(
				ctx,
				root,
				contentAddressableStorage,
				digestFunction,
				writableFileUploadDelay,
				&actionResult,
				/* forceUploadTreesAndDirectories = */ false))
		require.Equal(t, remoteexecution.ActionResult{
			OutputDirectories: []*remoteexecution.OutputDirectory{
				{
					Path: "..",
					TreeDigest: &remoteexecution.Digest{
						Hash:      "9dd94c5a4b02914af42e8e6372e0b709",
						SizeBytes: 2,
					},
					IsTopologicallySorted: true,
				},
			},
		}, actionResult)
	})

	t.Run("LstatFailureDirectory", func(t *testing.T) {
		// Failure to Lstat() an output directory should cause
		// it to be skipped.
		root.EXPECT().Lstat(path.MustNewComponent("foo")).Return(filesystem.FileInfo{}, status.Error(codes.Internal, "I/O error"))

		oh, err := builder.NewOutputHierarchy(&remoteexecution.Command{
			WorkingDirectory:  "",
			OutputDirectories: []string{"foo"},
		}, true)
		require.NoError(t, err)
		var actionResult remoteexecution.ActionResult
		testutil.RequireEqualStatus(
			t,
			status.Error(codes.Internal, "Failed to read attributes of output directory \"foo\": I/O error"),
			oh.UploadOutputs(
				ctx,
				root,
				contentAddressableStorage,
				digestFunction,
				writableFileUploadDelay,
				&actionResult,
				/* forceUploadTreesAndDirectories = */ false))
		require.Equal(t, remoteexecution.ActionResult{}, actionResult)
	})

	t.Run("LstatFailureFile", func(t *testing.T) {
		// Failure to Lstat() an output file should cause it to
		// be skipped.
		root.EXPECT().Lstat(path.MustNewComponent("foo")).Return(filesystem.FileInfo{}, status.Error(codes.Internal, "I/O error"))

		oh, err := builder.NewOutputHierarchy(&remoteexecution.Command{
			WorkingDirectory: "",
			OutputFiles:      []string{"foo"},
		}, true)
		require.NoError(t, err)
		var actionResult remoteexecution.ActionResult
		testutil.RequireEqualStatus(
			t,
			status.Error(codes.Internal, "Failed to read attributes of output file \"foo\": I/O error"),
			oh.UploadOutputs(
				ctx,
				root,
				contentAddressableStorage,
				digestFunction,
				writableFileUploadDelay,
				&actionResult,
				/* forceUploadTreesAndDirectories = */ false))
		require.Equal(t, remoteexecution.ActionResult{}, actionResult)
	})

	t.Run("LstatFailurePath", func(t *testing.T) {
		// Failure to Lstat() an output path should cause it to
		// be skipped.
		root.EXPECT().Lstat(path.MustNewComponent("foo")).Return(filesystem.FileInfo{}, status.Error(codes.Internal, "I/O error"))

		oh, err := builder.NewOutputHierarchy(&remoteexecution.Command{
			WorkingDirectory: "",
			OutputPaths:      []string{"foo"},
		}, false)
		require.NoError(t, err)
		var actionResult remoteexecution.ActionResult
		testutil.RequireEqualStatus(
			t,
			status.Error(codes.Internal, "Failed to read attributes of output path \"foo\": I/O error"),
			oh.UploadOutputs(
				ctx,
				root,
				contentAddressableStorage,
				digestFunction,
				writableFileUploadDelay,
				&actionResult,
				/* forceUploadTreesAndDirectories = */ false))
		require.Equal(t, remoteexecution.ActionResult{}, actionResult)
	})

	t.Run("OutputDirectoryFormatTreeAndDirectory", func(t *testing.T) {
		// If the client sets Command's output_directory_format
		// to TREE_AND_DIRECTORY, we must store both Tree and
		// Directory messages in the CAS.
		root.EXPECT().ReadDir().Return([]filesystem.FileInfo{
			filesystem.NewFileInfo(path.MustNewComponent("directory1"), filesystem.FileTypeDirectory, false),
			filesystem.NewFileInfo(path.MustNewComponent("file1"), filesystem.FileTypeRegularFile, false),
			filesystem.NewFileInfo(path.MustNewComponent("symlink1"), filesystem.FileTypeSymlink, false),
		}, nil)
		root.EXPECT().UploadFile(ctx, path.MustNewComponent("file1"), gomock.Any(), writableFileUploadDelay).
			Return(digest.MustNewDigest("example", remoteexecution.DigestFunction_MD5, "132d36a32eb9e41afb86d8ba65fe9657", 123), nil)
		root.EXPECT().Readlink(path.MustNewComponent("symlink1")).Return(path.UNIXFormat.NewParser("target1"), nil)

		directory1 := mock.NewMockUploadableDirectory(ctrl)
		root.EXPECT().EnterUploadableDirectory(path.MustNewComponent("directory1")).Return(directory1, nil)
		directory1.EXPECT().ReadDir().Return([]filesystem.FileInfo{
			filesystem.NewFileInfo(path.MustNewComponent("file2"), filesystem.FileTypeRegularFile, false),
			filesystem.NewFileInfo(path.MustNewComponent("symlink2"), filesystem.FileTypeSymlink, false),
		}, nil)
		directory1.EXPECT().UploadFile(ctx, path.MustNewComponent("file2"), gomock.Any(), writableFileUploadDelay).
			Return(digest.MustNewDigest("example", remoteexecution.DigestFunction_MD5, "09ae70542cc258d5c1007d774da5ccb1", 456), nil)
		directory1.EXPECT().Readlink(path.MustNewComponent("symlink2")).Return(path.UNIXFormat.NewParser("target2"), nil)
		directory1.EXPECT().Close()

		rootDirectory := &remoteexecution.Directory{
			Directories: []*remoteexecution.DirectoryNode{{
				Name: "directory1",
				Digest: &remoteexecution.Digest{
					Hash:      "460270223db29e8867bad29c658c1395",
					SizeBytes: 69,
				},
			}},
			Files: []*remoteexecution.FileNode{{
				Name: "file1",
				Digest: &remoteexecution.Digest{
					Hash:      "132d36a32eb9e41afb86d8ba65fe9657",
					SizeBytes: 123,
				},
			}},
			Symlinks: []*remoteexecution.SymlinkNode{{
				Name:   "symlink1",
				Target: "target1",
			}},
		}
		directory1Directory := &remoteexecution.Directory{
			Files: []*remoteexecution.FileNode{{
				Name: "file2",
				Digest: &remoteexecution.Digest{
					Hash:      "09ae70542cc258d5c1007d774da5ccb1",
					SizeBytes: 456,
				},
			}},
			Symlinks: []*remoteexecution.SymlinkNode{{
				Name:   "symlink2",
				Target: "target2",
			}},
		}

		contentAddressableStorage.EXPECT().Put(
			ctx,
			digest.MustNewDigest("example", remoteexecution.DigestFunction_MD5, "aa5a55cc8d4d32abd00adf5dd1ed93b5", 193),
			gomock.Any()).
			DoAndReturn(func(ctx context.Context, digest digest.Digest, b buffer.Buffer) error {
				m, err := b.ToProto(&remoteexecution.Tree{}, 10000)
				require.NoError(t, err)
				testutil.RequireEqualProto(t, &remoteexecution.Tree{
					Root: rootDirectory,
					Children: []*remoteexecution.Directory{
						directory1Directory,
					},
				}, m)
				return nil
			})
		contentAddressableStorage.EXPECT().Put(
			ctx,
			digest.MustNewDigest("example", remoteexecution.DigestFunction_MD5, "f782fc2043b00886534aee47de8c522a", 120),
			gomock.Any()).
			DoAndReturn(func(ctx context.Context, digest digest.Digest, b buffer.Buffer) error {
				m, err := b.ToProto(&remoteexecution.Directory{}, 10000)
				require.NoError(t, err)
				testutil.RequireEqualProto(t, rootDirectory, m)
				return nil
			})
		contentAddressableStorage.EXPECT().Put(
			ctx,
			digest.MustNewDigest("example", remoteexecution.DigestFunction_MD5, "460270223db29e8867bad29c658c1395", 69),
			gomock.Any()).
			DoAndReturn(func(ctx context.Context, digest digest.Digest, b buffer.Buffer) error {
				m, err := b.ToProto(&remoteexecution.Directory{}, 10000)
				require.NoError(t, err)
				testutil.RequireEqualProto(t, directory1Directory, m)
				return nil
			})

		oh, err := builder.NewOutputHierarchy(&remoteexecution.Command{
			OutputPaths:           []string{"."},
			OutputDirectoryFormat: remoteexecution.Command_TREE_AND_DIRECTORY,
		}, false)
		require.NoError(t, err)
		var actionResult remoteexecution.ActionResult
		require.NoError(
			t,
			oh.UploadOutputs(
				ctx,
				root,
				contentAddressableStorage,
				digestFunction,
				writableFileUploadDelay,
				&actionResult,
				/* forceUploadTreesAndDirectories = */ false))
		testutil.RequireEqualProto(t, &remoteexecution.ActionResult{
			OutputDirectories: []*remoteexecution.OutputDirectory{{
				Path: ".",
				TreeDigest: &remoteexecution.Digest{
					Hash:      "aa5a55cc8d4d32abd00adf5dd1ed93b5",
					SizeBytes: 193,
				},
				IsTopologicallySorted: true,
				RootDirectoryDigest: &remoteexecution.Digest{
					Hash:      "f782fc2043b00886534aee47de8c522a",
					SizeBytes: 120,
				},
			}},
		}, &actionResult)
	})

	// TODO: Are there other cases we'd like to unit test?
}
