// Copyright 2016 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <string.h>

#include <memory>

#include "base/bind.h"
#include "base/lazy_instance.h"
#include "base/synchronization/lock.h"
#include "base/synchronization/waitable_event.h"
#include "base/threading/thread.h"
#include "base/trace_event/category_registry.h"
#include "base/trace_event/trace_category.h"
#include "build/build_config.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace base {
namespace trace_event {

// Static initializers are generally forbidden. However, in the past we ran in
// the case of some test using tracing in a static initializer. This test checks
// That the category registry doesn't rely on static initializers itself and is
// functional even if called from another static initializer.
bool Initializer() {
  return CategoryRegistry::kCategoryMetadata &&
         CategoryRegistry::kCategoryMetadata->is_valid();
}
bool g_initializer_check = Initializer();

class TraceCategoryTest : public testing::Test {
 public:
  void SetUp() override { CategoryRegistry::Initialize(); }

  void TearDown() override { CategoryRegistry::ResetForTesting(); }

  static bool GetOrCreateCategoryByName(const char* name, TraceCategory** cat) {
    static LazyInstance<Lock>::Leaky g_lock = LAZY_INSTANCE_INITIALIZER;
    bool is_new_cat = false;
    *cat = CategoryRegistry::GetCategoryByName(name);
    if (!*cat) {
      AutoLock lock(g_lock.Get());
      is_new_cat = CategoryRegistry::GetOrCreateCategoryLocked(
          name, [](TraceCategory*) {}, cat);
    }
    return is_new_cat;
  };

  static CategoryRegistry::Range GetAllCategories() {
    return CategoryRegistry::GetAllCategories();
  }

  static void TestRaceThreadMain(WaitableEvent* event) {
    TraceCategory* cat = nullptr;
    event->Wait();
    GetOrCreateCategoryByName("__test_race", &cat);
    EXPECT_NE(nullptr, cat);
  }
};

TEST_F(TraceCategoryTest, Basic) {
  ASSERT_NE(nullptr, CategoryRegistry::kCategoryMetadata);
  ASSERT_TRUE(CategoryRegistry::kCategoryMetadata->is_valid());
  ASSERT_FALSE(CategoryRegistry::kCategoryMetadata->is_enabled());

  // Metadata category is built-in and should create a new category.
  TraceCategory* cat_meta = nullptr;
  const char* kMetadataName = CategoryRegistry::kCategoryMetadata->name();
  ASSERT_FALSE(GetOrCreateCategoryByName(kMetadataName, &cat_meta));
  ASSERT_EQ(CategoryRegistry::kCategoryMetadata, cat_meta);

  TraceCategory* cat_1 = nullptr;
  ASSERT_TRUE(GetOrCreateCategoryByName("__test_basic_ab", &cat_1));
  ASSERT_FALSE(cat_1->is_enabled());
  ASSERT_EQ(0u, cat_1->enabled_filters());
  cat_1->set_state_flag(TraceCategory::ENABLED_FOR_RECORDING);
  cat_1->set_state_flag(TraceCategory::ENABLED_FOR_FILTERING);
  ASSERT_EQ(TraceCategory::ENABLED_FOR_RECORDING |
                TraceCategory::ENABLED_FOR_FILTERING,
            cat_1->state());

  cat_1->set_enabled_filters(129);
  ASSERT_EQ(129u, cat_1->enabled_filters());
  ASSERT_EQ(cat_1, CategoryRegistry::GetCategoryByStatePtr(cat_1->state_ptr()));

  cat_1->clear_state_flag(TraceCategory::ENABLED_FOR_FILTERING);
  ASSERT_EQ(TraceCategory::ENABLED_FOR_RECORDING, cat_1->state());
  ASSERT_EQ(TraceCategory::ENABLED_FOR_RECORDING, *cat_1->state_ptr());
  ASSERT_TRUE(cat_1->is_enabled());

  TraceCategory* cat_2 = nullptr;
  ASSERT_TRUE(GetOrCreateCategoryByName("__test_basic_a", &cat_2));
  ASSERT_FALSE(cat_2->is_enabled());
  cat_2->set_state_flag(TraceCategory::ENABLED_FOR_RECORDING);

  TraceCategory* cat_2_copy = nullptr;
  ASSERT_FALSE(GetOrCreateCategoryByName("__test_basic_a", &cat_2_copy));
  ASSERT_EQ(cat_2, cat_2_copy);

  TraceCategory* cat_3 = nullptr;
  ASSERT_TRUE(
      GetOrCreateCategoryByName("__test_basic_ab,__test_basic_a", &cat_3));
  ASSERT_FALSE(cat_3->is_enabled());
  ASSERT_EQ(0u, cat_3->enabled_filters());

  int num_test_categories_seen = 0;
  for (const TraceCategory& cat : GetAllCategories()) {
    if (strcmp(cat.name(), kMetadataName) == 0)
      ASSERT_TRUE(CategoryRegistry::IsBuiltinCategory(&cat));

    if (strncmp(cat.name(), "__test_basic_", 13) == 0) {
      ASSERT_FALSE(CategoryRegistry::IsBuiltinCategory(&cat));
      num_test_categories_seen++;
    }
  }
  ASSERT_EQ(3, num_test_categories_seen);
  ASSERT_TRUE(g_initializer_check);
}

// Tries to cover the case of multiple threads creating the same category
// simultaneously. Should never end up with distinct entries with the same name.
#if defined(OS_FUCHSIA)
// TODO(crbug.com/738275): This is flaky on Fuchsia.
#define MAYBE_ThreadRaces DISABLED_ThreadRaces
#else
#define MAYBE_ThreadRaces ThreadRaces
#endif
TEST_F(TraceCategoryTest, MAYBE_ThreadRaces) {
  const int kNumThreads = 32;
  std::unique_ptr<Thread> threads[kNumThreads];
  for (int i = 0; i < kNumThreads; i++) {
    threads[i].reset(new Thread("test thread"));
    threads[i]->Start();
  }
  WaitableEvent sync_event(WaitableEvent::ResetPolicy::MANUAL,
                           WaitableEvent::InitialState::NOT_SIGNALED);
  for (int i = 0; i < kNumThreads; i++) {
    threads[i]->task_runner()->PostTask(
        FROM_HERE, BindOnce(&TestRaceThreadMain, Unretained(&sync_event)));
  }
  sync_event.Signal();
  for (int i = 0; i < kNumThreads; i++)
    threads[i]->Stop();

  int num_times_seen = 0;
  for (const TraceCategory& cat : GetAllCategories()) {
    if (strcmp(cat.name(), "__test_race") == 0)
      num_times_seen++;
  }
  ASSERT_EQ(1, num_times_seen);
}

}  // namespace trace_event
}  // namespace base
