/*
 * Copyright (C) 2020 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 <map>
#include <memory>
#include <string>

#include <android-base/logging.h>

#include <gtest/gtest.h>

#include "execv_helper.h"
#include "run_dex2oat.h"
#include "unique_file.h"

namespace android {
namespace installd {

class RunDex2OatTest : public testing::Test {
  public:
    static constexpr const char* INPUT_PATH = "/dir/input/basename.apk";
    static constexpr const char* OUTPUT_PATH = "/dir/output/basename.oat";
    static constexpr const char* FLAG_UNUSED = "{{FLAG_UNUSED}}";

    // UniqueFile closes FD. Avoid using standard I/O since the test is expected to print gtest
    // results. Alternatively, mock out UniqueFile to avoid the side effect of close(2).
    static constexpr int ZIP_FD = 3;
    static constexpr int OAT_FD = 4;
    static constexpr int INPUT_VDEX_FD = 5;
    static constexpr int OUTPUT_VDEX_FD = 6;
    static constexpr int IMAGE_FD = 7;
    static constexpr int PROFILE_FD = 8;
    static constexpr int DEX_METADATA_FD = 9;
    static constexpr int SWAP_FD = 10;

    using FakeSystemProperties = std::map<std::string, std::string>;

    // A fake RunDex2Oat that allows to override (fake) system properties and starts with none.
    class FakeRunDex2Oat : public RunDex2Oat {
      private:
        static constexpr const char* TRUE_STR = "true";
        static constexpr const char* FALSE_STR = "false";

      public:
        FakeRunDex2Oat(ExecVHelper* execv_helper, FakeSystemProperties* properties)
          : RunDex2Oat("/dir/bin/dex2oat", execv_helper), properties_(properties) { }

        virtual ~FakeRunDex2Oat() {}

        virtual std::string GetProperty(const std::string& key,
                                        const std::string& default_value) override {
            if (!properties_) {
                return default_value;
            }
            auto iter = properties_->find(key);
            if (iter == properties_->end()) {
                return default_value;
            }
            return iter->second;
        }

        virtual bool GetBoolProperty(const std::string& key, bool default_value) override {
            std::string value = GetProperty(key, "");
            if (value == "") {
                return default_value;
            }
            return value == TRUE_STR;
        }

      private:
        FakeSystemProperties* properties_;
    };

    struct RunDex2OatArgs {
        static std::unique_ptr<RunDex2OatArgs> MakeDefaultTestArgs() {
            auto args = std::make_unique<RunDex2OatArgs>();
            args->input_dex.reset(ZIP_FD, INPUT_PATH);
            args->output_oat.reset(OAT_FD, OUTPUT_PATH);
            args->input_vdex.reset(INPUT_VDEX_FD, "UNUSED_PATH");
            args->output_vdex.reset(OUTPUT_VDEX_FD, "UNUSED_PATH");
            args->instruction_set = "arm64";
            args->compilation_reason = "rundex2oattest";
            return args;
        }

        UniqueFile output_oat;
        UniqueFile output_vdex;
        UniqueFile output_image;
        UniqueFile input_dex;
        UniqueFile input_vdex;
        UniqueFile dex_metadata;
        UniqueFile profile;
        int swap_fd = -1;
        const char* instruction_set = nullptr;
        const char* compiler_filter = "extract";
        bool debuggable = false;
        bool post_bootcomplete = false;
        bool for_restore = false;
        const char* class_loader_context = nullptr;
        std::string class_loader_context_fds;
        int target_sdk_version = 0;
        bool enable_hidden_api_checks = false;
        bool generate_compact_dex = true;
        bool use_jitzygote_image = false;
        const char* compilation_reason = nullptr;
    };

    class FakeExecVHelper : public ExecVHelper {
      public:
        bool HasArg(const std::string& arg) const {
            auto end = argv_.end() - 1;  // To exclude the terminating nullptr
            return find(argv_.begin(), end, arg) != end;
        }

        bool FlagNotUsed(const std::string& flag) const {
            auto has_prefix = [flag](const char* arg) {
                return strncmp(arg, flag.c_str(), flag.size()) == 0;
            };
            auto end = argv_.end() - 1;  // To exclude the terminating nullptr
            return find_if(argv_.begin(), end, has_prefix) == end;
        }

        virtual void Exec(int exit_code) override {
            std::string cmd;
            for (auto arg : argv_) {
                if (arg == nullptr) {
                  continue;
                }
                cmd += arg;
                cmd += " ";
            }
            LOG(DEBUG) << "FakeExecVHelper exit_code: " << exit_code << " cmd: " << cmd << "\n";
        }
    };

    virtual void SetUp() override {
        execv_helper_.reset(new FakeExecVHelper());
        system_properties_.clear();
        initializeDefaultExpectedFlags();
    }

    // Initializes the default flags expected to a run.  It currently matches to the expected flags
    // with RunDex2OatArgs::MakeDefaultTestArgs.
    //
    // default_expected_flags_ defines a mapping of <flag_name, expected_value>, where flag_name is
    // something like "--flag-name", and expected_value can be "=value" or ":value" (depending on
    // its delimiter), "" (if no value is needed), or a special value of FLAG_UNUSED to indicates
    // that it should not be used.
    void initializeDefaultExpectedFlags() {
        default_expected_flags_.clear();

        // Files
        default_expected_flags_["--zip-fd"] = "=" + std::to_string(ZIP_FD);
        default_expected_flags_["--zip-location"] = "=basename.apk";
        default_expected_flags_["--oat-fd"] = "=" + std::to_string(OAT_FD);
        default_expected_flags_["--oat-location"] = "=" + std::string(OUTPUT_PATH);
        default_expected_flags_["--input-vdex-fd"] = "=" + std::to_string(INPUT_VDEX_FD);
        default_expected_flags_["--output-vdex-fd"] = "=" + std::to_string(OUTPUT_VDEX_FD);
        default_expected_flags_["--classpath-dir"] = "=/dir/input";
        default_expected_flags_["--app-image-fd"] = FLAG_UNUSED;
        default_expected_flags_["--profile-file-fd"] = FLAG_UNUSED;
        default_expected_flags_["--swap-fd"] = FLAG_UNUSED;
        default_expected_flags_["--class-loader-context"] = FLAG_UNUSED;
        default_expected_flags_["--class-loader-context-fds"] = FLAG_UNUSED;

        // Arch
        default_expected_flags_["--instruction-set"] = "=arm64";
        default_expected_flags_["--instruction-set-features"] = FLAG_UNUSED;
        default_expected_flags_["--instruction-set-variant"] = FLAG_UNUSED;
        default_expected_flags_["--cpu-set"] = FLAG_UNUSED;

        // Misc
        default_expected_flags_["--compiler-filter"] = "=extract";
        default_expected_flags_["--compilation-reason"] = "=rundex2oattest";
        default_expected_flags_["--compact-dex-level"] = FLAG_UNUSED;
        default_expected_flags_["-j"] = FLAG_UNUSED;
        default_expected_flags_["--max-image-block-size"] = FLAG_UNUSED;
        default_expected_flags_["--very-large-app-threshold"] = FLAG_UNUSED;
        default_expected_flags_["--resolve-startup-const-strings"] = FLAG_UNUSED;

        // Debug
        default_expected_flags_["--debuggable"] = FLAG_UNUSED;
        default_expected_flags_["--generate-debug-info"] = FLAG_UNUSED;
        default_expected_flags_["--generate-mini-debug-info"] = FLAG_UNUSED;

        // Runtime
        // TODO(victorhsieh): Check if the previous flag is actually --runtime-arg.
        default_expected_flags_["-Xms"] = FLAG_UNUSED;
        default_expected_flags_["-Xmx"] = FLAG_UNUSED;
        default_expected_flags_["-Xbootclasspath"] = FLAG_UNUSED;
        default_expected_flags_["-Xtarget-sdk-version"] = FLAG_UNUSED;
        default_expected_flags_["-Xhidden-api-policy"] = FLAG_UNUSED;
        default_expected_flags_["-Xnorelocate"] = FLAG_UNUSED;

        // Test only
        default_expected_flags_["--foo"] = FLAG_UNUSED;
        default_expected_flags_["--bar"] = FLAG_UNUSED;
        default_expected_flags_["--baz"] = FLAG_UNUSED;
    }

    void SetExpectedFlagUsed(const std::string& flag, const std::string& value) {
        auto iter = default_expected_flags_.find(flag);
        ASSERT_NE(iter, default_expected_flags_.end()) << "Must define the default value";
        iter->second = value;
    }

    void VerifyExpectedFlags() {
        for (auto const& [flag, value] : default_expected_flags_) {
            if (value == FLAG_UNUSED) {
                EXPECT_TRUE(execv_helper_->FlagNotUsed(flag))
                    << "Flag " << flag << " should be unused, but got the value " << value;
            } else if (value == "") {
                EXPECT_TRUE(execv_helper_->HasArg(flag))
                    << "Flag " << flag << " should be specified without value, but got " << value;
            } else {
                EXPECT_TRUE(execv_helper_->HasArg(flag + value))
                    << "Flag " << flag << value << " is not specificed";
            }
        }
    }

    void setSystemProperty(const std::string& key, const std::string& value) {
        system_properties_[key] = value;
    }

    void CallRunDex2Oat(std::unique_ptr<RunDex2OatArgs> args) {
        FakeRunDex2Oat runner(execv_helper_.get(), &system_properties_);
        runner.Initialize(args->output_oat,
                          args->output_vdex,
                          args->output_image,
                          args->input_dex,
                          args->input_vdex,
                          args->dex_metadata,
                          args->profile,
                          args->class_loader_context,
                          args->class_loader_context_fds,
                          args->swap_fd,
                          args->instruction_set,
                          args->compiler_filter,
                          args->debuggable,
                          args->post_bootcomplete,
                          args->for_restore,
                          args->target_sdk_version,
                          args->enable_hidden_api_checks,
                          args->generate_compact_dex,
                          args->use_jitzygote_image,
                          args->compilation_reason);
        runner.Exec(/*exit_code=*/ 0);
    }

  private:
    std::unique_ptr<FakeExecVHelper> execv_helper_;
    std::map<std::string, std::string> default_expected_flags_;
    FakeSystemProperties system_properties_;
};

TEST_F(RunDex2OatTest, BasicInputOutput) {
    auto execv_helper = std::make_unique<FakeExecVHelper>();
    CallRunDex2Oat(RunDex2OatArgs::MakeDefaultTestArgs());

    VerifyExpectedFlags();
}

TEST_F(RunDex2OatTest, WithAllOtherInputFds) {
    auto args = RunDex2OatArgs::MakeDefaultTestArgs();
    args->output_image.reset(IMAGE_FD, "UNUSED_PATH");
    args->profile.reset(PROFILE_FD, "UNUSED_PATH");
    args->swap_fd = SWAP_FD;
    CallRunDex2Oat(std::move(args));

    SetExpectedFlagUsed("--app-image-fd", "=" + std::to_string(IMAGE_FD));
    SetExpectedFlagUsed("--profile-file-fd", "=" + std::to_string(PROFILE_FD));
    SetExpectedFlagUsed("--swap-fd", "=" + std::to_string(SWAP_FD));
    VerifyExpectedFlags();
}

TEST_F(RunDex2OatTest, WithClassLoaderContext) {
    auto args = RunDex2OatArgs::MakeDefaultTestArgs();
    args->class_loader_context = "CLASS_LOADER_CONTEXT";
    CallRunDex2Oat(std::move(args));

    SetExpectedFlagUsed("--class-loader-context", "=CLASS_LOADER_CONTEXT");
    SetExpectedFlagUsed("--class-loader-context-fds", FLAG_UNUSED);
    VerifyExpectedFlags();
}

TEST_F(RunDex2OatTest, WithClassLoaderContextAndFds) {
    auto args = RunDex2OatArgs::MakeDefaultTestArgs();
    args->class_loader_context = "CLASS_LOADER_CONTEXT";
    args->class_loader_context_fds = "CLASS_LOADER_CONTEXT_FDS";
    CallRunDex2Oat(std::move(args));

    SetExpectedFlagUsed("--class-loader-context", "=CLASS_LOADER_CONTEXT");
    SetExpectedFlagUsed("--class-loader-context-fds", "=CLASS_LOADER_CONTEXT_FDS");
    VerifyExpectedFlags();
}

TEST_F(RunDex2OatTest, WithOnlyClassLoaderContextFds) {
    auto args = RunDex2OatArgs::MakeDefaultTestArgs();
    args->class_loader_context_fds = "CLASS_LOADER_CONTEXT_FDS";
    CallRunDex2Oat(std::move(args));

    SetExpectedFlagUsed("--class-loader-context", FLAG_UNUSED);
    SetExpectedFlagUsed("--class-loader-context-fds", FLAG_UNUSED);
    VerifyExpectedFlags();
}

TEST_F(RunDex2OatTest, DoNotGenerateCompactDex) {
    auto args = RunDex2OatArgs::MakeDefaultTestArgs();
    args->generate_compact_dex = false;
    CallRunDex2Oat(std::move(args));

    SetExpectedFlagUsed("--compact-dex-level", "=none");
    VerifyExpectedFlags();
}

TEST_F(RunDex2OatTest, DoNotGenerateCompactDexWithVdexInPlaceUpdate) {
    auto args = RunDex2OatArgs::MakeDefaultTestArgs();
    args->generate_compact_dex = true;
    args->input_vdex.reset(INPUT_VDEX_FD, "UNUSED_PATH");
    args->output_vdex.reset(INPUT_VDEX_FD, "UNUSED_PATH");
    CallRunDex2Oat(std::move(args));

    SetExpectedFlagUsed("--compact-dex-level", "=none");
    SetExpectedFlagUsed("--output-vdex-fd", "=" + std::to_string(INPUT_VDEX_FD));
    VerifyExpectedFlags();
}

TEST_F(RunDex2OatTest, ISA) {
    setSystemProperty("dalvik.vm.isa.x86.features", "a-x86-feature");
    setSystemProperty("dalvik.vm.isa.x86.variant", "a-x86-variant");
    auto args = RunDex2OatArgs::MakeDefaultTestArgs();
    args->instruction_set = "x86";
    CallRunDex2Oat(std::move(args));

    SetExpectedFlagUsed("--instruction-set", "=x86");
    SetExpectedFlagUsed("--instruction-set-features", "=a-x86-feature");
    SetExpectedFlagUsed("--instruction-set-variant", "=a-x86-variant");
    VerifyExpectedFlags();
}

TEST_F(RunDex2OatTest, CpuSetPreBootComplete) {
    setSystemProperty("dalvik.vm.boot-dex2oat-cpu-set", "1,2");
    auto args = RunDex2OatArgs::MakeDefaultTestArgs();
    args->post_bootcomplete = false;
    CallRunDex2Oat(std::move(args));

    SetExpectedFlagUsed("--cpu-set", "=1,2");
    VerifyExpectedFlags();
}

TEST_F(RunDex2OatTest, CpuSetPostBootCompleteNotForRestore) {
    setSystemProperty("dalvik.vm.dex2oat-cpu-set", "1,2");
    auto args = RunDex2OatArgs::MakeDefaultTestArgs();
    args->post_bootcomplete = true;
    args->for_restore = false;
    CallRunDex2Oat(std::move(args));

    SetExpectedFlagUsed("--cpu-set", "=1,2");
    VerifyExpectedFlags();
}

TEST_F(RunDex2OatTest, CpuSetPostBootCompleteForRestore) {
    setSystemProperty("dalvik.vm.restore-dex2oat-cpu-set", "1,2");
    setSystemProperty("dalvik.vm.dex2oat-cpu-set", "2,3");
    auto args = RunDex2OatArgs::MakeDefaultTestArgs();
    args->post_bootcomplete = true;
    args->for_restore = true;
    CallRunDex2Oat(std::move(args));

    SetExpectedFlagUsed("--cpu-set", "=1,2");
    VerifyExpectedFlags();
}

TEST_F(RunDex2OatTest, CpuSetPostBootCompleteForRestore_Backup) {
    setSystemProperty("dalvik.vm.restore-dex2oat-cpu-set", "");
    setSystemProperty("dalvik.vm.dex2oat-cpu-set", "1,2");
    auto args = RunDex2OatArgs::MakeDefaultTestArgs();
    args->post_bootcomplete = true;
    args->for_restore = true;
    CallRunDex2Oat(std::move(args));

    SetExpectedFlagUsed("--cpu-set", "=1,2");
    VerifyExpectedFlags();
}

TEST_F(RunDex2OatTest, Runtime) {
    setSystemProperty("dalvik.vm.dex2oat-Xms", "1234m");
    setSystemProperty("dalvik.vm.dex2oat-Xmx", "5678m");
    auto args = RunDex2OatArgs::MakeDefaultTestArgs();
    args->target_sdk_version = 30;
    args->enable_hidden_api_checks = true;
    CallRunDex2Oat(std::move(args));

    SetExpectedFlagUsed("-Xms", "1234m");
    SetExpectedFlagUsed("-Xmx", "5678m");
    SetExpectedFlagUsed("-Xtarget-sdk-version", ":30");
    SetExpectedFlagUsed("-Xhidden-api-policy", ":enabled");
    SetExpectedFlagUsed("-Xnorelocate", FLAG_UNUSED);
    VerifyExpectedFlags();
}

TEST_F(RunDex2OatTest, SkipRelocationInMinFramework) {
    setSystemProperty("vold.decrypt", "trigger_restart_min_framework");
    CallRunDex2Oat(RunDex2OatArgs::MakeDefaultTestArgs());

    SetExpectedFlagUsed("--compiler-filter", "=extract");
    SetExpectedFlagUsed("-Xnorelocate", "");
    VerifyExpectedFlags();
}

TEST_F(RunDex2OatTest, SkipRelocationIfDecryptedWithFullDiskEncryption) {
    setSystemProperty("vold.decrypt", "1");
    CallRunDex2Oat(RunDex2OatArgs::MakeDefaultTestArgs());

    SetExpectedFlagUsed("--compiler-filter", "=extract");
    SetExpectedFlagUsed("-Xnorelocate", "");
    VerifyExpectedFlags();
}

TEST_F(RunDex2OatTest, DalvikVmDex2oatFilter) {
    setSystemProperty("dalvik.vm.dex2oat-filter", "speed");
    auto args = RunDex2OatArgs::MakeDefaultTestArgs();
    args->compiler_filter = nullptr;
    CallRunDex2Oat(std::move(args));

    SetExpectedFlagUsed("--compiler-filter", "=speed");
    VerifyExpectedFlags();
}

TEST_F(RunDex2OatTest, ResolveStartupStartings) {
    setSystemProperty("dalvik.vm.dex2oat-resolve-startup-strings", "false");
    CallRunDex2Oat(RunDex2OatArgs::MakeDefaultTestArgs());

    SetExpectedFlagUsed("--resolve-startup-const-strings", "=false");
    VerifyExpectedFlags();
}

TEST_F(RunDex2OatTest, ResolveStartupStartingsOverride) {
    setSystemProperty("dalvik.vm.dex2oat-resolve-startup-strings", "false");
    setSystemProperty("persist.device_config.runtime.dex2oat_resolve_startup_strings", "true");
    CallRunDex2Oat(RunDex2OatArgs::MakeDefaultTestArgs());

    SetExpectedFlagUsed("--resolve-startup-const-strings", "=true");
    VerifyExpectedFlags();
}

TEST_F(RunDex2OatTest, ThreadsPreBootComplete) {
    setSystemProperty("dalvik.vm.boot-dex2oat-threads", "2");
    auto args = RunDex2OatArgs::MakeDefaultTestArgs();
    args->post_bootcomplete = false;
    CallRunDex2Oat(std::move(args));

    SetExpectedFlagUsed("-j", "2");
    VerifyExpectedFlags();
}

TEST_F(RunDex2OatTest, ThreadsPostBootCompleteNotForRestore) {
    setSystemProperty("dalvik.vm.dex2oat-threads", "3");
    auto args = RunDex2OatArgs::MakeDefaultTestArgs();
    args->post_bootcomplete = true;
    args->for_restore = false;
    CallRunDex2Oat(std::move(args));

    SetExpectedFlagUsed("-j", "3");
    VerifyExpectedFlags();
}

TEST_F(RunDex2OatTest, ThreadsPostBootCompleteForRestore) {
    setSystemProperty("dalvik.vm.restore-dex2oat-threads", "4");
    setSystemProperty("dalvik.vm.dex2oat-threads", "5");
    auto args = RunDex2OatArgs::MakeDefaultTestArgs();
    args->post_bootcomplete = true;
    args->for_restore = true;
    CallRunDex2Oat(std::move(args));

    SetExpectedFlagUsed("-j", "4");
    VerifyExpectedFlags();
}

TEST_F(RunDex2OatTest, ThreadsPostBootCompleteForRestore_Backup) {
    setSystemProperty("dalvik.vm.restore-dex2oat-threads", "");
    setSystemProperty("dalvik.vm.dex2oat-threads", "5");
    auto args = RunDex2OatArgs::MakeDefaultTestArgs();
    args->post_bootcomplete = true;
    args->for_restore = true;
    CallRunDex2Oat(std::move(args));

    SetExpectedFlagUsed("-j", "5");
    VerifyExpectedFlags();
}

TEST_F(RunDex2OatTest, Debuggable) {
    auto args = RunDex2OatArgs::MakeDefaultTestArgs();
    args->debuggable = true;
    CallRunDex2Oat(std::move(args));

    SetExpectedFlagUsed("--debuggable", "");
    VerifyExpectedFlags();
}

TEST_F(RunDex2OatTest, AlwaysDebuggable) {
    setSystemProperty("dalvik.vm.always_debuggable", "1");
    CallRunDex2Oat(RunDex2OatArgs::MakeDefaultTestArgs());

    SetExpectedFlagUsed("--debuggable", "");
    VerifyExpectedFlags();
}

TEST_F(RunDex2OatTest, GenerateDebugInfo) {
    setSystemProperty("debug.generate-debug-info", "true");
    CallRunDex2Oat(RunDex2OatArgs::MakeDefaultTestArgs());

    SetExpectedFlagUsed("--generate-debug-info", "");
    VerifyExpectedFlags();
}

TEST_F(RunDex2OatTest, HiddenApiCheck) {
    auto args = RunDex2OatArgs::MakeDefaultTestArgs();
    args->enable_hidden_api_checks = true;
    CallRunDex2Oat(std::move(args));

    SetExpectedFlagUsed("-Xhidden-api-policy", ":enabled");
    VerifyExpectedFlags();
}

TEST_F(RunDex2OatTest, Misc) {
    setSystemProperty("dalvik.vm.dex2oat-max-image-block-size", "524288");
    setSystemProperty("dalvik.vm.dex2oat-very-large", "100000");
    CallRunDex2Oat(RunDex2OatArgs::MakeDefaultTestArgs());

    SetExpectedFlagUsed("--max-image-block-size", "=524288");
    SetExpectedFlagUsed("--very-large-app-threshold", "=100000");
    VerifyExpectedFlags();
}

TEST_F(RunDex2OatTest, ExtraFlags) {
    setSystemProperty("dalvik.vm.dex2oat-flags", "--foo=123 --bar:456 --baz");
    CallRunDex2Oat(RunDex2OatArgs::MakeDefaultTestArgs());

    SetExpectedFlagUsed("--foo", "=123");
    SetExpectedFlagUsed("--bar", ":456");
    SetExpectedFlagUsed("--baz", "");
    VerifyExpectedFlags();
}

}  // namespace installd
}  // namespace android
