//===-- LogTest.cpp -------------------------------------------------------===//
//
// 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 "gmock/gmock.h"
#include "gtest/gtest.h"

#include "lldb/Utility/Log.h"
#include "lldb/Utility/StreamString.h"
#include "llvm/ADT/BitmaskEnum.h"
#include "llvm/Support/ManagedStatic.h"
#include "llvm/Support/Threading.h"
#include <thread>

using namespace lldb;
using namespace lldb_private;

enum class TestChannel : Log::MaskType {
  FOO = Log::ChannelFlag<0>,
  BAR = Log::ChannelFlag<1>,
  LLVM_MARK_AS_BITMASK_ENUM(BAR),
};

LLVM_ENABLE_BITMASK_ENUMS_IN_NAMESPACE();

static constexpr Log::Category test_categories[] = {
    {{"foo"}, {"log foo"}, TestChannel::FOO},
    {{"bar"}, {"log bar"}, TestChannel::BAR},
};

static Log::Channel test_channel(test_categories, TestChannel::FOO);

namespace lldb_private {
template <> Log::Channel &LogChannelFor<TestChannel>() { return test_channel; }
} // namespace lldb_private

// Wrap enable, disable and list functions to make them easier to test.
static bool EnableChannel(std::shared_ptr<LogHandler> log_handler_sp,
                          uint32_t log_options, llvm::StringRef channel,
                          llvm::ArrayRef<const char *> categories,
                          std::string &error) {
  error.clear();
  llvm::raw_string_ostream error_stream(error);
  return Log::EnableLogChannel(log_handler_sp, log_options, channel, categories,
                               error_stream);
}

static bool DisableChannel(llvm::StringRef channel,
                           llvm::ArrayRef<const char *> categories,
                           std::string &error) {
  error.clear();
  llvm::raw_string_ostream error_stream(error);
  return Log::DisableLogChannel(channel, categories, error_stream);
}

static bool ListCategories(llvm::StringRef channel, std::string &result) {
  result.clear();
  llvm::raw_string_ostream result_stream(result);
  return Log::ListChannelCategories(channel, result_stream);
}

namespace {
// A test fixture which provides tests with a pre-registered channel.
struct LogChannelTest : public ::testing::Test {
  void TearDown() override { Log::DisableAllLogChannels(); }

  static void SetUpTestCase() { Log::Register("chan", test_channel); }

  static void TearDownTestCase() {
    Log::Unregister("chan");
    llvm::llvm_shutdown();
  }
};

class TestLogHandler : public LogHandler {
public:
  TestLogHandler() : m_messages(), m_stream(m_messages) {}

  void Emit(llvm::StringRef message) override { m_stream << message; }

  llvm::SmallString<0> m_messages;
  llvm::raw_svector_ostream m_stream;
};

// A test fixture which provides tests with a pre-registered and pre-enabled
// channel. Additionally, the messages written to that channel are captured and
// made available via getMessage().
class LogChannelEnabledTest : public LogChannelTest {
  std::shared_ptr<TestLogHandler> m_log_handler_sp =
      std::make_shared<TestLogHandler>();
  Log *m_log;
  size_t m_consumed_bytes = 0;

protected:
  std::shared_ptr<LogHandler> getLogHandler() { return m_log_handler_sp; }
  Log *getLog() { return m_log; }
  llvm::StringRef takeOutput();
  llvm::StringRef logAndTakeOutput(llvm::StringRef Message);

public:
  void SetUp() override;
};

static std::string GetDumpAsString(const RotatingLogHandler &handler) {
  std::string buffer;
  llvm::raw_string_ostream stream(buffer);
  handler.Dump(stream);
  return buffer;
}
} // end anonymous namespace

void LogChannelEnabledTest::SetUp() {
  LogChannelTest::SetUp();

  std::string error;
  ASSERT_TRUE(EnableChannel(m_log_handler_sp, 0, "chan", {}, error));

  m_log = GetLog(TestChannel::FOO);
  ASSERT_NE(nullptr, m_log);
}

llvm::StringRef LogChannelEnabledTest::takeOutput() {
  llvm::StringRef result =
      m_log_handler_sp->m_stream.str().drop_front(m_consumed_bytes);
  m_consumed_bytes += result.size();
  return result;
}

llvm::StringRef
LogChannelEnabledTest::logAndTakeOutput(llvm::StringRef Message) {
  LLDB_LOG(m_log, "{0}", Message);
  return takeOutput();
}

TEST(LogTest, LLDB_LOG_nullptr) {
  Log *log = nullptr;
  LLDB_LOG(log, "{0}", 0); // Shouldn't crash
}

TEST(LogTest, Register) {
  llvm::llvm_shutdown_obj obj;
  Log::Register("chan", test_channel);
  Log::Unregister("chan");
  Log::Register("chan", test_channel);
  Log::Unregister("chan");
}

TEST(LogTest, Unregister) {
  llvm::llvm_shutdown_obj obj;
  Log::Register("chan", test_channel);
  EXPECT_EQ(nullptr, GetLog(TestChannel::FOO));
  std::string message;
  auto log_handler_sp = std::make_shared<TestLogHandler>();
  EXPECT_TRUE(
      Log::EnableLogChannel(log_handler_sp, 0, "chan", {"foo"}, llvm::nulls()));
  EXPECT_NE(nullptr, GetLog(TestChannel::FOO));
  Log::Unregister("chan");
  EXPECT_EQ(nullptr, GetLog(TestChannel::FOO));
}

namespace {
static char test_baton;
static size_t callback_count = 0;
static void TestCallback(const char *data, void *baton) {
  EXPECT_STREQ("Foobar", data);
  EXPECT_EQ(&test_baton, baton);
  ++callback_count;
}
} // namespace

TEST(LogTest, CallbackLogHandler) {
  CallbackLogHandler handler(TestCallback, &test_baton);
  handler.Emit("Foobar");
  EXPECT_EQ(1u, callback_count);
}

TEST(LogHandlerTest, RotatingLogHandler) {
  RotatingLogHandler handler(3);

  handler.Emit("foo");
  handler.Emit("bar");
  EXPECT_EQ(GetDumpAsString(handler), "foobar");

  handler.Emit("baz");
  handler.Emit("qux");
  EXPECT_EQ(GetDumpAsString(handler), "barbazqux");

  handler.Emit("quux");
  EXPECT_EQ(GetDumpAsString(handler), "bazquxquux");
}

TEST_F(LogChannelTest, Enable) {
  EXPECT_EQ(nullptr, GetLog(TestChannel::FOO));
  std::string message;
  auto log_handler_sp = std::make_shared<TestLogHandler>();
  std::string error;
  ASSERT_FALSE(EnableChannel(log_handler_sp, 0, "chanchan", {}, error));
  EXPECT_EQ("Invalid log channel 'chanchan'.\n", error);

  EXPECT_TRUE(EnableChannel(log_handler_sp, 0, "chan", {}, error));
  EXPECT_NE(nullptr, GetLog(TestChannel::FOO));
  EXPECT_EQ(nullptr, GetLog(TestChannel::BAR));
  EXPECT_NE(nullptr, GetLog(TestChannel::FOO | TestChannel::BAR));

  EXPECT_TRUE(EnableChannel(log_handler_sp, 0, "chan", {"bar"}, error));
  EXPECT_NE(nullptr, GetLog(TestChannel::FOO));
  EXPECT_NE(nullptr, GetLog(TestChannel::BAR));

  EXPECT_TRUE(EnableChannel(log_handler_sp, 0, "chan", {"baz"}, error));
  EXPECT_NE(std::string::npos, error.find("unrecognized log category 'baz'"))
      << "error: " << error;
}

TEST_F(LogChannelTest, EnableOptions) {
  EXPECT_EQ(nullptr, GetLog(TestChannel::FOO));
  std::string message;
  auto log_handler_sp = std::make_shared<TestLogHandler>();
  std::string error;
  EXPECT_TRUE(EnableChannel(log_handler_sp, LLDB_LOG_OPTION_VERBOSE, "chan", {},
                            error));

  Log *log = GetLog(TestChannel::FOO);
  ASSERT_NE(nullptr, log);
  EXPECT_TRUE(log->GetVerbose());
}

TEST_F(LogChannelTest, Disable) {
  EXPECT_EQ(nullptr, GetLog(TestChannel::FOO));
  std::string message;
  auto log_handler_sp = std::make_shared<TestLogHandler>();
  std::string error;
  EXPECT_TRUE(EnableChannel(log_handler_sp, 0, "chan", {"foo", "bar"}, error));
  EXPECT_NE(nullptr, GetLog(TestChannel::FOO));
  EXPECT_NE(nullptr, GetLog(TestChannel::BAR));

  EXPECT_TRUE(DisableChannel("chan", {"bar"}, error));
  EXPECT_NE(nullptr, GetLog(TestChannel::FOO));
  EXPECT_EQ(nullptr, GetLog(TestChannel::BAR));

  EXPECT_TRUE(DisableChannel("chan", {"baz"}, error));
  EXPECT_NE(std::string::npos, error.find("unrecognized log category 'baz'"))
      << "error: " << error;
  EXPECT_NE(nullptr, GetLog(TestChannel::FOO));
  EXPECT_EQ(nullptr, GetLog(TestChannel::BAR));

  EXPECT_TRUE(DisableChannel("chan", {}, error));
  EXPECT_EQ(nullptr, GetLog(TestChannel::FOO | TestChannel::BAR));
}

TEST_F(LogChannelTest, List) {
  std::string list;
  EXPECT_TRUE(ListCategories("chan", list));
  std::string expected =
      R"(Logging categories for 'chan':
  all - all available logging categories
  default - default set of logging categories
  foo - log foo
  bar - log bar
)";
  EXPECT_EQ(expected, list);

  EXPECT_FALSE(ListCategories("chanchan", list));
  EXPECT_EQ("Invalid log channel 'chanchan'.\n", list);
}

TEST_F(LogChannelEnabledTest, log_options) {
  std::string Err;
  EXPECT_EQ("Hello World\n", logAndTakeOutput("Hello World"));
  EXPECT_TRUE(EnableChannel(getLogHandler(), 0, "chan", {}, Err));
  EXPECT_EQ("Hello World\n", logAndTakeOutput("Hello World"));

  {
    EXPECT_TRUE(EnableChannel(getLogHandler(), LLDB_LOG_OPTION_PREPEND_SEQUENCE,
                              "chan", {}, Err));
    llvm::StringRef Msg = logAndTakeOutput("Hello World");
    int seq_no;
    EXPECT_EQ(1, sscanf(Msg.str().c_str(), "%d Hello World", &seq_no));
  }

  {
    EXPECT_TRUE(EnableChannel(getLogHandler(),
                              LLDB_LOG_OPTION_PREPEND_FILE_FUNCTION, "chan", {},
                              Err));
    llvm::StringRef Msg = logAndTakeOutput("Hello World");
    char File[12];
    char Function[17];

    sscanf(Msg.str().c_str(),
           "%[^:]:%s                                 Hello World", File,
           Function);
    EXPECT_STRCASEEQ("LogTest.cpp", File);
    EXPECT_STREQ("logAndTakeOutput", Function);
  }

  EXPECT_TRUE(EnableChannel(getLogHandler(),
                            LLDB_LOG_OPTION_PREPEND_PROC_AND_THREAD, "chan", {},
                            Err));
  EXPECT_EQ(llvm::formatv("[{0,0+4}/{1,0+4}] Hello World\n", ::getpid(),
                          llvm::get_threadid())
                .str(),
            logAndTakeOutput("Hello World"));
}

TEST_F(LogChannelEnabledTest, LLDB_LOG_ERROR) {
  LLDB_LOG_ERROR(getLog(), llvm::Error::success(), "Foo failed: {0}");
  ASSERT_EQ("", takeOutput());

  LLDB_LOG_ERROR(getLog(),
                 llvm::make_error<llvm::StringError>(
                     "My Error", llvm::inconvertibleErrorCode()),
                 "Foo failed: {0}");
  ASSERT_EQ("Foo failed: My Error\n", takeOutput());

  // Doesn't log, but doesn't assert either
  LLDB_LOG_ERROR(nullptr,
                 llvm::make_error<llvm::StringError>(
                     "My Error", llvm::inconvertibleErrorCode()),
                 "Foo failed: {0}");
}

TEST_F(LogChannelEnabledTest, LogThread) {
  // Test that we are able to concurrently write to a log channel and disable
  // it.
  std::string err;

  // Start logging on one thread. Concurrently, try disabling the log channel.
  std::thread log_thread([this] { LLDB_LOG(getLog(), "Hello World"); });
  EXPECT_TRUE(DisableChannel("chan", {}, err));
  log_thread.join();

  // The log thread either managed to write to the log in time, or it didn't. In
  // either case, we should not trip any undefined behavior (run the test under
  // TSAN to verify this).
  EXPECT_THAT(takeOutput(), testing::AnyOf("", "Hello World\n"));
}

TEST_F(LogChannelEnabledTest, LogVerboseThread) {
  // Test that we are able to concurrently check the verbose flag of a log
  // channel and enable it.
  std::string err;

  // Start logging on one thread. Concurrently, try enabling the log channel
  // (with different log options).
  std::thread log_thread([this] { LLDB_LOGV(getLog(), "Hello World"); });
  EXPECT_TRUE(
      EnableChannel(getLogHandler(), LLDB_LOG_OPTION_VERBOSE, "chan", {}, err));
  log_thread.join();

  // The log thread either managed to write to the log, or it didn't. In either
  // case, we should not trip any undefined behavior (run the test under TSAN to
  // verify this).
  EXPECT_THAT(takeOutput(), testing::AnyOf("", "Hello World\n"));
}

TEST_F(LogChannelEnabledTest, LogGetLogThread) {
  // Test that we are able to concurrently get mask of a Log object and disable
  // it.
  std::string err;

  // Try fetching the log mask on one thread. Concurrently, try disabling the
  // log channel.
  uint32_t mask;
  std::thread log_thread([this, &mask] { mask = getLog()->GetMask().Get(); });
  EXPECT_TRUE(DisableChannel("chan", {}, err));
  log_thread.join();

  // The mask should be either zero of "FOO". In either case, we should not trip
  // any undefined behavior (run the test under TSAN to verify this).
  EXPECT_THAT(mask, testing::AnyOf(0, Log::MaskType(TestChannel::FOO)));
}
