/*
 * Copyright (C) 2018 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 <dlfcn.h>
#include <inttypes.h>
#include <signal.h>
#include <stdint.h>

#include <memory>
#include <string>
#include <vector>

#include <gtest/gtest.h>

#include <android-base/stringprintf.h>

#include <unwindstack/LocalUnwinder.h>

namespace unwindstack {

static std::vector<LocalFrameData>* g_frame_info;
static LocalUnwinder* g_unwinder;

extern "C" void SignalLocalInnerFunction() {
  g_unwinder->Unwind(g_frame_info, 256);
}

extern "C" void SignalLocalMiddleFunction() {
  SignalLocalInnerFunction();
}

extern "C" void SignalLocalOuterFunction() {
  SignalLocalMiddleFunction();
}

static void SignalLocalCallerHandler(int, siginfo_t*, void*) {
  SignalLocalOuterFunction();
}

static std::string ErrorMsg(const std::vector<const char*>& function_names,
                            const std::vector<LocalFrameData>& frame_info) {
  std::string unwind;
  size_t i = 0;
  for (const auto& frame : frame_info) {
    unwind += android::base::StringPrintf("#%02zu pc 0x%" PRIx64 " rel_pc 0x%" PRIx64, i++,
                                          frame.pc, frame.rel_pc);
    if (frame.map_info != nullptr) {
      if (!frame.map_info->name.empty()) {
        unwind += " " + frame.map_info->name;
      } else {
        unwind += android::base::StringPrintf(" 0x%" PRIx64 "-0x%" PRIx64, frame.map_info->start,
                                              frame.map_info->end);
      }
      if (frame.map_info->offset != 0) {
        unwind += android::base::StringPrintf(" offset 0x%" PRIx64, frame.map_info->offset);
      }
    }
    if (!frame.function_name.empty()) {
      unwind += " " + frame.function_name;
      if (frame.function_offset != 0) {
        unwind += android::base::StringPrintf("+%" PRId64, frame.function_offset);
      }
    }
    unwind += '\n';
  }

  return std::string(
             "Unwind completed without finding all frames\n"
             "  Looking for function: ") +
         function_names.front() + "\n" + "Unwind data:\n" + unwind;
}

// This test assumes that this code is compiled with optimizations turned
// off. If this doesn't happen, then all of the calls will be optimized
// away.
extern "C" void LocalInnerFunction(LocalUnwinder* unwinder, bool unwind_through_signal) {
  std::vector<LocalFrameData> frame_info;
  g_frame_info = &frame_info;
  g_unwinder = unwinder;
  std::vector<const char*> expected_function_names;

  if (unwind_through_signal) {
    struct sigaction act, oldact;
    memset(&act, 0, sizeof(act));
    act.sa_sigaction = SignalLocalCallerHandler;
    act.sa_flags = SA_RESTART | SA_ONSTACK;
    ASSERT_EQ(0, sigaction(SIGUSR1, &act, &oldact));

    raise(SIGUSR1);

    ASSERT_EQ(0, sigaction(SIGUSR1, &oldact, nullptr));

    expected_function_names = {"LocalOuterFunction",        "LocalMiddleFunction",
                               "LocalInnerFunction",        "SignalLocalOuterFunction",
                               "SignalLocalMiddleFunction", "SignalLocalInnerFunction"};
  } else {
    ASSERT_TRUE(unwinder->Unwind(&frame_info, 256));

    expected_function_names = {"LocalOuterFunction", "LocalMiddleFunction", "LocalInnerFunction"};
  }

  for (auto& frame : frame_info) {
    if (frame.function_name == expected_function_names.back()) {
      expected_function_names.pop_back();
      if (expected_function_names.empty()) {
        break;
      }
    }
  }

  ASSERT_TRUE(expected_function_names.empty()) << ErrorMsg(expected_function_names, frame_info);
}

extern "C" void LocalMiddleFunction(LocalUnwinder* unwinder, bool unwind_through_signal) {
  LocalInnerFunction(unwinder, unwind_through_signal);
}

extern "C" void LocalOuterFunction(LocalUnwinder* unwinder, bool unwind_through_signal) {
  LocalMiddleFunction(unwinder, unwind_through_signal);
}

class LocalUnwinderTest : public ::testing::Test {
 protected:
  void SetUp() override {
    unwinder_.reset(new LocalUnwinder);
    ASSERT_TRUE(unwinder_->Init());
  }

  std::unique_ptr<LocalUnwinder> unwinder_;
};

TEST_F(LocalUnwinderTest, local) {
  LocalOuterFunction(unwinder_.get(), false);
}

TEST_F(LocalUnwinderTest, local_signal) {
  LocalOuterFunction(unwinder_.get(), true);
}

TEST_F(LocalUnwinderTest, local_multiple) {
  ASSERT_NO_FATAL_FAILURE(LocalOuterFunction(unwinder_.get(), false));

  ASSERT_NO_FATAL_FAILURE(LocalOuterFunction(unwinder_.get(), true));

  ASSERT_NO_FATAL_FAILURE(LocalOuterFunction(unwinder_.get(), false));

  ASSERT_NO_FATAL_FAILURE(LocalOuterFunction(unwinder_.get(), true));
}

// This test verifies that doing an unwind before and after a dlopen
// works. It's verifying that the maps read during the first unwind
// do not cause a problem when doing the unwind using the code in
// the dlopen'd code.
TEST_F(LocalUnwinderTest, unwind_after_dlopen) {
  // Prime the maps data.
  ASSERT_NO_FATAL_FAILURE(LocalOuterFunction(unwinder_.get(), false));

  std::string testlib(testing::internal::GetArgvs()[0]);
  auto const value = testlib.find_last_of('/');
  if (value != std::string::npos) {
    testlib = testlib.substr(0, value + 1);
  } else {
    testlib = "";
  }
  testlib += "libunwindstack_local.so";

  void* handle = dlopen(testlib.c_str(), RTLD_NOW);
  ASSERT_TRUE(handle != nullptr);

  void (*unwind_function)(void*, void*) =
      reinterpret_cast<void (*)(void*, void*)>(dlsym(handle, "TestlibLevel1"));
  ASSERT_TRUE(unwind_function != nullptr);

  std::vector<LocalFrameData> frame_info;
  unwind_function(unwinder_.get(), &frame_info);

  ASSERT_EQ(0, dlclose(handle));

  std::vector<const char*> expected_function_names{"TestlibLevel1", "TestlibLevel2",
                                                   "TestlibLevel3", "TestlibLevel4"};

  for (auto& frame : frame_info) {
    if (frame.function_name == expected_function_names.back()) {
      expected_function_names.pop_back();
      if (expected_function_names.empty()) {
        break;
      }
    }
  }

  ASSERT_TRUE(expected_function_names.empty()) << ErrorMsg(expected_function_names, frame_info);
}

}  // namespace unwindstack
