//===-- Unittests for WrapperGen ------------------------------------------===//
//
// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
//
//===----------------------------------------------------------------------===//

#include "llvm/ADT/SmallString.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/ADT/Twine.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/Error.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/FileUtilities.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/Program.h"
#include "llvm/Support/raw_ostream.h"
#include "gmock/gmock.h"
#include "gtest/gtest.h"
#include <unistd.h>

llvm::cl::opt<std::string>
    LibcPath("path", llvm::cl::desc("Path to the top level libc directory."),
             llvm::cl::value_desc("<path to libc>"), llvm::cl::Required);
llvm::cl::opt<std::string>
    ToolPath("tool", llvm::cl::desc("Path to the tool executable."),
             llvm::cl::value_desc("<path to tool>"), llvm::cl::Required);
llvm::cl::opt<std::string>
    APIPath("api",
            llvm::cl::desc("Path to the api tablegen file used by the tests."),
            llvm::cl::value_desc("<path to testapi.td>"), llvm::cl::Required);

class WrapperGenTest : public ::testing::Test {
public:
  std::string IncludeArg;
  std::string APIArg;
  llvm::StringRef ProgPath;
  llvm::Expected<llvm::sys::fs::TempFile> STDOutFile =
      llvm::sys::fs::TempFile::create("wrappergen-stdout-%%-%%-%%-%%.txt");
  llvm::Expected<llvm::sys::fs::TempFile> STDErrFile =
      llvm::sys::fs::TempFile::create("wrappergen-stderr-%%-%%-%%-%%.txt");

protected:
  void SetUp() override {
    IncludeArg = "-I=";
    IncludeArg.append(LibcPath);
    APIArg = APIPath;
    ProgPath = llvm::StringRef(ToolPath);

    if (!STDOutFile) {
      llvm::errs() << "Error: " << llvm::toString(STDOutFile.takeError())
                   << "\n";
      llvm::report_fatal_error(
          "Temporary file failed to initialize for libc-wrappergen tests.");
    }
    if (!STDErrFile) {
      llvm::errs() << "Error: " << llvm::toString(STDErrFile.takeError())
                   << "\n";
      llvm::report_fatal_error(
          "Temporary file failed to initialize for libc-wrappergen tests.");
    }
  }
  void TearDown() override {
    llvm::consumeError(STDOutFile.get().discard());
    llvm::consumeError(STDErrFile.get().discard());
  }
};

using LlvmLibcWrapperGenTest = WrapperGenTest;

TEST_F(LlvmLibcWrapperGenTest, RunWrapperGenAndGetNoErrors) {
  llvm::Optional<llvm::StringRef> Redirects[] = {
      llvm::None, llvm::StringRef(STDOutFile.get().TmpName),
      llvm::StringRef(STDErrFile.get().TmpName)};

  llvm::StringRef ArgV[] = {ProgPath,
                            llvm::StringRef(IncludeArg),
                            llvm::StringRef(APIArg),
                            "--gen-wrapper",
                            "--name",
                            "strlen"};

  int ExitCode =
      llvm::sys::ExecuteAndWait(ProgPath, ArgV, llvm::None, Redirects);

  EXPECT_EQ(ExitCode, 0);

  auto STDErrOrError = llvm::MemoryBuffer::getFile(STDErrFile.get().TmpName);
  std::string STDErrOutput = STDErrOrError.get()->getBuffer().str();
  ASSERT_EQ(STDErrOutput, "");
}

TEST_F(LlvmLibcWrapperGenTest, RunWrapperGenOnStrlen) {
  llvm::Optional<llvm::StringRef> Redirects[] = {
      llvm::None, llvm::StringRef(STDOutFile.get().TmpName),
      llvm::StringRef(STDErrFile.get().TmpName)};

  llvm::StringRef ArgV[] = {ProgPath,
                            llvm::StringRef(IncludeArg),
                            llvm::StringRef(APIArg),
                            "--gen-wrapper",
                            "--name",
                            "strlen"};

  int ExitCode =
      llvm::sys::ExecuteAndWait(ProgPath, ArgV, llvm::None, Redirects);

  EXPECT_EQ(ExitCode, 0);

  auto STDErrOrError = llvm::MemoryBuffer::getFile(STDErrFile.get().TmpName);
  std::string STDErrOutput = STDErrOrError.get()->getBuffer().str();

  ASSERT_EQ(STDErrOutput, "");

  auto STDOutOrError = llvm::MemoryBuffer::getFile(STDOutFile.get().TmpName);
  std::string STDOutOutput = STDOutOrError.get()->getBuffer().str();

  ASSERT_EQ(STDOutOutput, "#include \"src/string/strlen.h\"\n"
                          "extern \"C\" size_t strlen(const char * __arg0) {\n"
                          "  return __llvm_libc::strlen(__arg0);\n"
                          "}\n");
  // TODO:(michaelrj) Figure out how to make this output comparison
  // less brittle. Currently it's just comparing the output of the program
  // to an exact string, this means that even a small formatting change
  // would break this test.
}

TEST_F(LlvmLibcWrapperGenTest, GenAliasForStrlen) {
  llvm::Optional<llvm::StringRef> Redirects[] = {
      llvm::None, llvm::StringRef(STDOutFile.get().TmpName),
      llvm::StringRef(STDErrFile.get().TmpName)};

  llvm::StringRef ArgV[] = {ProgPath,
                            llvm::StringRef(IncludeArg),
                            llvm::StringRef(APIArg),
                            "--gen-alias",
                            "--mangled-name",
                            "__llvm_libc_strlen_mangled_name",
                            "--name",
                            "strlen"};

  int ExitCode =
      llvm::sys::ExecuteAndWait(ProgPath, ArgV, llvm::None, Redirects);

  EXPECT_EQ(ExitCode, 0);

  auto STDErrOrError = llvm::MemoryBuffer::getFile(STDErrFile.get().TmpName);
  std::string STDErrOutput = STDErrOrError.get()->getBuffer().str();

  ASSERT_EQ(STDErrOutput, "");

  auto STDOutOrError = llvm::MemoryBuffer::getFile(STDOutFile.get().TmpName);
  std::string STDOutOutput = STDOutOrError.get()->getBuffer().str();

  ASSERT_EQ(STDOutOutput,
            "extern \"C\" size_t strlen(const char * __arg0) "
            "__attribute__((alias(\"__llvm_libc_strlen_mangled_name\")));\n");
  // TODO:(michaelrj) Figure out how to make this output comparison
  // less brittle. Currently it's just comparing the output of the program
  // to an exact string, this means that even a small formatting change
  // would break this test.
}

TEST_F(LlvmLibcWrapperGenTest, DeclStrlenAliasUsingMangledNameFile) {
  llvm::Optional<llvm::StringRef> Redirects[] = {
      llvm::None, llvm::StringRef(STDOutFile.get().TmpName),
      llvm::StringRef(STDErrFile.get().TmpName)};

  const char *MangledNameFileContent =
      "abc\nxyz__llvm_libc_strlen_mangled_name\nijk\n";
  llvm::SmallVector<char> MangledNameFilePath;
  auto MangledNameFileCreateError = llvm::sys::fs::createUniqueFile(
      "libc-wrappergen-test-aliasee-file-%%-%%-%%-%%.txt", MangledNameFilePath);
  ASSERT_FALSE(MangledNameFileCreateError);
  auto MangledNameFileWriteError = llvm::writeFileAtomically(
      "libc-wrappergen-temp-test-aliasee-file-%%-%%-%%-%%.txt",
      llvm::StringRef(MangledNameFilePath.data()),
      llvm::StringRef(MangledNameFileContent));
  ASSERT_FALSE(MangledNameFileWriteError);

  llvm::StringRef ArgV[] = {ProgPath,
                            llvm::StringRef(IncludeArg),
                            llvm::StringRef(APIArg),
                            "--gen-alias",
                            "--mangled-name-file",
                            llvm::StringRef(MangledNameFilePath.data()),
                            "--name",
                            "strlen"};

  int ExitCode =
      llvm::sys::ExecuteAndWait(ProgPath, ArgV, llvm::None, Redirects);

  EXPECT_EQ(ExitCode, 0);

  auto STDErrOrError = llvm::MemoryBuffer::getFile(STDErrFile.get().TmpName);
  std::string STDErrOutput = STDErrOrError.get()->getBuffer().str();

  ASSERT_EQ(STDErrOutput, "");

  auto STDOutOrError = llvm::MemoryBuffer::getFile(STDOutFile.get().TmpName);
  std::string STDOutOutput = STDOutOrError.get()->getBuffer().str();

  ASSERT_EQ(
      STDOutOutput,
      "extern \"C\" size_t strlen(const char * __arg0) "
      "__attribute__((alias(\"xyz__llvm_libc_strlen_mangled_name\")));\n");
}

/////////////////////////////////////////////////////////////////////
/////////////////////////////////////////////////////////////////////
// BAD INPUT TESTS
// all of the tests after this point are testing inputs that should
// return errors
/////////////////////////////////////////////////////////////////////

TEST_F(LlvmLibcWrapperGenTest,
       RunWrapperGenOnStrlenWithMangledNameAndMangledNameFile) {
  llvm::Optional<llvm::StringRef> Redirects[] = {
      llvm::None, llvm::StringRef(STDOutFile.get().TmpName),
      llvm::StringRef(STDErrFile.get().TmpName)};

  llvm::StringRef ArgV[] = {ProgPath,
                            llvm::StringRef(IncludeArg),
                            llvm::StringRef(APIArg),
                            "--gen-alias",
                            "--mangled-name",
                            "__llvm_libc_strlen_mangled_name",
                            "--mangled-name-file",
                            "non-existant-mangled-name-file.txt",
                            "--name",
                            "strlen"};

  int ExitCode =
      llvm::sys::ExecuteAndWait(ProgPath, ArgV, llvm::None, Redirects);

  EXPECT_EQ(ExitCode, 1);

  auto STDErrOrError = llvm::MemoryBuffer::getFile(STDErrFile.get().TmpName);
  std::string STDErrOutput = STDErrOrError.get()->getBuffer().str();

  ASSERT_EQ(STDErrOutput,
            "error: The options 'mangled-name' and 'mangled-name-file' "
            "cannot be specified simultaneously.\n");

  auto STDOutOrError = llvm::MemoryBuffer::getFile(STDOutFile.get().TmpName);
  std::string STDOutOutput = STDOutOrError.get()->getBuffer().str();

  ASSERT_EQ(STDOutOutput, "");
}

TEST_F(LlvmLibcWrapperGenTest, RunWrapperGenOnBadFuncName) {
  llvm::Optional<llvm::StringRef> Redirects[] = {
      llvm::None, llvm::StringRef(STDOutFile.get().TmpName),
      llvm::StringRef(STDErrFile.get().TmpName)};

  llvm::StringRef BadFuncName = "FAKE_TEST_FUNC";

  llvm::StringRef ArgV[] = {ProgPath,
                            llvm::StringRef(IncludeArg),
                            llvm::StringRef(APIArg),
                            "--gen-wrapper",
                            "--name",
                            BadFuncName};

  int ExitCode =
      llvm::sys::ExecuteAndWait(ProgPath, ArgV, llvm::None, Redirects);

  EXPECT_EQ(ExitCode, 1);

  auto STDErrOrError = llvm::MemoryBuffer::getFile(STDErrFile.get().TmpName);
  std::string STDErrOutput = STDErrOrError.get()->getBuffer().str();

  ASSERT_EQ(STDErrOutput, ("error: Function '" + BadFuncName +
                           "' not found in any standard spec.\n")
                              .str());

  auto STDOutOrError = llvm::MemoryBuffer::getFile(STDOutFile.get().TmpName);
  std::string STDOutOutput = STDOutOrError.get()->getBuffer().str();

  ASSERT_EQ(STDOutOutput, "");
}

TEST_F(LlvmLibcWrapperGenTest, RunWrapperGenOnStrlenWithBadMangledNameFile) {
  llvm::Optional<llvm::StringRef> Redirects[] = {
      llvm::None, llvm::StringRef(STDOutFile.get().TmpName),
      llvm::StringRef(STDErrFile.get().TmpName)};

  llvm::StringRef BadMangledNameFileName = "FILE_THAT_DOESNT_EXIST.txt";

  llvm::StringRef ArgV[] = {ProgPath,
                            llvm::StringRef(IncludeArg),
                            llvm::StringRef(APIArg),
                            "--gen-alias",
                            "--mangled-name-file",
                            BadMangledNameFileName,
                            "--name",
                            "strlen"};

  int ExitCode =
      llvm::sys::ExecuteAndWait(ProgPath, ArgV, llvm::None, Redirects);

  EXPECT_EQ(ExitCode, 1);

  auto STDErrOrError = llvm::MemoryBuffer::getFile(STDErrFile.get().TmpName);
  std::string STDErrOutput = STDErrOrError.get()->getBuffer().str();

  ASSERT_EQ(STDErrOutput, ("error: Unable to read the mangled name file " +
                           BadMangledNameFileName + "\n")
                              .str());

  auto STDOutOrError = llvm::MemoryBuffer::getFile(STDOutFile.get().TmpName);
  std::string STDOutOutput = STDOutOrError.get()->getBuffer().str();

  ASSERT_EQ(STDOutOutput, "");
}

TEST_F(LlvmLibcWrapperGenTest, RunWithMangledNameFileMissingLLVMLibcName) {
  llvm::Optional<llvm::StringRef> Redirects[] = {
      llvm::None, llvm::StringRef(STDOutFile.get().TmpName),
      llvm::StringRef(STDErrFile.get().TmpName)};

  llvm::SmallVector<char> MangledNameFilePath;
  auto MangledNameFileCreateError = llvm::sys::fs::createUniqueFile(
      "libc-wrappergen-test-mangled-name-file-%%-%%-%%-%%.txt",
      MangledNameFilePath);
  ASSERT_FALSE(MangledNameFileCreateError);

  llvm::StringRef ArgV[] = {ProgPath,
                            llvm::StringRef(IncludeArg),
                            llvm::StringRef(APIArg),
                            "--gen-alias",
                            "--mangled-name-file",
                            llvm::StringRef(MangledNameFilePath.data()),
                            "--name",
                            "strlen"};

  int ExitCode =
      llvm::sys::ExecuteAndWait(ProgPath, ArgV, llvm::None, Redirects);

  EXPECT_NE(ExitCode, 0);

  auto STDErrOrError = llvm::MemoryBuffer::getFile(STDErrFile.get().TmpName);
  std::string STDErrOutput = STDErrOrError.get()->getBuffer().str();

  ASSERT_EQ(STDErrOutput, ("error: Did not find an LLVM libc mangled name in " +
                           MangledNameFilePath + "\n")
                              .str());
}
