/*
 * Copyright 2022 Bloomberg Finance LP
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <gtest/gtest.h>
#include <string>

#include <buildboxcommon_digestgenerator.h>
#include <buildboxcommon_merklize.h>
#include <buildboxcommon_protos.h>
#include <buildboxcommon_remoteexecutionclient.h>

#include <build/bazel/remote/execution/v2/remote_execution_mock.grpc.pb.h>
#include <build/buildgrid/local_cas_mock.grpc.pb.h>
#include <google/bytestream/bytestream_mock.grpc.pb.h>
#include <grpcpp/test/mock_stream.h>

#include <trexe_actionbuilder.h>

using namespace ::testing;
using namespace trexe;

using buildboxcommon::DigestGenerator;

namespace {
const auto digestFunctionInitializer = []() {
    buildboxcommon::DigestGenerator::init();
    return 0;
}();
} // namespace

TEST(ActionBuilderTest, TestEchoNoInputsNoOutputs)
{
    auto actionData =
        buildAction(nullptr, {"/bin/echo", "hello world"}, ".", {}, nullptr,
                    {}, {}, {}, 0, false, false, "", {}, 0);

    // Check `Command` proto
    const auto command = actionData.d_commandProto;
    const auto commandDigest =
        DigestGenerator::hash(command.SerializeAsString());
    ASSERT_EQ(commandDigest.hash(), actionData.d_commandDigest.hash());
    ASSERT_EQ(commandDigest.size_bytes(),
              actionData.d_commandDigest.size_bytes());

    const auto &args = command.arguments();
    ASSERT_EQ(2, command.arguments_size());
    ASSERT_EQ("/bin/echo", args[0]);
    ASSERT_EQ("hello world", args[1]);

    ASSERT_EQ(0, command.output_paths_size());

    ASSERT_TRUE(command.environment_variables().empty());
    ASSERT_TRUE(commandPlatformDeprecated(command).properties().empty());

    // Check `Action` proto
    const auto action = actionData.d_actionProto;
    const auto actionDigest =
        DigestGenerator::hash(action.SerializeAsString());
    ASSERT_EQ(actionDigest.hash(), actionData.d_actionDigest.hash());
    ASSERT_EQ(actionDigest.size_bytes(),
              actionData.d_actionDigest.size_bytes());

    ASSERT_EQ(commandDigest.hash(), action.command_digest().hash());
    ASSERT_EQ(commandDigest.size_bytes(),
              action.command_digest().size_bytes());

    // Empty input root
    NestedDirectory emptyDir;
    const auto emptyDirDigest = emptyDir.to_digest();
    ASSERT_EQ(emptyDirDigest.hash(), action.input_root_digest().hash());
    ASSERT_EQ(emptyDirDigest.size_bytes(),
              action.input_root_digest().size_bytes());

    ASSERT_EQ(0, action.timeout().seconds());
    ASSERT_EQ(false, action.do_not_cache());
}

TEST(ActionBuilderTest, TestEchoNoInputsNoOutputsWithEnv)
{
    auto actionData = buildAction(
        nullptr, {"/bin/echo", "hello world"}, ".", {}, nullptr, {}, {},
        {std::make_pair("ENV1", "A"), std::make_pair("ENV2", "B")}, 0, false,
        false, "", {}, 0);

    // Check `Command` proto
    const auto command = actionData.d_commandProto;
    const auto commandDigest =
        DigestGenerator::hash(command.SerializeAsString());
    ASSERT_EQ(commandDigest.hash(), actionData.d_commandDigest.hash());
    ASSERT_EQ(commandDigest.size_bytes(),
              actionData.d_commandDigest.size_bytes());

    const auto &args = command.arguments();
    ASSERT_EQ(2, command.arguments_size());
    ASSERT_EQ("/bin/echo", args[0]);
    ASSERT_EQ("hello world", args[1]);

    ASSERT_EQ(0, command.output_paths_size());

    ASSERT_EQ(2, command.environment_variables_size());
    ASSERT_EQ("ENV1", command.environment_variables(0).name());
    ASSERT_EQ("A", command.environment_variables(0).value());
    ASSERT_EQ("ENV2", command.environment_variables(1).name());
    ASSERT_EQ("B", command.environment_variables(1).value());

    ASSERT_TRUE(commandPlatformDeprecated(command).properties().empty());

    // Check `Action` proto
    const auto action = actionData.d_actionProto;
    const auto actionDigest =
        DigestGenerator::hash(action.SerializeAsString());
    ASSERT_EQ(actionDigest.hash(), actionData.d_actionDigest.hash());
    ASSERT_EQ(actionDigest.size_bytes(),
              actionData.d_actionDigest.size_bytes());

    ASSERT_EQ(commandDigest.hash(), action.command_digest().hash());
    ASSERT_EQ(commandDigest.size_bytes(),
              action.command_digest().size_bytes());

    // Empty input root
    NestedDirectory emptyDir;
    const auto emptyDirDigest = emptyDir.to_digest();
    ASSERT_EQ(emptyDirDigest.hash(), action.input_root_digest().hash());
    ASSERT_EQ(emptyDirDigest.size_bytes(),
              action.input_root_digest().size_bytes());

    ASSERT_EQ(0, action.timeout().seconds());
    ASSERT_EQ(false, action.do_not_cache());
}

TEST(ActionBuilderTest, TestEchoNoInputWithOutputs)
{
    auto actionData =
        buildAction(nullptr, {"/bin/echo", "hello world"}, ".", {}, nullptr,
                    {"/path/to/output-1", "/path/to/output-2"}, {}, {}, 0,
                    false, false, "", {}, 0);

    // Check `Command` proto
    const auto command = actionData.d_commandProto;
    const auto commandDigest =
        DigestGenerator::hash(command.SerializeAsString());
    ASSERT_EQ(commandDigest.hash(), actionData.d_commandDigest.hash());
    ASSERT_EQ(commandDigest.size_bytes(),
              actionData.d_commandDigest.size_bytes());

    const auto &args = command.arguments();
    ASSERT_EQ(2, command.arguments_size());
    ASSERT_EQ("/bin/echo", args[0]);
    ASSERT_EQ("hello world", args[1]);

    const auto &outputs = command.output_paths();
    ASSERT_EQ(2, command.output_paths_size());
    ASSERT_EQ("/path/to/output-1", outputs[0]);
    ASSERT_EQ("/path/to/output-2", outputs[1]);

    ASSERT_TRUE(command.environment_variables().empty());
    ASSERT_TRUE(commandPlatformDeprecated(command).properties().empty());

    // Check `Action` proto
    const auto action = actionData.d_actionProto;
    const auto actionDigest =
        DigestGenerator::hash(action.SerializeAsString());
    ASSERT_EQ(actionDigest.hash(), actionData.d_actionDigest.hash());
    ASSERT_EQ(actionDigest.size_bytes(),
              actionData.d_actionDigest.size_bytes());

    ASSERT_EQ(commandDigest.hash(), action.command_digest().hash());
    ASSERT_EQ(commandDigest.size_bytes(),
              action.command_digest().size_bytes());

    // Empty input root
    NestedDirectory emptyDir;
    const auto emptyDirDigest = emptyDir.to_digest();
    ASSERT_EQ(emptyDirDigest.hash(), action.input_root_digest().hash());
    ASSERT_EQ(emptyDirDigest.size_bytes(),
              action.input_root_digest().size_bytes());

    ASSERT_EQ(0, action.timeout().seconds());
    ASSERT_EQ(false, action.do_not_cache());
}

TEST(ActionBuilderTest, TestEchoNoInputsNoOutputsWithPlatform)
{
    auto actionData = buildAction(
        nullptr, {"/bin/echo", "hello world"}, ".", {}, nullptr, {},
        {std::make_pair("PLT1", "A"), std::make_pair("PLT2", "B")}, {}, 0,
        false, false, "", {}, 0);

    // Check `Command` proto
    const auto command = actionData.d_commandProto;
    const auto commandDigest =
        DigestGenerator::hash(command.SerializeAsString());
    ASSERT_EQ(commandDigest.hash(), actionData.d_commandDigest.hash());
    ASSERT_EQ(commandDigest.size_bytes(),
              actionData.d_commandDigest.size_bytes());

    const auto &args = command.arguments();
    ASSERT_EQ(2, command.arguments_size());
    ASSERT_EQ("/bin/echo", args[0]);
    ASSERT_EQ("hello world", args[1]);

    ASSERT_EQ(0, command.output_paths_size());

    ASSERT_TRUE(command.environment_variables().empty());

    ASSERT_EQ(2, commandPlatformDeprecated(command).properties_size());
    ASSERT_EQ("PLT1", commandPlatformDeprecated(command).properties(0).name());
    ASSERT_EQ("A", commandPlatformDeprecated(command).properties(0).value());
    ASSERT_EQ("PLT2", commandPlatformDeprecated(command).properties(1).name());
    ASSERT_EQ("B", commandPlatformDeprecated(command).properties(1).value());

    // Check `Action` proto
    const auto action = actionData.d_actionProto;
    const auto actionDigest =
        DigestGenerator::hash(action.SerializeAsString());
    ASSERT_EQ(actionDigest.hash(), actionData.d_actionDigest.hash());
    ASSERT_EQ(actionDigest.size_bytes(),
              actionData.d_actionDigest.size_bytes());

    ASSERT_EQ(commandDigest.hash(), action.command_digest().hash());
    ASSERT_EQ(commandDigest.size_bytes(),
              action.command_digest().size_bytes());

    // Empty input root
    NestedDirectory emptyDir;
    const auto emptyDirDigest = emptyDir.to_digest();
    ASSERT_EQ(emptyDirDigest.hash(), action.input_root_digest().hash());
    ASSERT_EQ(emptyDirDigest.size_bytes(),
              action.input_root_digest().size_bytes());

    ASSERT_EQ(0, action.timeout().seconds());
    ASSERT_EQ(false, action.do_not_cache());
}

TEST(ActionBuilderTest, TestEchoNoInputsNoOutputsWithTimeout)
{
    auto actionData =
        buildAction(nullptr, {"/bin/echo", "hello world"}, ".", {}, nullptr,
                    {}, {}, {}, 1, false, false, "", {}, 0);

    // Check `Command` proto
    const auto command = actionData.d_commandProto;
    const auto commandDigest =
        DigestGenerator::hash(command.SerializeAsString());
    ASSERT_EQ(commandDigest.hash(), actionData.d_commandDigest.hash());
    ASSERT_EQ(commandDigest.size_bytes(),
              actionData.d_commandDigest.size_bytes());

    const auto &args = command.arguments();
    ASSERT_EQ(2, command.arguments_size());
    ASSERT_EQ("/bin/echo", args[0]);
    ASSERT_EQ("hello world", args[1]);

    ASSERT_EQ(0, command.output_paths_size());

    ASSERT_TRUE(command.environment_variables().empty());
    ASSERT_TRUE(commandPlatformDeprecated(command).properties().empty());

    // Check `Action` proto
    const auto action = actionData.d_actionProto;
    const auto actionDigest =
        DigestGenerator::hash(action.SerializeAsString());
    ASSERT_EQ(actionDigest.hash(), actionData.d_actionDigest.hash());
    ASSERT_EQ(actionDigest.size_bytes(),
              actionData.d_actionDigest.size_bytes());

    ASSERT_EQ(commandDigest.hash(), action.command_digest().hash());
    ASSERT_EQ(commandDigest.size_bytes(),
              action.command_digest().size_bytes());

    // Empty input root
    NestedDirectory emptyDir;
    const auto emptyDirDigest = emptyDir.to_digest();
    ASSERT_EQ(emptyDirDigest.hash(), action.input_root_digest().hash());
    ASSERT_EQ(emptyDirDigest.size_bytes(),
              action.input_root_digest().size_bytes());

    ASSERT_EQ(1, action.timeout().seconds());
    ASSERT_EQ(false, action.do_not_cache());
}

TEST(ActionBuilderTest, TestEchoNoInputsNoOutputsDoNotCache)
{
    auto actionData =
        buildAction(nullptr, {"/bin/echo", "hello world"}, ".", {}, nullptr,
                    {}, {}, {}, 0, true, false, "", {}, 0);

    // Check `Command` proto
    const auto command = actionData.d_commandProto;
    const auto commandDigest =
        DigestGenerator::hash(command.SerializeAsString());
    ASSERT_EQ(commandDigest.hash(), actionData.d_commandDigest.hash());
    ASSERT_EQ(commandDigest.size_bytes(),
              actionData.d_commandDigest.size_bytes());

    const auto &args = command.arguments();
    ASSERT_EQ(2, command.arguments_size());
    ASSERT_EQ("/bin/echo", args[0]);
    ASSERT_EQ("hello world", args[1]);

    ASSERT_EQ(0, command.output_paths_size());

    ASSERT_TRUE(command.environment_variables().empty());
    ASSERT_TRUE(commandPlatformDeprecated(command).properties().empty());

    // Check `Action` proto
    const auto action = actionData.d_actionProto;
    const auto actionDigest =
        DigestGenerator::hash(action.SerializeAsString());
    ASSERT_EQ(actionDigest.hash(), actionData.d_actionDigest.hash());
    ASSERT_EQ(actionDigest.size_bytes(),
              actionData.d_actionDigest.size_bytes());

    ASSERT_EQ(commandDigest.hash(), action.command_digest().hash());
    ASSERT_EQ(commandDigest.size_bytes(),
              action.command_digest().size_bytes());

    // Empty input root
    NestedDirectory emptyDir;
    const auto emptyDirDigest = emptyDir.to_digest();
    ASSERT_EQ(emptyDirDigest.hash(), action.input_root_digest().hash());
    ASSERT_EQ(emptyDirDigest.size_bytes(),
              action.input_root_digest().size_bytes());

    ASSERT_EQ(0, action.timeout().seconds());
    ASSERT_EQ(true, action.do_not_cache());
}

TEST(ActionBuilderTest, TestCatMultipleInputsNoOutputs)
{
    auto actionData =
        buildAction(nullptr, {"/bin/sh", "-c", "'/bin/cat foo.log'"}, ".",
                    {InputPathOption{"./data/input_1"},
                     InputPathOption{"./data/input_2/subdir/foo.log"}},
                    nullptr, {}, {}, {}, 0, true, false, "", {}, 0);

    // Check `Command` proto
    const auto command = actionData.d_commandProto;
    const auto commandDigest =
        DigestGenerator::hash(command.SerializeAsString());
    ASSERT_EQ(commandDigest.hash(), actionData.d_commandDigest.hash());
    ASSERT_EQ(commandDigest.size_bytes(),
              actionData.d_commandDigest.size_bytes());

    const auto &args = command.arguments();
    ASSERT_EQ(3, command.arguments_size());
    ASSERT_EQ("/bin/sh", args[0]);
    ASSERT_EQ("-c", args[1]);
    ASSERT_EQ("'/bin/cat foo.log'", args[2]);

    ASSERT_EQ(0, command.output_paths_size());

    // Check `Action` proto
    const auto action = actionData.d_actionProto;
    const auto actionDigest =
        DigestGenerator::hash(action.SerializeAsString());
    ASSERT_EQ(actionDigest.hash(), actionData.d_actionDigest.hash());
    ASSERT_EQ(actionDigest.size_bytes(),
              actionData.d_actionDigest.size_bytes());

    ASSERT_EQ(commandDigest.hash(), action.command_digest().hash());
    ASSERT_EQ(commandDigest.size_bytes(),
              action.command_digest().size_bytes());
}

TEST(ActionBuilderTest, TestCatMultipleInputsWithMappingsNoOutputs)
{
    auto actionData = buildAction(
        nullptr, {"/bin/sh", "-c", "'/bin/cat foo.log'"}, ".",
        {InputPathOption{"./data/input_1", "."},
         InputPathOption{"./data/input_2/subdir/foo.log", "subdir/foo.log"}},
        nullptr, {}, {}, {}, 0, true, false, "", {}, 0);

    // Check `Command` proto
    const auto command = actionData.d_commandProto;
    const auto commandDigest =
        DigestGenerator::hash(command.SerializeAsString());
    ASSERT_EQ(commandDigest.hash(), actionData.d_commandDigest.hash());
    ASSERT_EQ(commandDigest.size_bytes(),
              actionData.d_commandDigest.size_bytes());

    const auto &args = command.arguments();
    ASSERT_EQ(3, command.arguments_size());
    ASSERT_EQ("/bin/sh", args[0]);
    ASSERT_EQ("-c", args[1]);
    ASSERT_EQ("'/bin/cat foo.log'", args[2]);

    ASSERT_EQ(0, command.output_paths_size());

    // Check `Action` proto
    const auto action = actionData.d_actionProto;
    const auto actionDigest =
        DigestGenerator::hash(action.SerializeAsString());
    ASSERT_EQ(actionDigest.hash(), actionData.d_actionDigest.hash());
    ASSERT_EQ(actionDigest.size_bytes(),
              actionData.d_actionDigest.size_bytes());

    ASSERT_EQ(commandDigest.hash(), action.command_digest().hash());
    ASSERT_EQ(commandDigest.size_bytes(),
              action.command_digest().size_bytes());
}

TEST(ActionBuilderTest, TestInputRootDigestNoOutputs)
{
    Directory inputRoot;

    auto actionData =
        buildAction(nullptr, {"/bin/sh", "-c", "'/bin/cat foo.log'"}, ".", {},
                    std::make_shared<Digest>(
                        DigestGenerator::hash(inputRoot.SerializeAsString())),
                    {}, {}, {}, 0, true, false, "", {}, 0);

    ASSERT_TRUE(actionData.d_inputDigestsToPaths.empty());
    ASSERT_TRUE(actionData.d_inputDigestsToSerializedProtos.empty());
    // Check `Command` proto
    const auto command = actionData.d_commandProto;
    const auto commandDigest =
        DigestGenerator::hash(command.SerializeAsString());
    ASSERT_EQ(commandDigest.hash(), actionData.d_commandDigest.hash());
    ASSERT_EQ(commandDigest.size_bytes(),
              actionData.d_commandDigest.size_bytes());

    const auto &args = command.arguments();
    ASSERT_EQ(3, command.arguments_size());
    ASSERT_EQ("/bin/sh", args[0]);
    ASSERT_EQ("-c", args[1]);
    ASSERT_EQ("'/bin/cat foo.log'", args[2]);

    ASSERT_EQ(0, command.output_paths_size());

    const auto expectedDigest =
        DigestGenerator::hash(inputRoot.SerializeAsString());

    // Check `Action` proto
    const auto action = actionData.d_actionProto;
    const auto actionDigest =
        DigestGenerator::hash(action.SerializeAsString());
    ASSERT_EQ(action.input_root_digest().hash(), expectedDigest.hash());
    ASSERT_EQ(action.input_root_digest().size_bytes(),
              expectedDigest.size_bytes());
    ASSERT_EQ(actionDigest.hash(), actionData.d_actionDigest.hash());
    ASSERT_EQ(actionDigest.size_bytes(),
              actionData.d_actionDigest.size_bytes());

    ASSERT_EQ(commandDigest.hash(), action.command_digest().hash());
    ASSERT_EQ(commandDigest.size_bytes(),
              action.command_digest().size_bytes());
}

TEST(ActionBuilderTest, TestMultipleInputsAndInputRootDigest)
{
    // This test checks that merging an input from CAS and local directories
    // works as expected by simulating CAS and checking the contents of merged
    // directory.

    //
    // Create a directory structure in fake CAS:
    // ./
    // ./src
    // ./src/build.sh*
    //

    Directory src_directory;
    FileNode *buildNode = src_directory.add_files();
    buildNode->set_name("build.sh");
    buildNode->set_is_executable(true);
    buildNode->mutable_digest()->CopyFrom(DigestGenerator::hash("exit 0\n"));
    const auto src_directory_digest = DigestGenerator::hash(src_directory);

    Directory root_directory;
    DirectoryNode *srcNode = root_directory.add_directories();
    srcNode->set_name("src");
    srcNode->mutable_digest()->CopyFrom(src_directory_digest);

    //
    // Create a mock client that returns the directory structure created above
    // when requsted
    //

    auto grpcClient = std::make_shared<GrpcClient>();
    auto bytestreamClient =
        std::make_shared<google::bytestream::MockByteStreamStub>();
    auto casClient = std::make_shared<MockContentAddressableStorageStub>();
    auto localCasClient =
        std::make_shared<MockLocalContentAddressableStorageStub>();
    auto capabilitiesClient = std::make_shared<MockCapabilitiesStub>();

    // Capabilities request
    CacheCapabilities *cacheCapabilities = new CacheCapabilities();
    cacheCapabilities->set_max_batch_total_size_bytes(64000000);
    cacheCapabilities->add_digest_functions(
        DigestFunction_Value::DigestFunction_Value_SHA256);
    ServerCapabilities serverCapabilities;
    serverCapabilities.set_allocated_cache_capabilities(cacheCapabilities);

    EXPECT_CALL(*capabilitiesClient, GetCapabilities(_, _, _))
        .WillOnce(DoAll(SetArgPointee<2>(serverCapabilities),
                        Return(grpc::Status::OK)));

    // GetTree request
    grpc::testing::MockClientReader<
        typename build::bazel::remote::execution::v2::GetTreeResponse>
        *gettreereader = new grpc::testing::MockClientReader<
            typename build::bazel::remote::execution::v2::GetTreeResponse>();

    EXPECT_CALL(*casClient, GetTreeRaw(_, _)).WillOnce(Return(gettreereader));

    GetTreeResponse response;
    response.add_directories()->CopyFrom(root_directory);
    response.add_directories()->CopyFrom(src_directory);

    EXPECT_CALL(*gettreereader, Read(_))
        .WillOnce(DoAll(SetArgPointee<0>(response), Return(true)))
        .WillOnce(Return(false));

    EXPECT_CALL(*gettreereader, Finish()).WillOnce(Return(grpc::Status::OK));

    // Fake client
    CASClient client(grpcClient);
    client.init(bytestreamClient, casClient, localCasClient,
                capabilitiesClient);

    //
    // Build the action using the mock client and local directories
    //

    auto actionData = buildAction(
        std::make_shared<CASClient>(client),
        {"/bin/sh", "-c", "'/bin/cat foo.log'"}, ".",
        {InputPathOption{"./data/input_1", "."},
         InputPathOption{"./data/input_2/subdir/foo.log", "subdir/foo.log"}},
        std::make_shared<Digest>(
            DigestGenerator::hash(root_directory.SerializeAsString())),
        {}, {}, {}, 0, true, false, "", {}, 0);

    //
    // Check the result
    //

    const auto action = actionData.d_actionProto;

    // generated input root
    const auto &actionInputRootDigest = action.input_root_digest();

    // Find the root dir in the action data
    auto actionRootDirSerialized =
        actionData.d_inputDigestsToSerializedProtos.at(actionInputRootDigest);
    ASSERT_NE(actionRootDirSerialized, "");

    // Unpack the root dir and check its contents, it should contain file
    // "hello.txt" and directories "src" and "subdir".
    Directory actionRootDir;
    actionRootDir.ParseFromString(actionRootDirSerialized);

    ASSERT_EQ(actionRootDir.files_size(), 1);
    ASSERT_EQ(actionRootDir.files(0).name(), "hello.txt");

    ASSERT_EQ(actionRootDir.directories_size(), 2);
    const DirectoryNode d0 = actionRootDir.directories(0);
    const DirectoryNode d1 = actionRootDir.directories(1);

    const Digest actionSrcDirDigest = (d0.name() == "src" ? d0 : d1).digest();

    const Digest actionSubdirDirDigest =
        (d0.name() == "subdir" ? d0 : d1).digest();
    // subdir directory should be present
    auto actionSubdirDirSerialized =
        actionData.d_inputDigestsToSerializedProtos.at(actionSubdirDirDigest);
    ASSERT_NE(actionSubdirDirSerialized, "");

    // subdir should contain one file "foo.log"
    Directory actionSubdirDir;
    actionSubdirDir.ParseFromString(actionSubdirDirSerialized);
    ASSERT_EQ(actionSubdirDir.files_size(), 1);
    ASSERT_EQ(actionSubdirDir.files(0).name(), "foo.log");
}

TEST(ActionBuilderTest, TestSetSalt)
{
    auto actionData =
        buildAction(nullptr, {"/bin/echo", "hello world"}, ".", {}, nullptr,
                    {}, {}, {}, 0, true, false, "1234", {}, 0);

    const auto action = actionData.d_actionProto;
    ASSERT_EQ("1234", action.salt());
    ASSERT_NE("", action.salt());
}

TEST(ActionBuilderTest, TestNodeProperties)
{
    auto actionData =
        buildAction(nullptr, {"/bin/echo", "hello world"}, ".", {}, nullptr,
                    {}, {}, {}, 0, true, false, "", {"mtime", "unix_mode"}, 0);

    // Check `Command` proto
    const auto command = actionData.d_commandProto;
    const auto commandDigest =
        DigestGenerator::hash(command.SerializeAsString());
    ASSERT_EQ(commandDigest.hash(), actionData.d_commandDigest.hash());
    ASSERT_EQ(commandDigest.size_bytes(),
              actionData.d_commandDigest.size_bytes());

    const auto &properties = command.output_node_properties();
    ASSERT_EQ(2, command.output_node_properties_size());
    ASSERT_EQ("mtime", properties[0]);
    ASSERT_EQ("unix_mode", properties[1]);
}
