/*
 * Copyright (C) 2022 The Android Open Source Project
 *
 * 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 "artd.h"

#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

#include <algorithm>
#include <chrono>
#include <condition_variable>
#include <csignal>
#include <filesystem>
#include <functional>
#include <memory>
#include <mutex>
#include <optional>
#include <string>
#include <thread>
#include <type_traits>
#include <unordered_set>
#include <utility>
#include <vector>

#include "aidl/com/android/server/art/ArtConstants.h"
#include "aidl/com/android/server/art/BnArtd.h"
#include "android-base/collections.h"
#include "android-base/errors.h"
#include "android-base/file.h"
#include "android-base/logging.h"
#include "android-base/parseint.h"
#include "android-base/result.h"
#include "android-base/scopeguard.h"
#include "android-base/strings.h"
#include "android/binder_auto_utils.h"
#include "android/binder_status.h"
#include "base/array_ref.h"
#include "base/common_art_test.h"
#include "exec_utils.h"
#include "fmt/format.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include "oat_file.h"
#include "path_utils.h"
#include "profman/profman_result.h"
#include "testing.h"
#include "tools/system_properties.h"

namespace art {
namespace artd {
namespace {

using ::aidl::com::android::server::art::ArtConstants;
using ::aidl::com::android::server::art::ArtdDexoptResult;
using ::aidl::com::android::server::art::ArtifactsPath;
using ::aidl::com::android::server::art::DexMetadataPath;
using ::aidl::com::android::server::art::DexoptOptions;
using ::aidl::com::android::server::art::FileVisibility;
using ::aidl::com::android::server::art::FsPermission;
using ::aidl::com::android::server::art::GetDexoptStatusResult;
using ::aidl::com::android::server::art::IArtdCancellationSignal;
using ::aidl::com::android::server::art::OutputArtifacts;
using ::aidl::com::android::server::art::OutputProfile;
using ::aidl::com::android::server::art::PriorityClass;
using ::aidl::com::android::server::art::ProfilePath;
using ::aidl::com::android::server::art::VdexPath;
using ::android::base::Append;
using ::android::base::Error;
using ::android::base::make_scope_guard;
using ::android::base::ParseInt;
using ::android::base::ReadFdToString;
using ::android::base::ReadFileToString;
using ::android::base::Result;
using ::android::base::ScopeGuard;
using ::android::base::Split;
using ::android::base::WriteStringToFd;
using ::android::base::WriteStringToFile;
using ::testing::_;
using ::testing::AllOf;
using ::testing::AnyNumber;
using ::testing::AnyOf;
using ::testing::Contains;
using ::testing::ContainsRegex;
using ::testing::DoAll;
using ::testing::ElementsAre;
using ::testing::Field;
using ::testing::HasSubstr;
using ::testing::IsEmpty;
using ::testing::Matcher;
using ::testing::MockFunction;
using ::testing::Not;
using ::testing::Property;
using ::testing::ResultOf;
using ::testing::Return;
using ::testing::SetArgPointee;
using ::testing::UnorderedElementsAreArray;
using ::testing::WithArg;

using PrimaryCurProfilePath = ProfilePath::PrimaryCurProfilePath;
using PrimaryRefProfilePath = ProfilePath::PrimaryRefProfilePath;
using TmpProfilePath = ProfilePath::TmpProfilePath;

using ::fmt::literals::operator""_format;  // NOLINT

ScopeGuard<std::function<void()>> ScopedSetLogger(android::base::LogFunction&& logger) {
  android::base::LogFunction old_logger = android::base::SetLogger(std::move(logger));
  return make_scope_guard([old_logger = std::move(old_logger)]() mutable {
    android::base::SetLogger(std::move(old_logger));
  });
}

void CheckContent(const std::string& path, const std::string& expected_content) {
  std::string actual_content;
  ASSERT_TRUE(ReadFileToString(path, &actual_content));
  EXPECT_EQ(actual_content, expected_content);
}

void CheckOtherReadable(const std::string& path, bool expected_value) {
  EXPECT_EQ((std::filesystem::status(path).permissions() & std::filesystem::perms::others_read) !=
                std::filesystem::perms::none,
            expected_value);
}

Result<std::vector<std::string>> GetFlagValues(ArrayRef<const std::string> args,
                                               std::string_view flag) {
  std::vector<std::string> values;
  for (const std::string& arg : args) {
    std::string_view value(arg);
    if (android::base::ConsumePrefix(&value, flag)) {
      values.emplace_back(value);
    }
  }
  if (values.empty()) {
    return Errorf("Flag '{}' not found", flag);
  }
  return values;
}

Result<std::string> GetFlagValue(ArrayRef<const std::string> args, std::string_view flag) {
  std::vector<std::string> flag_values = OR_RETURN(GetFlagValues(args, flag));
  if (flag_values.size() > 1) {
    return Errorf("Duplicate flag '{}'", flag);
  }
  return flag_values[0];
}

void WriteToFdFlagImpl(const std::vector<std::string>& args,
                       std::string_view flag,
                       std::string_view content,
                       bool assume_empty) {
  std::string value = OR_FAIL(GetFlagValue(ArrayRef<const std::string>(args), flag));
  ASSERT_NE(value, "");
  int fd;
  ASSERT_TRUE(ParseInt(value, &fd));
  if (assume_empty) {
    ASSERT_EQ(lseek(fd, /*offset=*/0, SEEK_CUR), 0);
  } else {
    ASSERT_EQ(ftruncate(fd, /*length=*/0), 0);
    ASSERT_EQ(lseek(fd, /*offset=*/0, SEEK_SET), 0);
  }
  ASSERT_TRUE(WriteStringToFd(content, fd));
}

// Writes `content` to the FD specified by the `flag`.
ACTION_P(WriteToFdFlag, flag, content) {
  WriteToFdFlagImpl(arg0, flag, content, /*assume_empty=*/true);
}

// Clears any existing content and writes `content` to the FD specified by the `flag`.
ACTION_P(ClearAndWriteToFdFlag, flag, content) {
  WriteToFdFlagImpl(arg0, flag, content, /*assume_empty=*/false);
}

// Matches a flag that starts with `flag` and whose value matches `matcher`.
MATCHER_P2(Flag, flag, matcher, "") {
  std::string_view value(arg);
  if (!android::base::ConsumePrefix(&value, flag)) {
    return false;
  }
  return ExplainMatchResult(matcher, std::string(value), result_listener);
}

// Matches a flag that starts with `flag` and whose value is a colon-separated list that matches
// `matcher`. The matcher acts on an `std::vector<std::string>` of the split list argument.
MATCHER_P2(ListFlag, flag, matcher, "") {
  return ExplainMatchResult(
      Flag(flag, ResultOf(std::bind(Split, std::placeholders::_1, ":"), matcher)),
      arg,
      result_listener);
}

// Matches an FD of a file whose path matches `matcher`.
MATCHER_P(FdOf, matcher, "") {
  std::string proc_path = "/proc/self/fd/{}"_format(arg);
  char path[PATH_MAX];
  ssize_t len = readlink(proc_path.c_str(), path, sizeof(path));
  if (len < 0) {
    return false;
  }
  return ExplainMatchResult(matcher, std::string(path, static_cast<size_t>(len)), result_listener);
}

// Matches an FD of a file whose content matches `matcher`.
MATCHER_P(FdHasContent, matcher, "") {
  int fd;
  if (!ParseInt(arg, &fd)) {
    return false;
  }
  std::string actual_content;
  if (!ReadFdToString(fd, &actual_content)) {
    return false;
  }
  return ExplainMatchResult(matcher, actual_content, result_listener);
}

template <typename T, typename U>
Result<std::pair<ArrayRef<const T>, ArrayRef<const T>>> SplitBy(const std::vector<T>& list,
                                                                const U& separator) {
  auto it = std::find(list.begin(), list.end(), separator);
  if (it == list.end()) {
    return Errorf("'{}' not found", separator);
  }
  size_t pos = it - list.begin();
  return std::make_pair(ArrayRef<const T>(list).SubArray(0, pos),
                        ArrayRef<const T>(list).SubArray(pos + 1));
}

// Matches a container that, when split by `separator`, the first part matches `head_matcher`, and
// the second part matches `tail_matcher`.
MATCHER_P3(WhenSplitBy, separator, head_matcher, tail_matcher, "") {
  auto [head, tail] = OR_MISMATCH(SplitBy(arg, separator));
  return ExplainMatchResult(head_matcher, head, result_listener) &&
         ExplainMatchResult(tail_matcher, tail, result_listener);
}

MATCHER_P(HasKeepFdsForImpl, fd_flags, "") {
  auto [head, tail] = OR_MISMATCH(SplitBy(arg, "--"));
  std::string keep_fds_value = OR_MISMATCH(GetFlagValue(head, "--keep-fds="));
  std::vector<std::string> keep_fds = Split(keep_fds_value, ":");
  std::vector<std::string> fd_flag_values;
  for (std::string_view fd_flag : fd_flags) {
    for (const std::string& fd_flag_value : OR_MISMATCH(GetFlagValues(tail, fd_flag))) {
      for (std::string& fd : Split(fd_flag_value, ":")) {
        fd_flag_values.push_back(std::move(fd));
      }
    }
  }
  return ExplainMatchResult(UnorderedElementsAreArray(fd_flag_values), keep_fds, result_listener);
}

// Matches an argument list that has the "--keep-fds=" flag before "--", whose value is a
// semicolon-separated list that contains exactly the values of the given flags after "--".
//
// E.g., if the flags after "--" are "--foo=1", "--bar=2:3", "--baz=4", "--baz=5", and the matcher
// is `HasKeepFdsFor("--foo=", "--bar=", "--baz=")`, then it requires the "--keep-fds=" flag before
// "--" to contain exactly 1, 2, 3, 4, and 5.
template <typename... Args>
auto HasKeepFdsFor(Args&&... args) {
  std::vector<std::string_view> fd_flags;
  Append(fd_flags, std::forward<Args>(args)...);
  return HasKeepFdsForImpl(fd_flags);
}

class MockSystemProperties : public tools::SystemProperties {
 public:
  MOCK_METHOD(std::string, GetProperty, (const std::string& key), (const, override));
};

class MockExecUtils : public ExecUtils {
 public:
  // A workaround to avoid MOCK_METHOD on a method with an `std::string*` parameter, which will lead
  // to a conflict between gmock and android-base/logging.h (b/132668253).
  ExecResult ExecAndReturnResult(const std::vector<std::string>& arg_vector,
                                 int,
                                 const ExecCallbacks& callbacks,
                                 ProcessStat* stat,
                                 std::string*) const override {
    Result<int> code = DoExecAndReturnCode(arg_vector, callbacks, stat);
    if (code.ok()) {
      return {.status = ExecResult::kExited, .exit_code = code.value()};
    }
    return {.status = ExecResult::kUnknown};
  }

  MOCK_METHOD(Result<int>,
              DoExecAndReturnCode,
              (const std::vector<std::string>& arg_vector,
               const ExecCallbacks& callbacks,
               ProcessStat* stat),
              (const));
};

class ArtdTest : public CommonArtTest {
 protected:
  void SetUp() override {
    CommonArtTest::SetUp();
    auto mock_props = std::make_unique<MockSystemProperties>();
    mock_props_ = mock_props.get();
    EXPECT_CALL(*mock_props_, GetProperty).Times(AnyNumber()).WillRepeatedly(Return(""));
    auto mock_exec_utils = std::make_unique<MockExecUtils>();
    mock_exec_utils_ = mock_exec_utils.get();
    artd_ = ndk::SharedRefBase::make<Artd>(std::move(mock_props),
                                           std::move(mock_exec_utils),
                                           mock_kill_.AsStdFunction(),
                                           mock_fstat_.AsStdFunction());
    scratch_dir_ = std::make_unique<ScratchDir>();
    scratch_path_ = scratch_dir_->GetPath();
    // Remove the trailing '/';
    scratch_path_.resize(scratch_path_.length() - 1);

    ON_CALL(mock_fstat_, Call).WillByDefault(fstat);

    // Use an arbitrary existing directory as ART root.
    art_root_ = scratch_path_ + "/com.android.art";
    std::filesystem::create_directories(art_root_);
    setenv("ANDROID_ART_ROOT", art_root_.c_str(), /*overwrite=*/1);

    // Use an arbitrary existing directory as Android data.
    android_data_ = scratch_path_ + "/data";
    std::filesystem::create_directories(android_data_);
    setenv("ANDROID_DATA", android_data_.c_str(), /*overwrite=*/1);

    // Use an arbitrary existing directory as Android expand.
    android_expand_ = scratch_path_ + "/mnt/expand";
    std::filesystem::create_directories(android_expand_);
    setenv("ANDROID_EXPAND", android_expand_.c_str(), /*overwrite=*/1);

    dex_file_ = scratch_path_ + "/a/b.apk";
    isa_ = "arm64";
    artifacts_path_ = ArtifactsPath{
        .dexPath = dex_file_,
        .isa = isa_,
        .isInDalvikCache = false,
    };
    struct stat st;
    ASSERT_EQ(stat(scratch_path_.c_str(), &st), 0);
    output_artifacts_ = OutputArtifacts{
        .artifactsPath = artifacts_path_,
        .permissionSettings =
            OutputArtifacts::PermissionSettings{
                .dirFsPermission =
                    FsPermission{
                        .uid = static_cast<int32_t>(st.st_uid),
                        .gid = static_cast<int32_t>(st.st_gid),
                        .isOtherReadable = true,
                        .isOtherExecutable = true,
                    },
                .fileFsPermission =
                    FsPermission{
                        .uid = static_cast<int32_t>(st.st_uid),
                        .gid = static_cast<int32_t>(st.st_gid),
                        .isOtherReadable = true,
                    },
            },
    };
    clc_1_ = GetTestDexFileName("Main");
    clc_2_ = GetTestDexFileName("Nested");
    class_loader_context_ = "PCL[{}:{}]"_format(clc_1_, clc_2_);
    compiler_filter_ = "speed";
    TmpProfilePath tmp_profile_path{
        .finalPath =
            PrimaryRefProfilePath{.packageName = "com.android.foo", .profileName = "primary"},
        .id = "12345"};
    profile_path_ = tmp_profile_path;
    vdex_path_ = artifacts_path_;
    dm_path_ = DexMetadataPath{.dexPath = dex_file_};
    std::filesystem::create_directories(
        std::filesystem::path(OR_FATAL(BuildFinalProfilePath(tmp_profile_path))).parent_path());
  }

  void TearDown() override {
    scratch_dir_.reset();
    CommonArtTest::TearDown();
  }

  void RunDexopt(binder_exception_t expected_status = EX_NONE,
                 Matcher<ArtdDexoptResult> aidl_return_matcher = Field(&ArtdDexoptResult::cancelled,
                                                                       false),
                 std::shared_ptr<IArtdCancellationSignal> cancellation_signal = nullptr) {
    RunDexopt(Property(&ndk::ScopedAStatus::getExceptionCode, expected_status),
              std::move(aidl_return_matcher),
              cancellation_signal);
  }

  void RunDexopt(Matcher<ndk::ScopedAStatus> status_matcher,
                 Matcher<ArtdDexoptResult> aidl_return_matcher = Field(&ArtdDexoptResult::cancelled,
                                                                       false),
                 std::shared_ptr<IArtdCancellationSignal> cancellation_signal = nullptr) {
    InitFilesBeforeDexopt();
    if (cancellation_signal == nullptr) {
      ASSERT_TRUE(artd_->createCancellationSignal(&cancellation_signal).isOk());
    }
    ArtdDexoptResult aidl_return;
    ndk::ScopedAStatus status = artd_->dexopt(output_artifacts_,
                                              dex_file_,
                                              isa_,
                                              class_loader_context_,
                                              compiler_filter_,
                                              profile_path_,
                                              vdex_path_,
                                              dm_path_,
                                              priority_class_,
                                              dexopt_options_,
                                              cancellation_signal,
                                              &aidl_return);
    ASSERT_THAT(status, std::move(status_matcher)) << status.getMessage();
    if (status.isOk()) {
      ASSERT_THAT(aidl_return, std::move(aidl_return_matcher));
    }
  }

  void CreateFile(const std::string& filename, const std::string& content = "") {
    std::filesystem::path path(filename);
    std::filesystem::create_directories(path.parent_path());
    ASSERT_TRUE(WriteStringToFile(content, filename));
  }

  std::shared_ptr<Artd> artd_;
  std::unique_ptr<ScratchDir> scratch_dir_;
  std::string scratch_path_;
  std::string art_root_;
  std::string android_data_;
  std::string android_expand_;
  MockFunction<android::base::LogFunction> mock_logger_;
  ScopedUnsetEnvironmentVariable art_root_env_ = ScopedUnsetEnvironmentVariable("ANDROID_ART_ROOT");
  ScopedUnsetEnvironmentVariable android_data_env_ = ScopedUnsetEnvironmentVariable("ANDROID_DATA");
  ScopedUnsetEnvironmentVariable android_expand_env_ =
      ScopedUnsetEnvironmentVariable("ANDROID_EXPAND");
  MockSystemProperties* mock_props_;
  MockExecUtils* mock_exec_utils_;
  MockFunction<int(pid_t, int)> mock_kill_;
  MockFunction<int(int, struct stat*)> mock_fstat_;

  std::string dex_file_;
  std::string isa_;
  ArtifactsPath artifacts_path_;
  OutputArtifacts output_artifacts_;
  std::string clc_1_;
  std::string clc_2_;
  std::optional<std::string> class_loader_context_;
  std::string compiler_filter_;
  std::optional<VdexPath> vdex_path_;
  std::optional<DexMetadataPath> dm_path_;
  PriorityClass priority_class_ = PriorityClass::BACKGROUND;
  DexoptOptions dexopt_options_;
  std::optional<ProfilePath> profile_path_;
  bool dex_file_other_readable_ = true;
  bool profile_other_readable_ = true;

 private:
  void InitFilesBeforeDexopt() {
    // Required files.
    CreateFile(dex_file_);
    std::filesystem::permissions(dex_file_,
                                 std::filesystem::perms::others_read,
                                 dex_file_other_readable_ ? std::filesystem::perm_options::add :
                                                            std::filesystem::perm_options::remove);

    // Optional files.
    if (vdex_path_.has_value()) {
      CreateFile(OR_FATAL(BuildVdexPath(vdex_path_.value())), "old_vdex");
    }
    if (dm_path_.has_value()) {
      CreateFile(OR_FATAL(BuildDexMetadataPath(dm_path_.value())));
    }
    if (profile_path_.has_value()) {
      std::string path = OR_FATAL(BuildProfileOrDmPath(profile_path_.value()));
      CreateFile(path);
      std::filesystem::permissions(path,
                                   std::filesystem::perms::others_read,
                                   profile_other_readable_ ? std::filesystem::perm_options::add :
                                                             std::filesystem::perm_options::remove);
    }

    // Files to be replaced.
    std::string oat_path = OR_FATAL(BuildOatPath(artifacts_path_));
    CreateFile(oat_path, "old_oat");
    CreateFile(OatPathToVdexPath(oat_path), "old_vdex");
    CreateFile(OatPathToArtPath(oat_path), "old_art");
  }
};

TEST_F(ArtdTest, ConstantsAreInSync) { EXPECT_EQ(ArtConstants::REASON_VDEX, kReasonVdex); }

TEST_F(ArtdTest, isAlive) {
  bool result = false;
  artd_->isAlive(&result);
  EXPECT_TRUE(result);
}

TEST_F(ArtdTest, deleteArtifacts) {
  std::string oat_dir = scratch_path_ + "/a/oat/arm64";
  std::filesystem::create_directories(oat_dir);
  ASSERT_TRUE(WriteStringToFile("abcd", oat_dir + "/b.odex"));  // 4 bytes.
  ASSERT_TRUE(WriteStringToFile("ab", oat_dir + "/b.vdex"));    // 2 bytes.
  ASSERT_TRUE(WriteStringToFile("a", oat_dir + "/b.art"));      // 1 byte.

  int64_t result = -1;
  EXPECT_TRUE(artd_->deleteArtifacts(artifacts_path_, &result).isOk());
  EXPECT_EQ(result, 4 + 2 + 1);

  EXPECT_FALSE(std::filesystem::exists(oat_dir + "/b.odex"));
  EXPECT_FALSE(std::filesystem::exists(oat_dir + "/b.vdex"));
  EXPECT_FALSE(std::filesystem::exists(oat_dir + "/b.art"));
}

TEST_F(ArtdTest, deleteArtifactsMissingFile) {
  // Missing VDEX file.
  std::string oat_dir = android_data_ + "/dalvik-cache/arm64";
  std::filesystem::create_directories(oat_dir);
  ASSERT_TRUE(WriteStringToFile("abcd", oat_dir + "/a@b.apk@classes.dex"));  // 4 bytes.
  ASSERT_TRUE(WriteStringToFile("a", oat_dir + "/a@b.apk@classes.art"));     // 1 byte.

  auto scoped_set_logger = ScopedSetLogger(mock_logger_.AsStdFunction());
  EXPECT_CALL(mock_logger_, Call(_, _, _, _, _, HasSubstr("Failed to get the file size"))).Times(0);

  int64_t result = -1;
  EXPECT_TRUE(artd_
                  ->deleteArtifacts(
                      ArtifactsPath{
                          .dexPath = "/a/b.apk",
                          .isa = "arm64",
                          .isInDalvikCache = true,
                      },
                      &result)
                  .isOk());
  EXPECT_EQ(result, 4 + 1);

  EXPECT_FALSE(std::filesystem::exists(oat_dir + "/a@b.apk@classes.dex"));
  EXPECT_FALSE(std::filesystem::exists(oat_dir + "/a@b.apk@classes.art"));
}

TEST_F(ArtdTest, deleteArtifactsNoFile) {
  auto scoped_set_logger = ScopedSetLogger(mock_logger_.AsStdFunction());
  EXPECT_CALL(mock_logger_, Call(_, _, _, _, _, HasSubstr("Failed to get the file size"))).Times(0);

  int64_t result = -1;
  EXPECT_TRUE(artd_->deleteArtifacts(artifacts_path_, &result).isOk());
  EXPECT_EQ(result, 0);
}

TEST_F(ArtdTest, deleteArtifactsPermissionDenied) {
  std::string oat_dir = scratch_path_ + "/a/oat/arm64";
  std::filesystem::create_directories(oat_dir);
  ASSERT_TRUE(WriteStringToFile("abcd", oat_dir + "/b.odex"));  // 4 bytes.
  ASSERT_TRUE(WriteStringToFile("ab", oat_dir + "/b.vdex"));    // 2 bytes.
  ASSERT_TRUE(WriteStringToFile("a", oat_dir + "/b.art"));      // 1 byte.

  auto scoped_set_logger = ScopedSetLogger(mock_logger_.AsStdFunction());
  EXPECT_CALL(mock_logger_, Call(_, _, _, _, _, HasSubstr("Failed to get the file size"))).Times(3);

  auto scoped_inaccessible = ScopedInaccessible(oat_dir);
  auto scoped_unroot = ScopedUnroot();

  int64_t result = -1;
  EXPECT_TRUE(artd_->deleteArtifacts(artifacts_path_, &result).isOk());
  EXPECT_EQ(result, 0);
}

TEST_F(ArtdTest, deleteArtifactsFileIsDir) {
  // VDEX file is a directory.
  std::string oat_dir = scratch_path_ + "/a/oat/arm64";
  std::filesystem::create_directories(oat_dir);
  std::filesystem::create_directories(oat_dir + "/b.vdex");
  ASSERT_TRUE(WriteStringToFile("abcd", oat_dir + "/b.odex"));  // 4 bytes.
  ASSERT_TRUE(WriteStringToFile("a", oat_dir + "/b.art"));      // 1 byte.

  auto scoped_set_logger = ScopedSetLogger(mock_logger_.AsStdFunction());
  EXPECT_CALL(mock_logger_,
              Call(_, _, _, _, _, ContainsRegex(R"re(Failed to get the file size.*b\.vdex)re")))
      .Times(1);

  int64_t result = -1;
  EXPECT_TRUE(artd_->deleteArtifacts(artifacts_path_, &result).isOk());
  EXPECT_EQ(result, 4 + 1);

  // The directory is kept because getting the file size failed.
  EXPECT_FALSE(std::filesystem::exists(oat_dir + "/b.odex"));
  EXPECT_TRUE(std::filesystem::exists(oat_dir + "/b.vdex"));
  EXPECT_FALSE(std::filesystem::exists(oat_dir + "/b.art"));
}

TEST_F(ArtdTest, dexopt) {
  dexopt_options_.generateAppImage = true;

  EXPECT_CALL(
      *mock_exec_utils_,
      DoExecAndReturnCode(
          AllOf(WhenSplitBy(
                    "--",
                    AllOf(Contains(art_root_ + "/bin/art_exec"), Contains("--drop-capabilities")),
                    AllOf(Contains(art_root_ + "/bin/dex2oat32"),
                          Contains(Flag("--zip-fd=", FdOf(dex_file_))),
                          Contains(Flag("--zip-location=", dex_file_)),
                          Contains(Flag("--oat-location=", scratch_path_ + "/a/oat/arm64/b.odex")),
                          Contains(Flag("--instruction-set=", "arm64")),
                          Contains(Flag("--compiler-filter=", "speed")),
                          Contains(Flag(
                              "--profile-file-fd=",
                              FdOf(android_data_ +
                                   "/misc/profiles/ref/com.android.foo/primary.prof.12345.tmp"))),
                          Contains(Flag("--input-vdex-fd=",
                                        FdOf(scratch_path_ + "/a/oat/arm64/b.vdex"))),
                          Contains(Flag("--dm-fd=", FdOf(scratch_path_ + "/a/b.dm"))))),
                HasKeepFdsFor("--zip-fd=",
                              "--profile-file-fd=",
                              "--input-vdex-fd=",
                              "--dm-fd=",
                              "--oat-fd=",
                              "--output-vdex-fd=",
                              "--app-image-fd=",
                              "--class-loader-context-fds=",
                              "--swap-fd=")),
          _,
          _))
      .WillOnce(DoAll(WithArg<0>(WriteToFdFlag("--oat-fd=", "oat")),
                      WithArg<0>(WriteToFdFlag("--output-vdex-fd=", "vdex")),
                      WithArg<0>(WriteToFdFlag("--app-image-fd=", "art")),
                      SetArgPointee<2>(ProcessStat{.wall_time_ms = 100, .cpu_time_ms = 400}),
                      Return(0)));
  RunDexopt(
      EX_NONE,
      AllOf(Field(&ArtdDexoptResult::cancelled, false),
            Field(&ArtdDexoptResult::wallTimeMs, 100),
            Field(&ArtdDexoptResult::cpuTimeMs, 400),
            Field(&ArtdDexoptResult::sizeBytes, strlen("art") + strlen("oat") + strlen("vdex")),
            Field(&ArtdDexoptResult::sizeBeforeBytes,
                  strlen("old_art") + strlen("old_oat") + strlen("old_vdex"))));

  CheckContent(scratch_path_ + "/a/oat/arm64/b.odex", "oat");
  CheckContent(scratch_path_ + "/a/oat/arm64/b.vdex", "vdex");
  CheckContent(scratch_path_ + "/a/oat/arm64/b.art", "art");
  CheckOtherReadable(scratch_path_ + "/a/oat/arm64/b.odex", true);
  CheckOtherReadable(scratch_path_ + "/a/oat/arm64/b.vdex", true);
  CheckOtherReadable(scratch_path_ + "/a/oat/arm64/b.art", true);
}

TEST_F(ArtdTest, dexoptClassLoaderContext) {
  EXPECT_CALL(
      *mock_exec_utils_,
      DoExecAndReturnCode(
          WhenSplitBy("--",
                      _,
                      AllOf(Contains(ListFlag("--class-loader-context-fds=",
                                              ElementsAre(FdOf(clc_1_), FdOf(clc_2_)))),
                            Contains(Flag("--class-loader-context=", class_loader_context_)),
                            Contains(Flag("--classpath-dir=", scratch_path_ + "/a")))),
          _,
          _))
      .WillOnce(Return(0));
  RunDexopt();
}

TEST_F(ArtdTest, dexoptClassLoaderContextNull) {
  class_loader_context_ = std::nullopt;

  EXPECT_CALL(
      *mock_exec_utils_,
      DoExecAndReturnCode(WhenSplitBy("--",
                                      _,
                                      AllOf(Not(Contains(Flag("--class-loader-context-fds=", _))),
                                            Not(Contains(Flag("--class-loader-context=", _))),
                                            Not(Contains(Flag("--classpath-dir=", _))))),
                          _,
                          _))
      .WillOnce(Return(0));
  RunDexopt();
}

TEST_F(ArtdTest, dexoptNoOptionalInputFiles) {
  profile_path_ = std::nullopt;
  vdex_path_ = std::nullopt;
  dm_path_ = std::nullopt;

  EXPECT_CALL(*mock_exec_utils_,
              DoExecAndReturnCode(WhenSplitBy("--",
                                              _,
                                              AllOf(Not(Contains(Flag("--profile-file-fd=", _))),
                                                    Not(Contains(Flag("--input-vdex-fd=", _))),
                                                    Not(Contains(Flag("--dm-fd=", _))))),
                                  _,
                                  _))
      .WillOnce(Return(0));
  RunDexopt();
}

TEST_F(ArtdTest, dexoptPriorityClassBoot) {
  priority_class_ = PriorityClass::BOOT;
  EXPECT_CALL(*mock_exec_utils_,
              DoExecAndReturnCode(WhenSplitBy("--",
                                              AllOf(Not(Contains(Flag("--set-task-profile=", _))),
                                                    Not(Contains(Flag("--set-priority=", _)))),
                                              Contains(Flag("--compact-dex-level=", "none"))),
                                  _,
                                  _))
      .WillOnce(Return(0));
  RunDexopt();
}

TEST_F(ArtdTest, dexoptPriorityClassInteractive) {
  priority_class_ = PriorityClass::INTERACTIVE;
  EXPECT_CALL(*mock_exec_utils_,
              DoExecAndReturnCode(
                  WhenSplitBy("--",
                              AllOf(Contains(Flag("--set-task-profile=", "Dex2OatBootComplete")),
                                    Contains(Flag("--set-priority=", "background"))),
                              Contains(Flag("--compact-dex-level=", "none"))),
                  _,
                  _))
      .WillOnce(Return(0));
  RunDexopt();
}

TEST_F(ArtdTest, dexoptPriorityClassInteractiveFast) {
  priority_class_ = PriorityClass::INTERACTIVE_FAST;
  EXPECT_CALL(*mock_exec_utils_,
              DoExecAndReturnCode(
                  WhenSplitBy("--",
                              AllOf(Contains(Flag("--set-task-profile=", "Dex2OatBootComplete")),
                                    Contains(Flag("--set-priority=", "background"))),
                              Contains(Flag("--compact-dex-level=", "none"))),
                  _,
                  _))
      .WillOnce(Return(0));
  RunDexopt();
}

TEST_F(ArtdTest, dexoptPriorityClassBackground) {
  priority_class_ = PriorityClass::BACKGROUND;
  EXPECT_CALL(*mock_exec_utils_,
              DoExecAndReturnCode(
                  WhenSplitBy("--",
                              AllOf(Contains(Flag("--set-task-profile=", "Dex2OatBackground")),
                                    Contains(Flag("--set-priority=", "background"))),
                              Not(Contains(Flag("--compact-dex-level=", _)))),
                  _,
                  _))
      .WillOnce(Return(0));
  RunDexopt();
}

TEST_F(ArtdTest, dexoptDexoptOptions) {
  dexopt_options_ = DexoptOptions{
      .compilationReason = "install",
      .targetSdkVersion = 123,
      .debuggable = false,
      .generateAppImage = false,
      .hiddenApiPolicyEnabled = false,
      .comments = "my-comments",
  };

  EXPECT_CALL(
      *mock_exec_utils_,
      DoExecAndReturnCode(WhenSplitBy("--",
                                      _,
                                      AllOf(Contains(Flag("--compilation-reason=", "install")),
                                            Contains(Flag("-Xtarget-sdk-version:", "123")),
                                            Not(Contains("--debuggable")),
                                            Not(Contains(Flag("--app-image-fd=", _))),
                                            Not(Contains(Flag("-Xhidden-api-policy:", _))),
                                            Contains(Flag("--comments=", "my-comments")))),
                          _,
                          _))
      .WillOnce(Return(0));

  // `sizeBeforeBytes` should include the size of the old ART file even if no new ART file is
  // generated.
  RunDexopt(EX_NONE,
            Field(&ArtdDexoptResult::sizeBeforeBytes,
                  strlen("old_art") + strlen("old_oat") + strlen("old_vdex")));
}

TEST_F(ArtdTest, dexoptDexoptOptions2) {
  dexopt_options_ = DexoptOptions{
      .compilationReason = "bg-dexopt",
      .targetSdkVersion = 456,
      .debuggable = true,
      .generateAppImage = true,
      .hiddenApiPolicyEnabled = true,
  };

  EXPECT_CALL(
      *mock_exec_utils_,
      DoExecAndReturnCode(WhenSplitBy("--",
                                      _,
                                      AllOf(Contains(Flag("--compilation-reason=", "bg-dexopt")),
                                            Contains(Flag("-Xtarget-sdk-version:", "456")),
                                            Contains("--debuggable"),
                                            Contains(Flag("--app-image-fd=", _)),
                                            Contains(Flag("-Xhidden-api-policy:", "enabled")))),
                          _,
                          _))
      .WillOnce(Return(0));

  RunDexopt();
}

TEST_F(ArtdTest, dexoptDefaultFlagsWhenNoSystemProps) {
  dexopt_options_.generateAppImage = true;

  EXPECT_CALL(*mock_exec_utils_,
              DoExecAndReturnCode(
                  WhenSplitBy("--",
                              _,
                              AllOf(Contains(Flag("--swap-fd=", FdOf(_))),
                                    Not(Contains(Flag("--instruction-set-features=", _))),
                                    Not(Contains(Flag("--instruction-set-variant=", _))),
                                    Not(Contains(Flag("--max-image-block-size=", _))),
                                    Not(Contains(Flag("--very-large-app-threshold=", _))),
                                    Not(Contains(Flag("--resolve-startup-const-strings=", _))),
                                    Not(Contains("--generate-debug-info")),
                                    Not(Contains("--generate-mini-debug-info")),
                                    Contains("-Xdeny-art-apex-data-files"),
                                    Not(Contains(Flag("--cpu-set=", _))),
                                    Not(Contains(Flag("-j", _))),
                                    Not(Contains(Flag("-Xms", _))),
                                    Not(Contains(Flag("-Xmx", _))),
                                    Not(Contains("--compile-individually")),
                                    Not(Contains(Flag("--image-format=", _))),
                                    Not(Contains("--force-jit-zygote")),
                                    Not(Contains(Flag("--boot-image=", _))))),
                  _,
                  _))
      .WillOnce(Return(0));
  RunDexopt();
}

TEST_F(ArtdTest, dexoptFlagsFromSystemProps) {
  dexopt_options_.generateAppImage = true;

  EXPECT_CALL(*mock_props_, GetProperty("dalvik.vm.dex2oat-swap")).WillOnce(Return("0"));
  EXPECT_CALL(*mock_props_, GetProperty("dalvik.vm.isa.arm64.features"))
      .WillOnce(Return("features"));
  EXPECT_CALL(*mock_props_, GetProperty("dalvik.vm.isa.arm64.variant")).WillOnce(Return("variant"));
  EXPECT_CALL(*mock_props_, GetProperty("dalvik.vm.dex2oat-max-image-block-size"))
      .WillOnce(Return("size"));
  EXPECT_CALL(*mock_props_, GetProperty("dalvik.vm.dex2oat-very-large"))
      .WillOnce(Return("threshold"));
  EXPECT_CALL(*mock_props_, GetProperty("dalvik.vm.dex2oat-resolve-startup-strings"))
      .WillOnce(Return("strings"));
  EXPECT_CALL(*mock_props_, GetProperty("debug.generate-debug-info")).WillOnce(Return("1"));
  EXPECT_CALL(*mock_props_, GetProperty("dalvik.vm.dex2oat-minidebuginfo")).WillOnce(Return("1"));
  EXPECT_CALL(*mock_props_, GetProperty("odsign.verification.success")).WillOnce(Return("1"));
  EXPECT_CALL(*mock_props_, GetProperty("dalvik.vm.dex2oat-Xms")).WillOnce(Return("xms"));
  EXPECT_CALL(*mock_props_, GetProperty("dalvik.vm.dex2oat-Xmx")).WillOnce(Return("xmx"));
  EXPECT_CALL(*mock_props_, GetProperty("ro.config.low_ram")).WillOnce(Return("1"));
  EXPECT_CALL(*mock_props_, GetProperty("dalvik.vm.appimageformat")).WillOnce(Return("imgfmt"));
  EXPECT_CALL(*mock_props_, GetProperty("dalvik.vm.boot-image")).WillOnce(Return("boot-image"));

  EXPECT_CALL(*mock_exec_utils_,
              DoExecAndReturnCode(
                  WhenSplitBy("--",
                              _,
                              AllOf(Not(Contains(Flag("--swap-fd=", _))),
                                    Contains(Flag("--instruction-set-features=", "features")),
                                    Contains(Flag("--instruction-set-variant=", "variant")),
                                    Contains(Flag("--max-image-block-size=", "size")),
                                    Contains(Flag("--very-large-app-threshold=", "threshold")),
                                    Contains(Flag("--resolve-startup-const-strings=", "strings")),
                                    Contains("--generate-debug-info"),
                                    Contains("--generate-mini-debug-info"),
                                    Not(Contains("-Xdeny-art-apex-data-files")),
                                    Contains(Flag("-Xms", "xms")),
                                    Contains(Flag("-Xmx", "xmx")),
                                    Contains("--compile-individually"),
                                    Contains(Flag("--image-format=", "imgfmt")),
                                    Not(Contains("--force-jit-zygote")),
                                    Contains(Flag("--boot-image=", "boot-image")))),
                  _,
                  _))
      .WillOnce(Return(0));
  RunDexopt();
}

TEST_F(ArtdTest, dexoptFlagsForceJitZygote) {
  EXPECT_CALL(*mock_props_,
              GetProperty("persist.device_config.runtime_native_boot.profilebootclasspath"))
      .WillOnce(Return("true"));
  ON_CALL(*mock_props_, GetProperty("dalvik.vm.boot-image")).WillByDefault(Return("boot-image"));

  EXPECT_CALL(*mock_exec_utils_,
              DoExecAndReturnCode(WhenSplitBy("--",
                                              _,
                                              AllOf(Contains("--force-jit-zygote"),
                                                    Not(Contains(Flag("--boot-image=", _))))),
                                  _,
                                  _))
      .WillOnce(Return(0));
  RunDexopt();
}

static void SetDefaultResourceControlProps(MockSystemProperties* mock_props) {
  EXPECT_CALL(*mock_props, GetProperty("dalvik.vm.dex2oat-cpu-set")).WillRepeatedly(Return("0,2"));
  EXPECT_CALL(*mock_props, GetProperty("dalvik.vm.dex2oat-threads")).WillRepeatedly(Return("4"));
}

TEST_F(ArtdTest, dexoptDefaultResourceControlBoot) {
  SetDefaultResourceControlProps(mock_props_);

  // The default resource control properties don't apply to BOOT.
  EXPECT_CALL(
      *mock_exec_utils_,
      DoExecAndReturnCode(
          WhenSplitBy(
              "--", _, AllOf(Not(Contains(Flag("--cpu-set=", _))), Contains(Not(Flag("-j", _))))),
          _,
          _))
      .WillOnce(Return(0));
  priority_class_ = PriorityClass::BOOT;
  RunDexopt();
}

TEST_F(ArtdTest, dexoptDefaultResourceControlOther) {
  SetDefaultResourceControlProps(mock_props_);

  EXPECT_CALL(
      *mock_exec_utils_,
      DoExecAndReturnCode(
          WhenSplitBy(
              "--", _, AllOf(Contains(Flag("--cpu-set=", "0,2")), Contains(Flag("-j", "4")))),
          _,
          _))
      .Times(3)
      .WillRepeatedly(Return(0));
  priority_class_ = PriorityClass::INTERACTIVE_FAST;
  RunDexopt();
  priority_class_ = PriorityClass::INTERACTIVE;
  RunDexopt();
  priority_class_ = PriorityClass::BACKGROUND;
  RunDexopt();
}

static void SetAllResourceControlProps(MockSystemProperties* mock_props) {
  EXPECT_CALL(*mock_props, GetProperty("dalvik.vm.dex2oat-cpu-set")).WillRepeatedly(Return("0,2"));
  EXPECT_CALL(*mock_props, GetProperty("dalvik.vm.dex2oat-threads")).WillRepeatedly(Return("4"));
  EXPECT_CALL(*mock_props, GetProperty("dalvik.vm.boot-dex2oat-cpu-set"))
      .WillRepeatedly(Return("0,1,2,3"));
  EXPECT_CALL(*mock_props, GetProperty("dalvik.vm.boot-dex2oat-threads"))
      .WillRepeatedly(Return("8"));
  EXPECT_CALL(*mock_props, GetProperty("dalvik.vm.restore-dex2oat-cpu-set"))
      .WillRepeatedly(Return("0,2,3"));
  EXPECT_CALL(*mock_props, GetProperty("dalvik.vm.restore-dex2oat-threads"))
      .WillRepeatedly(Return("6"));
  EXPECT_CALL(*mock_props, GetProperty("dalvik.vm.background-dex2oat-cpu-set"))
      .WillRepeatedly(Return("0"));
  EXPECT_CALL(*mock_props, GetProperty("dalvik.vm.background-dex2oat-threads"))
      .WillRepeatedly(Return("2"));
}

TEST_F(ArtdTest, dexoptAllResourceControlBoot) {
  SetAllResourceControlProps(mock_props_);

  EXPECT_CALL(
      *mock_exec_utils_,
      DoExecAndReturnCode(
          WhenSplitBy(
              "--", _, AllOf(Contains(Flag("--cpu-set=", "0,1,2,3")), Contains(Flag("-j", "8")))),
          _,
          _))
      .WillOnce(Return(0));
  priority_class_ = PriorityClass::BOOT;
  RunDexopt();
}

TEST_F(ArtdTest, dexoptAllResourceControlInteractiveFast) {
  SetAllResourceControlProps(mock_props_);

  EXPECT_CALL(
      *mock_exec_utils_,
      DoExecAndReturnCode(
          WhenSplitBy(
              "--", _, AllOf(Contains(Flag("--cpu-set=", "0,2,3")), Contains(Flag("-j", "6")))),
          _,
          _))
      .WillOnce(Return(0));
  priority_class_ = PriorityClass::INTERACTIVE_FAST;
  RunDexopt();
}

TEST_F(ArtdTest, dexoptAllResourceControlInteractive) {
  SetAllResourceControlProps(mock_props_);

  // INTERACTIVE always uses the default resource control properties.
  EXPECT_CALL(
      *mock_exec_utils_,
      DoExecAndReturnCode(
          WhenSplitBy(
              "--", _, AllOf(Contains(Flag("--cpu-set=", "0,2")), Contains(Flag("-j", "4")))),
          _,
          _))
      .WillOnce(Return(0));
  priority_class_ = PriorityClass::INTERACTIVE;
  RunDexopt();
}

TEST_F(ArtdTest, dexoptAllResourceControlBackground) {
  SetAllResourceControlProps(mock_props_);

  EXPECT_CALL(
      *mock_exec_utils_,
      DoExecAndReturnCode(
          WhenSplitBy("--", _, AllOf(Contains(Flag("--cpu-set=", "0")), Contains(Flag("-j", "2")))),
          _,
          _))
      .WillOnce(Return(0));
  priority_class_ = PriorityClass::BACKGROUND;
  RunDexopt();
}

TEST_F(ArtdTest, dexoptFailed) {
  dexopt_options_.generateAppImage = true;
  EXPECT_CALL(*mock_exec_utils_, DoExecAndReturnCode(_, _, _))
      .WillOnce(DoAll(WithArg<0>(WriteToFdFlag("--oat-fd=", "new_oat")),
                      WithArg<0>(WriteToFdFlag("--output-vdex-fd=", "new_vdex")),
                      WithArg<0>(WriteToFdFlag("--app-image-fd=", "new_art")),
                      Return(1)));
  RunDexopt(EX_SERVICE_SPECIFIC);

  CheckContent(scratch_path_ + "/a/oat/arm64/b.odex", "old_oat");
  CheckContent(scratch_path_ + "/a/oat/arm64/b.vdex", "old_vdex");
  CheckContent(scratch_path_ + "/a/oat/arm64/b.art", "old_art");
}

TEST_F(ArtdTest, dexoptFailedToCommit) {
  std::unique_ptr<ScopeGuard<std::function<void()>>> scoped_inaccessible;
  std::unique_ptr<ScopeGuard<std::function<void()>>> scoped_unroot;

  EXPECT_CALL(*mock_exec_utils_, DoExecAndReturnCode(_, _, _))
      .WillOnce(DoAll(WithArg<0>(WriteToFdFlag("--oat-fd=", "new_oat")),
                      WithArg<0>(WriteToFdFlag("--output-vdex-fd=", "new_vdex")),
                      [&](auto, auto, auto) {
                        scoped_inaccessible = std::make_unique<ScopeGuard<std::function<void()>>>(
                            ScopedInaccessible(scratch_path_ + "/a/oat/arm64"));
                        scoped_unroot =
                            std::make_unique<ScopeGuard<std::function<void()>>>(ScopedUnroot());
                        return 0;
                      }));

  RunDexopt(
      EX_SERVICE_SPECIFIC,
      AllOf(Field(&ArtdDexoptResult::sizeBytes, 0), Field(&ArtdDexoptResult::sizeBeforeBytes, 0)));
}

TEST_F(ArtdTest, dexoptCancelledBeforeDex2oat) {
  std::shared_ptr<IArtdCancellationSignal> cancellation_signal;
  ASSERT_TRUE(artd_->createCancellationSignal(&cancellation_signal).isOk());

  constexpr pid_t kPid = 123;

  EXPECT_CALL(*mock_exec_utils_, DoExecAndReturnCode(_, _, _))
      .WillOnce([&](auto, const ExecCallbacks& callbacks, auto) {
        callbacks.on_start(kPid);
        callbacks.on_end(kPid);
        return Error();
      });
  EXPECT_CALL(mock_kill_, Call(kPid, SIGKILL));

  cancellation_signal->cancel();

  RunDexopt(EX_NONE, Field(&ArtdDexoptResult::cancelled, true), cancellation_signal);

  CheckContent(scratch_path_ + "/a/oat/arm64/b.odex", "old_oat");
  CheckContent(scratch_path_ + "/a/oat/arm64/b.vdex", "old_vdex");
  CheckContent(scratch_path_ + "/a/oat/arm64/b.art", "old_art");
}

TEST_F(ArtdTest, dexoptCancelledDuringDex2oat) {
  std::shared_ptr<IArtdCancellationSignal> cancellation_signal;
  ASSERT_TRUE(artd_->createCancellationSignal(&cancellation_signal).isOk());

  constexpr pid_t kPid = 123;
  constexpr std::chrono::duration<int> kTimeout = std::chrono::seconds(1);

  std::condition_variable process_started_cv, process_killed_cv;
  std::mutex mu;

  EXPECT_CALL(*mock_exec_utils_, DoExecAndReturnCode(_, _, _))
      .WillOnce([&](auto, const ExecCallbacks& callbacks, auto) {
        std::unique_lock<std::mutex> lock(mu);
        // Step 2.
        callbacks.on_start(kPid);
        process_started_cv.notify_one();
        EXPECT_EQ(process_killed_cv.wait_for(lock, kTimeout), std::cv_status::no_timeout);
        // Step 5.
        callbacks.on_end(kPid);
        return Error();
      });

  EXPECT_CALL(mock_kill_, Call(kPid, SIGKILL)).WillOnce([&](auto, auto) {
    // Step 4.
    process_killed_cv.notify_one();
    return 0;
  });

  std::thread t;
  {
    std::unique_lock<std::mutex> lock(mu);
    // Step 1.
    t = std::thread([&] {
      RunDexopt(EX_NONE, Field(&ArtdDexoptResult::cancelled, true), cancellation_signal);
    });
    EXPECT_EQ(process_started_cv.wait_for(lock, kTimeout), std::cv_status::no_timeout);
    // Step 3.
    cancellation_signal->cancel();
  }

  t.join();

  // Step 6.
  CheckContent(scratch_path_ + "/a/oat/arm64/b.odex", "old_oat");
  CheckContent(scratch_path_ + "/a/oat/arm64/b.vdex", "old_vdex");
  CheckContent(scratch_path_ + "/a/oat/arm64/b.art", "old_art");
}

TEST_F(ArtdTest, dexoptCancelledAfterDex2oat) {
  std::shared_ptr<IArtdCancellationSignal> cancellation_signal;
  ASSERT_TRUE(artd_->createCancellationSignal(&cancellation_signal).isOk());

  constexpr pid_t kPid = 123;

  EXPECT_CALL(*mock_exec_utils_, DoExecAndReturnCode(_, _, _))
      .WillOnce(DoAll(WithArg<0>(WriteToFdFlag("--oat-fd=", "new_oat")),
                      WithArg<0>(WriteToFdFlag("--output-vdex-fd=", "new_vdex")),
                      [&](auto, const ExecCallbacks& callbacks, auto) {
                        callbacks.on_start(kPid);
                        callbacks.on_end(kPid);
                        return 0;
                      }));
  EXPECT_CALL(mock_kill_, Call).Times(0);

  RunDexopt(EX_NONE, Field(&ArtdDexoptResult::cancelled, false), cancellation_signal);

  // This signal should be ignored.
  cancellation_signal->cancel();

  CheckContent(scratch_path_ + "/a/oat/arm64/b.odex", "new_oat");
  CheckContent(scratch_path_ + "/a/oat/arm64/b.vdex", "new_vdex");
  EXPECT_FALSE(std::filesystem::exists(scratch_path_ + "/a/oat/arm64/b.art"));
}

TEST_F(ArtdTest, dexoptDexFileNotOtherReadable) {
  dex_file_other_readable_ = false;
  EXPECT_CALL(*mock_exec_utils_, DoExecAndReturnCode(_, _, _)).Times(0);
  RunDexopt(AllOf(Property(&ndk::ScopedAStatus::getExceptionCode, EX_SERVICE_SPECIFIC),
                  Property(&ndk::ScopedAStatus::getMessage,
                           HasSubstr("Outputs cannot be other-readable because the dex file"))));
}

TEST_F(ArtdTest, dexoptProfileNotOtherReadable) {
  profile_other_readable_ = false;
  EXPECT_CALL(*mock_exec_utils_, DoExecAndReturnCode(_, _, _)).Times(0);
  RunDexopt(AllOf(Property(&ndk::ScopedAStatus::getExceptionCode, EX_SERVICE_SPECIFIC),
                  Property(&ndk::ScopedAStatus::getMessage,
                           HasSubstr("Outputs cannot be other-readable because the profile"))));
}

TEST_F(ArtdTest, dexoptOutputNotOtherReadable) {
  output_artifacts_.permissionSettings.fileFsPermission.isOtherReadable = false;
  dex_file_other_readable_ = false;
  profile_other_readable_ = false;
  EXPECT_CALL(*mock_exec_utils_, DoExecAndReturnCode(_, _, _)).WillOnce(Return(0));
  RunDexopt();
  CheckOtherReadable(scratch_path_ + "/a/oat/arm64/b.odex", false);
  CheckOtherReadable(scratch_path_ + "/a/oat/arm64/b.vdex", false);
}

TEST_F(ArtdTest, dexoptUidMismatch) {
  output_artifacts_.permissionSettings.fileFsPermission.uid = 12345;
  output_artifacts_.permissionSettings.fileFsPermission.isOtherReadable = false;
  dex_file_other_readable_ = false;
  EXPECT_CALL(*mock_exec_utils_, DoExecAndReturnCode(_, _, _)).Times(0);
  RunDexopt(AllOf(Property(&ndk::ScopedAStatus::getExceptionCode, EX_SERVICE_SPECIFIC),
                  Property(&ndk::ScopedAStatus::getMessage,
                           HasSubstr("Outputs' owner doesn't match the dex file"))));
}

TEST_F(ArtdTest, dexoptGidMismatch) {
  output_artifacts_.permissionSettings.fileFsPermission.gid = 12345;
  output_artifacts_.permissionSettings.fileFsPermission.isOtherReadable = false;
  dex_file_other_readable_ = false;
  EXPECT_CALL(*mock_exec_utils_, DoExecAndReturnCode(_, _, _)).Times(0);
  RunDexopt(AllOf(Property(&ndk::ScopedAStatus::getExceptionCode, EX_SERVICE_SPECIFIC),
                  Property(&ndk::ScopedAStatus::getMessage,
                           HasSubstr("Outputs' owner doesn't match the dex file"))));
}

TEST_F(ArtdTest, dexoptGidMatchesUid) {
  output_artifacts_.permissionSettings.fileFsPermission = {
      .uid = 123, .gid = 123, .isOtherReadable = false};
  EXPECT_CALL(mock_fstat_, Call(_, _)).WillRepeatedly(fstat);  // For profile.
  EXPECT_CALL(mock_fstat_, Call(FdOf(dex_file_), _))
      .WillOnce(DoAll(SetArgPointee<1>((struct stat){
                          .st_mode = S_IRUSR | S_IRGRP, .st_uid = 123, .st_gid = 456}),
                      Return(0)));
  ON_CALL(*mock_exec_utils_, DoExecAndReturnCode(_, _, _)).WillByDefault(Return(0));
  // It's okay to fail on chown. This happens when the test is not run as root.
  RunDexopt(AnyOf(Property(&ndk::ScopedAStatus::getExceptionCode, EX_NONE),
                  AllOf(Property(&ndk::ScopedAStatus::getExceptionCode, EX_SERVICE_SPECIFIC),
                        Property(&ndk::ScopedAStatus::getMessage, HasSubstr("Failed to chown")))));
}

TEST_F(ArtdTest, dexoptGidMatchesGid) {
  output_artifacts_.permissionSettings.fileFsPermission = {
      .uid = 123, .gid = 456, .isOtherReadable = false};
  EXPECT_CALL(mock_fstat_, Call(_, _)).WillRepeatedly(fstat);  // For profile.
  EXPECT_CALL(mock_fstat_, Call(FdOf(dex_file_), _))
      .WillOnce(DoAll(SetArgPointee<1>((struct stat){
                          .st_mode = S_IRUSR | S_IRGRP, .st_uid = 123, .st_gid = 456}),
                      Return(0)));
  ON_CALL(*mock_exec_utils_, DoExecAndReturnCode(_, _, _)).WillByDefault(Return(0));
  // It's okay to fail on chown. This happens when the test is not run as root.
  RunDexopt(AnyOf(Property(&ndk::ScopedAStatus::getExceptionCode, EX_NONE),
                  AllOf(Property(&ndk::ScopedAStatus::getExceptionCode, EX_SERVICE_SPECIFIC),
                        Property(&ndk::ScopedAStatus::getMessage, HasSubstr("Failed to chown")))));
}

TEST_F(ArtdTest, dexoptUidGidChangeOk) {
  // The dex file is other-readable, so we don't check uid and gid.
  output_artifacts_.permissionSettings.fileFsPermission = {
      .uid = 12345, .gid = 12345, .isOtherReadable = false};
  ON_CALL(*mock_exec_utils_, DoExecAndReturnCode(_, _, _)).WillByDefault(Return(0));
  // It's okay to fail on chown. This happens when the test is not run as root.
  RunDexopt(AnyOf(Property(&ndk::ScopedAStatus::getExceptionCode, EX_NONE),
                  AllOf(Property(&ndk::ScopedAStatus::getExceptionCode, EX_SERVICE_SPECIFIC),
                        Property(&ndk::ScopedAStatus::getMessage, HasSubstr("Failed to chown")))));
}

TEST_F(ArtdTest, dexoptNoUidGidChange) {
  output_artifacts_.permissionSettings.fileFsPermission = {
      .uid = -1, .gid = -1, .isOtherReadable = false};
  dex_file_other_readable_ = false;
  EXPECT_CALL(*mock_exec_utils_, DoExecAndReturnCode(_, _, _)).WillOnce(Return(0));
  RunDexopt();
}

TEST_F(ArtdTest, isProfileUsable) {
  std::string profile_file = OR_FATAL(BuildProfileOrDmPath(profile_path_.value()));
  CreateFile(profile_file);
  CreateFile(dex_file_);

  EXPECT_CALL(
      *mock_exec_utils_,
      DoExecAndReturnCode(
          AllOf(WhenSplitBy(
                    "--",
                    AllOf(Contains(art_root_ + "/bin/art_exec"), Contains("--drop-capabilities")),
                    AllOf(Contains(art_root_ + "/bin/profman"),
                          Contains(Flag("--reference-profile-file-fd=", FdOf(profile_file))),
                          Contains(Flag("--apk-fd=", FdOf(dex_file_))))),
                HasKeepFdsFor("--reference-profile-file-fd=", "--apk-fd=")),
          _,
          _))
      .WillOnce(Return(ProfmanResult::kSkipCompilationSmallDelta));

  bool result;
  EXPECT_TRUE(artd_->isProfileUsable(profile_path_.value(), dex_file_, &result).isOk());
  EXPECT_TRUE(result);
}

TEST_F(ArtdTest, isProfileUsableFalse) {
  std::string profile_file = OR_FATAL(BuildProfileOrDmPath(profile_path_.value()));
  CreateFile(profile_file);
  CreateFile(dex_file_);

  EXPECT_CALL(*mock_exec_utils_, DoExecAndReturnCode(_, _, _))
      .WillOnce(Return(ProfmanResult::kSkipCompilationEmptyProfiles));

  bool result;
  EXPECT_TRUE(artd_->isProfileUsable(profile_path_.value(), dex_file_, &result).isOk());
  EXPECT_FALSE(result);
}

TEST_F(ArtdTest, isProfileUsableNotFound) {
  CreateFile(dex_file_);

  bool result;
  EXPECT_TRUE(artd_->isProfileUsable(profile_path_.value(), dex_file_, &result).isOk());
  EXPECT_FALSE(result);
}

TEST_F(ArtdTest, isProfileUsableFailed) {
  std::string profile_file = OR_FATAL(BuildProfileOrDmPath(profile_path_.value()));
  CreateFile(profile_file);
  CreateFile(dex_file_);

  EXPECT_CALL(*mock_exec_utils_, DoExecAndReturnCode(_, _, _)).WillOnce(Return(100));

  bool result;
  ndk::ScopedAStatus status = artd_->isProfileUsable(profile_path_.value(), dex_file_, &result);

  EXPECT_FALSE(status.isOk());
  EXPECT_EQ(status.getExceptionCode(), EX_SERVICE_SPECIFIC);
  EXPECT_THAT(status.getMessage(), HasSubstr("profman returned an unexpected code: 100"));
}

TEST_F(ArtdTest, copyAndRewriteProfile) {
  const TmpProfilePath& src = profile_path_->get<ProfilePath::tmpProfilePath>();
  std::string src_file = OR_FATAL(BuildTmpProfilePath(src));
  CreateFile(src_file, "abc");
  OutputProfile dst{.profilePath = src, .fsPermission = FsPermission{.uid = -1, .gid = -1}};
  dst.profilePath.id = "";
  dst.profilePath.tmpPath = "";

  CreateFile(dex_file_);

  EXPECT_CALL(
      *mock_exec_utils_,
      DoExecAndReturnCode(
          AllOf(WhenSplitBy(
                    "--",
                    AllOf(Contains(art_root_ + "/bin/art_exec"), Contains("--drop-capabilities")),
                    AllOf(Contains(art_root_ + "/bin/profman"),
                          Contains("--copy-and-update-profile-key"),
                          Contains(Flag("--profile-file-fd=", FdOf(src_file))),
                          Contains(Flag("--apk-fd=", FdOf(dex_file_))))),
                HasKeepFdsFor("--profile-file-fd=", "--reference-profile-file-fd=", "--apk-fd=")),
          _,
          _))
      .WillOnce(DoAll(WithArg<0>(WriteToFdFlag("--reference-profile-file-fd=", "def")),
                      Return(ProfmanResult::kCopyAndUpdateSuccess)));

  bool result;
  EXPECT_TRUE(artd_->copyAndRewriteProfile(src, &dst, dex_file_, &result).isOk());
  EXPECT_TRUE(result);
  EXPECT_THAT(dst.profilePath.id, Not(IsEmpty()));
  std::string real_path = OR_FATAL(BuildTmpProfilePath(dst.profilePath));
  EXPECT_EQ(dst.profilePath.tmpPath, real_path);
  CheckContent(real_path, "def");
}

TEST_F(ArtdTest, copyAndRewriteProfileFalse) {
  const TmpProfilePath& src = profile_path_->get<ProfilePath::tmpProfilePath>();
  std::string src_file = OR_FATAL(BuildTmpProfilePath(src));
  CreateFile(src_file, "abc");
  OutputProfile dst{.profilePath = src, .fsPermission = FsPermission{.uid = -1, .gid = -1}};
  dst.profilePath.id = "";
  dst.profilePath.tmpPath = "";

  CreateFile(dex_file_);

  EXPECT_CALL(*mock_exec_utils_, DoExecAndReturnCode(_, _, _))
      .WillOnce(Return(ProfmanResult::kCopyAndUpdateNoMatch));

  bool result;
  EXPECT_TRUE(artd_->copyAndRewriteProfile(src, &dst, dex_file_, &result).isOk());
  EXPECT_FALSE(result);
  EXPECT_THAT(dst.profilePath.id, IsEmpty());
  EXPECT_THAT(dst.profilePath.tmpPath, IsEmpty());
}

TEST_F(ArtdTest, copyAndRewriteProfileNotFound) {
  CreateFile(dex_file_);

  const TmpProfilePath& src = profile_path_->get<ProfilePath::tmpProfilePath>();
  OutputProfile dst{.profilePath = src, .fsPermission = FsPermission{.uid = -1, .gid = -1}};
  dst.profilePath.id = "";
  dst.profilePath.tmpPath = "";

  bool result;
  EXPECT_TRUE(artd_->copyAndRewriteProfile(src, &dst, dex_file_, &result).isOk());
  EXPECT_FALSE(result);
  EXPECT_THAT(dst.profilePath.id, IsEmpty());
  EXPECT_THAT(dst.profilePath.tmpPath, IsEmpty());
}

TEST_F(ArtdTest, copyAndRewriteProfileFailed) {
  const TmpProfilePath& src = profile_path_->get<ProfilePath::tmpProfilePath>();
  std::string src_file = OR_FATAL(BuildTmpProfilePath(src));
  CreateFile(src_file, "abc");
  OutputProfile dst{.profilePath = src, .fsPermission = FsPermission{.uid = -1, .gid = -1}};
  dst.profilePath.id = "";
  dst.profilePath.tmpPath = "";

  CreateFile(dex_file_);

  EXPECT_CALL(*mock_exec_utils_, DoExecAndReturnCode(_, _, _)).WillOnce(Return(100));

  bool result;
  ndk::ScopedAStatus status = artd_->copyAndRewriteProfile(src, &dst, dex_file_, &result);

  EXPECT_FALSE(status.isOk());
  EXPECT_EQ(status.getExceptionCode(), EX_SERVICE_SPECIFIC);
  EXPECT_THAT(status.getMessage(), HasSubstr("profman returned an unexpected code: 100"));
  EXPECT_THAT(dst.profilePath.id, IsEmpty());
  EXPECT_THAT(dst.profilePath.tmpPath, IsEmpty());
}

TEST_F(ArtdTest, commitTmpProfile) {
  const TmpProfilePath& tmp_profile_path = profile_path_->get<ProfilePath::tmpProfilePath>();
  std::string tmp_profile_file = OR_FATAL(BuildTmpProfilePath(tmp_profile_path));
  CreateFile(tmp_profile_file);

  EXPECT_TRUE(artd_->commitTmpProfile(tmp_profile_path).isOk());

  EXPECT_FALSE(std::filesystem::exists(tmp_profile_file));
  EXPECT_TRUE(std::filesystem::exists(OR_FATAL(BuildFinalProfilePath(tmp_profile_path))));
}

TEST_F(ArtdTest, commitTmpProfileFailed) {
  const TmpProfilePath& tmp_profile_path = profile_path_->get<ProfilePath::tmpProfilePath>();
  ndk::ScopedAStatus status = artd_->commitTmpProfile(tmp_profile_path);

  EXPECT_FALSE(status.isOk());
  EXPECT_EQ(status.getExceptionCode(), EX_SERVICE_SPECIFIC);
  EXPECT_THAT(
      status.getMessage(),
      ContainsRegex(R"re(Failed to move .*primary\.prof\.12345\.tmp.* to .*primary\.prof)re"));

  EXPECT_FALSE(std::filesystem::exists(OR_FATAL(BuildFinalProfilePath(tmp_profile_path))));
}

TEST_F(ArtdTest, deleteProfile) {
  std::string profile_file = OR_FATAL(BuildProfileOrDmPath(profile_path_.value()));
  CreateFile(profile_file);

  EXPECT_TRUE(artd_->deleteProfile(profile_path_.value()).isOk());

  EXPECT_FALSE(std::filesystem::exists(profile_file));
}

TEST_F(ArtdTest, deleteProfileDoesNotExist) {
  auto scoped_set_logger = ScopedSetLogger(mock_logger_.AsStdFunction());
  EXPECT_CALL(mock_logger_, Call).Times(0);

  EXPECT_TRUE(artd_->deleteProfile(profile_path_.value()).isOk());
}

TEST_F(ArtdTest, deleteProfileFailed) {
  auto scoped_set_logger = ScopedSetLogger(mock_logger_.AsStdFunction());
  EXPECT_CALL(
      mock_logger_,
      Call(_, _, _, _, _, ContainsRegex(R"re(Failed to remove .*primary\.prof\.12345\.tmp)re")));

  std::string profile_file = OR_FATAL(BuildProfileOrDmPath(profile_path_.value()));
  auto scoped_inaccessible = ScopedInaccessible(std::filesystem::path(profile_file).parent_path());
  auto scoped_unroot = ScopedUnroot();

  EXPECT_TRUE(artd_->deleteProfile(profile_path_.value()).isOk());
}

class ArtdGetVisibilityTest : public ArtdTest {
 protected:
  template <typename PathType>
  using Method = ndk::ScopedAStatus (Artd::*)(const PathType&, FileVisibility*);

  template <typename PathType>
  void TestGetVisibilityOtherReadable(Method<PathType> method,
                                      const PathType& input,
                                      const std::string& path) {
    CreateFile(path);
    std::filesystem::permissions(
        path, std::filesystem::perms::others_read, std::filesystem::perm_options::add);

    FileVisibility result;
    ASSERT_TRUE(((*artd_).*method)(input, &result).isOk());
    EXPECT_EQ(result, FileVisibility::OTHER_READABLE);
  }

  template <typename PathType>
  void TestGetVisibilityNotOtherReadable(Method<PathType> method,
                                         const PathType& input,
                                         const std::string& path) {
    CreateFile(path);
    std::filesystem::permissions(
        path, std::filesystem::perms::others_read, std::filesystem::perm_options::remove);

    FileVisibility result;
    ASSERT_TRUE(((*artd_).*method)(input, &result).isOk());
    EXPECT_EQ(result, FileVisibility::NOT_OTHER_READABLE);
  }

  template <typename PathType>
  void TestGetVisibilityNotFound(Method<PathType> method, const PathType& input) {
    FileVisibility result;
    ASSERT_TRUE(((*artd_).*method)(input, &result).isOk());
    EXPECT_EQ(result, FileVisibility::NOT_FOUND);
  }

  template <typename PathType>
  void TestGetVisibilityPermissionDenied(Method<PathType> method,
                                         const PathType& input,
                                         const std::string& path) {
    CreateFile(path);

    auto scoped_inaccessible = ScopedInaccessible(std::filesystem::path(path).parent_path());
    auto scoped_unroot = ScopedUnroot();

    FileVisibility result;
    ndk::ScopedAStatus status = ((*artd_).*method)(input, &result);
    EXPECT_FALSE(status.isOk());
    EXPECT_EQ(status.getExceptionCode(), EX_SERVICE_SPECIFIC);
    EXPECT_THAT(status.getMessage(), HasSubstr("Failed to get status of"));
  }
};

TEST_F(ArtdGetVisibilityTest, getProfileVisibilityOtherReadable) {
  TestGetVisibilityOtherReadable(&Artd::getProfileVisibility,
                                 profile_path_.value(),
                                 OR_FATAL(BuildProfileOrDmPath(profile_path_.value())));
}

TEST_F(ArtdGetVisibilityTest, getProfileVisibilityNotOtherReadable) {
  TestGetVisibilityNotOtherReadable(&Artd::getProfileVisibility,
                                    profile_path_.value(),
                                    OR_FATAL(BuildProfileOrDmPath(profile_path_.value())));
}

TEST_F(ArtdGetVisibilityTest, getProfileVisibilityNotFound) {
  TestGetVisibilityNotFound(&Artd::getProfileVisibility, profile_path_.value());
}

TEST_F(ArtdGetVisibilityTest, getProfileVisibilityPermissionDenied) {
  TestGetVisibilityPermissionDenied(&Artd::getProfileVisibility,
                                    profile_path_.value(),
                                    OR_FATAL(BuildProfileOrDmPath(profile_path_.value())));
}

TEST_F(ArtdGetVisibilityTest, getArtifactsVisibilityOtherReadable) {
  TestGetVisibilityOtherReadable(
      &Artd::getArtifactsVisibility, artifacts_path_, OR_FATAL(BuildOatPath(artifacts_path_)));
}

TEST_F(ArtdGetVisibilityTest, getArtifactsVisibilityNotOtherReadable) {
  TestGetVisibilityNotOtherReadable(
      &Artd::getArtifactsVisibility, artifacts_path_, OR_FATAL(BuildOatPath(artifacts_path_)));
}

TEST_F(ArtdGetVisibilityTest, getArtifactsVisibilityNotFound) {
  TestGetVisibilityNotFound(&Artd::getArtifactsVisibility, artifacts_path_);
}

TEST_F(ArtdGetVisibilityTest, getArtifactsVisibilityPermissionDenied) {
  TestGetVisibilityPermissionDenied(
      &Artd::getArtifactsVisibility, artifacts_path_, OR_FATAL(BuildOatPath(artifacts_path_)));
}

TEST_F(ArtdGetVisibilityTest, getDexFileVisibilityOtherReadable) {
  TestGetVisibilityOtherReadable(&Artd::getDexFileVisibility, dex_file_, dex_file_);
}

TEST_F(ArtdGetVisibilityTest, getDexFileVisibilityNotOtherReadable) {
  TestGetVisibilityNotOtherReadable(&Artd::getDexFileVisibility, dex_file_, dex_file_);
}

TEST_F(ArtdGetVisibilityTest, getDexFileVisibilityNotFound) {
  TestGetVisibilityNotFound(&Artd::getDexFileVisibility, dex_file_);
}

TEST_F(ArtdGetVisibilityTest, getDexFileVisibilityPermissionDenied) {
  TestGetVisibilityPermissionDenied(&Artd::getDexFileVisibility, dex_file_, dex_file_);
}

TEST_F(ArtdGetVisibilityTest, getDmFileVisibilityOtherReadable) {
  TestGetVisibilityOtherReadable(&Artd::getDmFileVisibility,
                                 dm_path_.value(),
                                 OR_FATAL(BuildDexMetadataPath(dm_path_.value())));
}

TEST_F(ArtdGetVisibilityTest, getDmFileVisibilityNotOtherReadable) {
  TestGetVisibilityNotOtherReadable(&Artd::getDmFileVisibility,
                                    dm_path_.value(),
                                    OR_FATAL(BuildDexMetadataPath(dm_path_.value())));
}

TEST_F(ArtdGetVisibilityTest, getDmFileVisibilityNotFound) {
  TestGetVisibilityNotFound(&Artd::getDmFileVisibility, dm_path_.value());
}

TEST_F(ArtdGetVisibilityTest, getDmFileVisibilityPermissionDenied) {
  TestGetVisibilityPermissionDenied(&Artd::getDmFileVisibility,
                                    dm_path_.value(),
                                    OR_FATAL(BuildDexMetadataPath(dm_path_.value())));
}

TEST_F(ArtdTest, mergeProfiles) {
  const TmpProfilePath& reference_profile_path = profile_path_->get<ProfilePath::tmpProfilePath>();
  std::string reference_profile_file = OR_FATAL(BuildTmpProfilePath(reference_profile_path));
  CreateFile(reference_profile_file, "abc");

  // Doesn't exist.
  PrimaryCurProfilePath profile_0_path{
      .userId = 0, .packageName = "com.android.foo", .profileName = "primary"};
  std::string profile_0_file = OR_FATAL(BuildPrimaryCurProfilePath(profile_0_path));

  PrimaryCurProfilePath profile_1_path{
      .userId = 1, .packageName = "com.android.foo", .profileName = "primary"};
  std::string profile_1_file = OR_FATAL(BuildPrimaryCurProfilePath(profile_1_path));
  CreateFile(profile_1_file, "def");

  OutputProfile output_profile{.profilePath = reference_profile_path,
                               .fsPermission = FsPermission{.uid = -1, .gid = -1}};
  output_profile.profilePath.id = "";
  output_profile.profilePath.tmpPath = "";

  std::string dex_file_1 = scratch_path_ + "/a/b.apk";
  std::string dex_file_2 = scratch_path_ + "/a/c.apk";
  CreateFile(dex_file_1);
  CreateFile(dex_file_2);

  EXPECT_CALL(
      *mock_exec_utils_,
      DoExecAndReturnCode(
          AllOf(WhenSplitBy(
                    "--",
                    AllOf(Contains(art_root_ + "/bin/art_exec"), Contains("--drop-capabilities")),
                    AllOf(Contains(art_root_ + "/bin/profman"),
                          Not(Contains(Flag("--profile-file-fd=", FdOf(profile_0_file)))),
                          Contains(Flag("--profile-file-fd=", FdOf(profile_1_file))),
                          Contains(Flag("--reference-profile-file-fd=", FdHasContent("abc"))),
                          Contains(Flag("--apk-fd=", FdOf(dex_file_1))),
                          Contains(Flag("--apk-fd=", FdOf(dex_file_2))),
                          Not(Contains("--force-merge")),
                          Not(Contains("--boot-image-merge")))),
                HasKeepFdsFor("--profile-file-fd=", "--reference-profile-file-fd=", "--apk-fd=")),
          _,
          _))
      .WillOnce(DoAll(WithArg<0>(ClearAndWriteToFdFlag("--reference-profile-file-fd=", "merged")),
                      Return(ProfmanResult::kCompile)));

  bool result;
  EXPECT_TRUE(artd_
                  ->mergeProfiles({profile_0_path, profile_1_path},
                                  reference_profile_path,
                                  &output_profile,
                                  {dex_file_1, dex_file_2},
                                  /*in_options=*/{},
                                  &result)
                  .isOk());
  EXPECT_TRUE(result);
  EXPECT_THAT(output_profile.profilePath.id, Not(IsEmpty()));
  std::string real_path = OR_FATAL(BuildTmpProfilePath(output_profile.profilePath));
  EXPECT_EQ(output_profile.profilePath.tmpPath, real_path);
  CheckContent(real_path, "merged");
}

TEST_F(ArtdTest, mergeProfilesEmptyReferenceProfile) {
  PrimaryCurProfilePath profile_0_path{
      .userId = 0, .packageName = "com.android.foo", .profileName = "primary"};
  std::string profile_0_file = OR_FATAL(BuildPrimaryCurProfilePath(profile_0_path));
  CreateFile(profile_0_file, "def");

  OutputProfile output_profile{.profilePath = profile_path_->get<ProfilePath::tmpProfilePath>(),
                               .fsPermission = FsPermission{.uid = -1, .gid = -1}};
  output_profile.profilePath.id = "";
  output_profile.profilePath.tmpPath = "";

  CreateFile(dex_file_);

  EXPECT_CALL(
      *mock_exec_utils_,
      DoExecAndReturnCode(
          WhenSplitBy("--",
                      AllOf(Contains(art_root_ + "/bin/art_exec"), Contains("--drop-capabilities")),
                      AllOf(Contains(art_root_ + "/bin/profman"),
                            Contains(Flag("--profile-file-fd=", FdOf(profile_0_file))),
                            Contains(Flag("--reference-profile-file-fd=", FdHasContent(""))),
                            Contains(Flag("--apk-fd=", FdOf(dex_file_))))),
          _,
          _))
      .WillOnce(DoAll(WithArg<0>(WriteToFdFlag("--reference-profile-file-fd=", "merged")),
                      Return(ProfmanResult::kCompile)));

  bool result;
  EXPECT_TRUE(artd_
                  ->mergeProfiles({profile_0_path},
                                  std::nullopt,
                                  &output_profile,
                                  {dex_file_},
                                  /*in_options=*/{},
                                  &result)
                  .isOk());
  EXPECT_TRUE(result);
  EXPECT_THAT(output_profile.profilePath.id, Not(IsEmpty()));
  EXPECT_THAT(output_profile.profilePath.tmpPath, Not(IsEmpty()));
}

TEST_F(ArtdTest, mergeProfilesProfilesDontExist) {
  const TmpProfilePath& reference_profile_path = profile_path_->get<ProfilePath::tmpProfilePath>();
  std::string reference_profile_file = OR_FATAL(BuildTmpProfilePath(reference_profile_path));
  CreateFile(reference_profile_file, "abc");

  // Doesn't exist.
  PrimaryCurProfilePath profile_0_path{
      .userId = 0, .packageName = "com.android.foo", .profileName = "primary"};
  std::string profile_0_file = OR_FATAL(BuildPrimaryCurProfilePath(profile_0_path));

  // Doesn't exist.
  PrimaryCurProfilePath profile_1_path{
      .userId = 1, .packageName = "com.android.foo", .profileName = "primary"};
  std::string profile_1_file = OR_FATAL(BuildPrimaryCurProfilePath(profile_1_path));

  OutputProfile output_profile{.profilePath = reference_profile_path,
                               .fsPermission = FsPermission{.uid = -1, .gid = -1}};
  output_profile.profilePath.id = "";
  output_profile.profilePath.tmpPath = "";

  CreateFile(dex_file_);

  EXPECT_CALL(*mock_exec_utils_, DoExecAndReturnCode).Times(0);

  bool result;
  EXPECT_TRUE(artd_
                  ->mergeProfiles({profile_0_path},
                                  std::nullopt,
                                  &output_profile,
                                  {dex_file_},
                                  /*in_options=*/{},
                                  &result)
                  .isOk());
  EXPECT_FALSE(result);
  EXPECT_THAT(output_profile.profilePath.id, IsEmpty());
  EXPECT_THAT(output_profile.profilePath.tmpPath, IsEmpty());
}

TEST_F(ArtdTest, mergeProfilesWithOptionsForceMerge) {
  PrimaryCurProfilePath profile_0_path{
      .userId = 0, .packageName = "com.android.foo", .profileName = "primary"};
  std::string profile_0_file = OR_FATAL(BuildPrimaryCurProfilePath(profile_0_path));
  CreateFile(profile_0_file, "def");

  OutputProfile output_profile{.profilePath = profile_path_->get<ProfilePath::tmpProfilePath>(),
                               .fsPermission = FsPermission{.uid = -1, .gid = -1}};
  output_profile.profilePath.id = "";
  output_profile.profilePath.tmpPath = "";

  CreateFile(dex_file_);

  EXPECT_CALL(
      *mock_exec_utils_,
      DoExecAndReturnCode(
          WhenSplitBy("--", _, AllOf(Contains("--force-merge"), Contains("--boot-image-merge"))),
          _,
          _))
      .WillOnce(Return(ProfmanResult::kSuccess));

  bool result;
  EXPECT_TRUE(artd_
                  ->mergeProfiles({profile_0_path},
                                  std::nullopt,
                                  &output_profile,
                                  {dex_file_},
                                  {.forceMerge = true, .forBootImage = true},
                                  &result)
                  .isOk());
  EXPECT_TRUE(result);
  EXPECT_THAT(output_profile.profilePath.id, Not(IsEmpty()));
  EXPECT_THAT(output_profile.profilePath.tmpPath, Not(IsEmpty()));
}

TEST_F(ArtdTest, mergeProfilesWithOptionsDumpOnly) {
  PrimaryCurProfilePath profile_0_path{
      .userId = 0, .packageName = "com.android.foo", .profileName = "primary"};
  std::string profile_0_file = OR_FATAL(BuildPrimaryCurProfilePath(profile_0_path));
  CreateFile(profile_0_file, "def");

  OutputProfile output_profile{.profilePath = profile_path_->get<ProfilePath::tmpProfilePath>(),
                               .fsPermission = FsPermission{.uid = -1, .gid = -1}};
  output_profile.profilePath.id = "";
  output_profile.profilePath.tmpPath = "";

  CreateFile(dex_file_);

  EXPECT_CALL(*mock_exec_utils_,
              DoExecAndReturnCode(
                  AllOf(WhenSplitBy("--",
                                    _,
                                    AllOf(Contains("--dump-only"),
                                          Not(Contains(Flag("--reference-profile-file-fd=", _))))),
                        HasKeepFdsFor("--profile-file-fd=", "--apk-fd=", "--dump-output-to-fd=")),
                  _,
                  _))
      .WillOnce(DoAll(WithArg<0>(WriteToFdFlag("--dump-output-to-fd=", "dump")),
                      Return(ProfmanResult::kSuccess)));

  bool result;
  EXPECT_TRUE(artd_
                  ->mergeProfiles({profile_0_path},
                                  std::nullopt,
                                  &output_profile,
                                  {dex_file_},
                                  {.dumpOnly = true},
                                  &result)
                  .isOk());
  EXPECT_TRUE(result);
  EXPECT_THAT(output_profile.profilePath.id, Not(IsEmpty()));
  CheckContent(output_profile.profilePath.tmpPath, "dump");
}

TEST_F(ArtdTest, mergeProfilesWithOptionsDumpClassesAndMethods) {
  PrimaryCurProfilePath profile_0_path{
      .userId = 0, .packageName = "com.android.foo", .profileName = "primary"};
  std::string profile_0_file = OR_FATAL(BuildPrimaryCurProfilePath(profile_0_path));
  CreateFile(profile_0_file, "def");

  OutputProfile output_profile{.profilePath = profile_path_->get<ProfilePath::tmpProfilePath>(),
                               .fsPermission = FsPermission{.uid = -1, .gid = -1}};
  output_profile.profilePath.id = "";
  output_profile.profilePath.tmpPath = "";

  CreateFile(dex_file_);

  EXPECT_CALL(*mock_exec_utils_,
              DoExecAndReturnCode(
                  WhenSplitBy("--",
                              _,
                              AllOf(Contains("--dump-classes-and-methods"),
                                    Not(Contains(Flag("--reference-profile-file-fd=", _))))),
                  _,
                  _))
      .WillOnce(DoAll(WithArg<0>(WriteToFdFlag("--dump-output-to-fd=", "dump")),
                      Return(ProfmanResult::kSuccess)));

  bool result;
  EXPECT_TRUE(artd_
                  ->mergeProfiles({profile_0_path},
                                  std::nullopt,
                                  &output_profile,
                                  {dex_file_},
                                  {.dumpClassesAndMethods = true},
                                  &result)
                  .isOk());
  EXPECT_TRUE(result);
  EXPECT_THAT(output_profile.profilePath.id, Not(IsEmpty()));
  CheckContent(output_profile.profilePath.tmpPath, "dump");
}

TEST_F(ArtdTest, cleanup) {
  std::vector<std::string> gc_removed_files;
  std::vector<std::string> gc_kept_files;

  auto CreateGcRemovedFile = [&](const std::string& path) {
    CreateFile(path);
    gc_removed_files.push_back(path);
  };

  auto CreateGcKeptFile = [&](const std::string& path) {
    CreateFile(path);
    gc_kept_files.push_back(path);
  };

  // Unmanaged files.
  CreateGcKeptFile(android_data_ + "/user_de/0/com.android.foo/1.odex");
  CreateGcKeptFile(android_data_ + "/user_de/0/com.android.foo/oat/1.odex");
  CreateGcKeptFile(android_data_ + "/user_de/0/com.android.foo/oat/1.txt");
  CreateGcKeptFile(android_data_ + "/user_de/0/com.android.foo/oat/arm64/1.txt");
  CreateGcKeptFile(android_data_ + "/user_de/0/com.android.foo/oat/arm64/1.tmp");

  // Files to keep.
  CreateGcKeptFile(android_data_ + "/misc/profiles/cur/1/com.android.foo/primary.prof");
  CreateGcKeptFile(android_data_ + "/misc/profiles/cur/3/com.android.foo/primary.prof");
  CreateGcKeptFile(android_data_ + "/dalvik-cache/arm64/system@app@Foo@Foo.apk@classes.dex");
  CreateGcKeptFile(android_data_ + "/dalvik-cache/arm64/system@app@Foo@Foo.apk@classes.vdex");
  CreateGcKeptFile(android_data_ + "/dalvik-cache/arm64/system@app@Foo@Foo.apk@classes.art");
  CreateGcKeptFile(android_data_ + "/user_de/0/com.android.foo/aaa/oat/arm64/1.vdex");
  CreateGcKeptFile(
      android_expand_ +
      "/123456-7890/app/~~nkfeankfna==/com.android.bar-jfoeaofiew==/oat/arm64/base.odex");
  CreateGcKeptFile(
      android_expand_ +
      "/123456-7890/app/~~nkfeankfna==/com.android.bar-jfoeaofiew==/oat/arm64/base.vdex");
  CreateGcKeptFile(
      android_expand_ +
      "/123456-7890/app/~~nkfeankfna==/com.android.bar-jfoeaofiew==/oat/arm64/base.art");
  CreateGcKeptFile(android_data_ + "/user_de/0/com.android.foo/aaa/oat/arm64/2.odex");
  CreateGcKeptFile(android_data_ + "/user_de/0/com.android.foo/aaa/oat/arm64/2.vdex");
  CreateGcKeptFile(android_data_ + "/user_de/0/com.android.foo/aaa/oat/arm64/2.art");

  // Files to remove.
  CreateGcRemovedFile(android_data_ + "/misc/profiles/ref/com.android.foo/primary.prof");
  CreateGcRemovedFile(android_data_ + "/misc/profiles/cur/2/com.android.foo/primary.prof");
  CreateGcRemovedFile(android_data_ + "/misc/profiles/cur/3/com.android.bar/primary.prof");
  CreateGcRemovedFile(android_data_ + "/dalvik-cache/arm64/extra.odex");
  CreateGcRemovedFile(android_data_ + "/dalvik-cache/arm64/system@app@Bar@Bar.apk@classes.dex");
  CreateGcRemovedFile(android_data_ + "/dalvik-cache/arm64/system@app@Bar@Bar.apk@classes.vdex");
  CreateGcRemovedFile(android_data_ + "/dalvik-cache/arm64/system@app@Bar@Bar.apk@classes.art");
  CreateGcRemovedFile(
      android_expand_ +
      "/123456-7890/app/~~daewfweaf==/com.android.foo-fjuwidhia==/oat/arm64/base.odex");
  CreateGcRemovedFile(
      android_expand_ +
      "/123456-7890/app/~~daewfweaf==/com.android.foo-fjuwidhia==/oat/arm64/base.vdex");
  CreateGcRemovedFile(
      android_expand_ +
      "/123456-7890/app/~~daewfweaf==/com.android.foo-fjuwidhia==/oat/arm64/base.art");
  CreateGcRemovedFile(android_data_ + "/user_de/0/com.android.foo/oat/1.prof");
  CreateGcRemovedFile(android_data_ + "/user_de/0/com.android.foo/oat/1.prof.123456.tmp");
  CreateGcRemovedFile(android_data_ + "/user_de/0/com.android.foo/oat/arm64/1.odex");
  CreateGcRemovedFile(android_data_ + "/user_de/0/com.android.foo/oat/arm64/1.vdex");
  CreateGcRemovedFile(android_data_ + "/user_de/0/com.android.foo/oat/arm64/1.art");
  CreateGcRemovedFile(android_data_ + "/user_de/0/com.android.foo/oat/arm64/1.odex.123456.tmp");
  CreateGcRemovedFile(android_data_ + "/user_de/0/com.android.foo/oat/arm64/2.odex.123456.tmp");
  CreateGcRemovedFile(android_data_ + "/user_de/0/com.android.foo/aaa/oat/arm64/1.odex");
  CreateGcRemovedFile(android_data_ + "/user_de/0/com.android.foo/aaa/oat/arm64/1.art");
  CreateGcRemovedFile(android_data_ + "/user_de/0/com.android.foo/aaa/oat/arm64/1.vdex.123456.tmp");
  CreateGcRemovedFile(android_data_ + "/user_de/0/com.android.foo/aaa/bbb/oat/arm64/1.odex");
  CreateGcRemovedFile(android_data_ + "/user_de/0/com.android.foo/aaa/bbb/oat/arm64/1.vdex");
  CreateGcRemovedFile(android_data_ + "/user_de/0/com.android.foo/aaa/bbb/oat/arm64/1.art");
  CreateGcRemovedFile(android_data_ +
                      "/user_de/0/com.android.foo/aaa/bbb/oat/arm64/1.art.123456.tmp");
  CreateGcRemovedFile(android_data_ + "/user_de/0/com.android.bar/aaa/oat/arm64/1.vdex");

  int64_t aidl_return;
  ASSERT_TRUE(
      artd_
          ->cleanup(
              {
                  PrimaryCurProfilePath{
                      .userId = 1, .packageName = "com.android.foo", .profileName = "primary"},
                  PrimaryCurProfilePath{
                      .userId = 3, .packageName = "com.android.foo", .profileName = "primary"},
              },
              {
                  ArtifactsPath{.dexPath = "/system/app/Foo/Foo.apk",
                                .isa = "arm64",
                                .isInDalvikCache = true},
                  ArtifactsPath{
                      .dexPath =
                          android_expand_ +
                          "/123456-7890/app/~~nkfeankfna==/com.android.bar-jfoeaofiew==/base.apk",
                      .isa = "arm64",
                      .isInDalvikCache = false},
                  ArtifactsPath{.dexPath = android_data_ + "/user_de/0/com.android.foo/aaa/2.apk",
                                .isa = "arm64",
                                .isInDalvikCache = false},
              },
              {
                  VdexPath{ArtifactsPath{
                      .dexPath = android_data_ + "/user_de/0/com.android.foo/aaa/1.apk",
                      .isa = "arm64",
                      .isInDalvikCache = false}},
              },
              &aidl_return)
          .isOk());

  for (const std::string& path : gc_removed_files) {
    EXPECT_FALSE(std::filesystem::exists(path)) << "'{}' should be removed"_format(path);
  }

  for (const std::string& path : gc_kept_files) {
    EXPECT_TRUE(std::filesystem::exists(path)) << "'{}' should be kept"_format(path);
  }
}

}  // namespace
}  // namespace artd
}  // namespace art
