blob: 0d3e4bf1da03d6556bad42e44f710e97bf3e2e8c [file] [log] [blame]
// Copyright 2013 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 <stddef.h>
#include <memory>
#include "base/bind.h"
#include "base/files/scoped_temp_dir.h"
#include "base/macros.h"
#include "base/run_loop.h"
#include "base/strings/string_number_conversions.h"
#include "base/test/scoped_task_environment.h"
#include "base/test/sequenced_worker_pool_owner.h"
#include "base/threading/platform_thread.h"
#include "chrome/browser/ui/app_list/search/history_factory.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "ui/app_list/search/dictionary_data_store.h"
#include "ui/app_list/search/history.h"
#include "ui/app_list/search/history_data.h"
#include "ui/app_list/search/history_data_observer.h"
#include "ui/app_list/search/history_data_store.h"
namespace app_list {
namespace test {
namespace {
const size_t kMaxPrimary = 3;
const size_t kMaxSecondary = 2;
// HistoryDataLoadWaiter waits for give |data| to be loaded from underlying
// store on the blocking pool. The waiter waits on the main message loop until
// OnHistoryDataLoadedFromStore() is invoked or the maximum allowed wait time
// has passed.
class HistoryDataLoadWaiter : public HistoryDataObserver {
public:
explicit HistoryDataLoadWaiter(HistoryData* data) : data_(data) {}
~HistoryDataLoadWaiter() override {}
void Wait() {
data_->AddObserver(this);
run_loop_.reset(new base::RunLoop);
run_loop_->Run();
data_->RemoveObserver(this);
}
private:
// HistoryDataObserver overrides:
void OnHistoryDataLoadedFromStore() override { run_loop_->Quit(); }
HistoryData* data_; // Not owned.
std::unique_ptr<base::RunLoop> run_loop_;
DISALLOW_COPY_AND_ASSIGN(HistoryDataLoadWaiter);
};
// StoreFlushWaiter waits for the given |store| to flush its data to disk.
// The flush and disk write happens on the blocking pool. The waiter waits
// on the main message loop until the OnFlushed() is invoked or the maximum
// allowed wait time has passed.
class StoreFlushWaiter {
public:
explicit StoreFlushWaiter(HistoryDataStore* store) : store_(store) {}
~StoreFlushWaiter() {}
void Wait() {
store_->Flush(
base::Bind(&StoreFlushWaiter::OnFlushed, base::Unretained(this)));
run_loop_.reset(new base::RunLoop);
run_loop_->Run();
}
private:
void OnFlushed() {
run_loop_->Quit();
}
HistoryDataStore* store_; // Not owned.
std::unique_ptr<base::RunLoop> run_loop_;
DISALLOW_COPY_AND_ASSIGN(StoreFlushWaiter);
};
} // namespace
class SearchHistoryTest : public testing::Test {
public:
SearchHistoryTest()
: scoped_task_environment_(
base::test::ScopedTaskEnvironment::MainThreadType::UI) {}
~SearchHistoryTest() override {}
// testing::Test overrides:
void SetUp() override {
worker_pool_owner_.reset(
new base::SequencedWorkerPoolOwner(2, "AppLauncherTest"));
ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
CreateHistory();
}
void TearDown() override {
Flush();
}
void CreateHistory() {
const char kStoreDataFileName[] = "app-launcher-test";
const base::FilePath data_file =
temp_dir_.GetPath().AppendASCII(kStoreDataFileName);
scoped_refptr<DictionaryDataStore> dictionary_data_store(
new DictionaryDataStore(data_file, worker_pool_owner_->pool().get()));
history_.reset(new History(scoped_refptr<HistoryDataStore>(
new HistoryDataStore(dictionary_data_store))));
// Replace |data_| with test params.
history_->data_->RemoveObserver(history_.get());
history_->data_.reset(
new HistoryData(history_->store_.get(), kMaxPrimary, kMaxSecondary));
history_->data_->AddObserver(history_.get());
HistoryDataLoadWaiter(history_->data_.get()).Wait();
ASSERT_TRUE(history_->IsReady());
}
void Flush() {
StoreFlushWaiter(history_->store_.get()).Wait();
}
size_t GetKnownResults(const std::string& query) {
known_results_ = history()->GetKnownResults(query);
return known_results_->size();
}
KnownResultType GetResultType(const std::string& result_id) {
return known_results_->find(result_id) != known_results_->end()
? (*known_results_.get())[result_id]
: UNKNOWN_RESULT;
}
History* history() { return history_.get(); }
const HistoryData::Associations& associations() const {
return history_->data_->associations();
}
private:
base::test::ScopedTaskEnvironment scoped_task_environment_;
base::ScopedTempDir temp_dir_;
std::unique_ptr<base::SequencedWorkerPoolOwner> worker_pool_owner_;
std::unique_ptr<History> history_;
std::unique_ptr<KnownResults> known_results_;
DISALLOW_COPY_AND_ASSIGN(SearchHistoryTest);
};
TEST_F(SearchHistoryTest, Persistence) {
// Ensure it's empty.
EXPECT_EQ(0u, GetKnownResults("cal"));
// Add one launch event.
history()->AddLaunchEvent("cal", "calendar");
EXPECT_EQ(1u, GetKnownResults("cal"));
// Flush and recreate the history object.
Flush();
CreateHistory();
// History should be initialized with data just added.
EXPECT_EQ(1u, GetKnownResults("cal"));
}
TEST_F(SearchHistoryTest, PerfectAndPrefixMatch) {
const char kQuery[] = "cal";
const char kQueryPrefix[] = "c";
const char kPrimary[] = "calendar";
const char kSecondary[] = "calculator";
history()->AddLaunchEvent(kQuery, kPrimary);
history()->AddLaunchEvent(kQuery, kSecondary);
EXPECT_EQ(2u, GetKnownResults(kQuery));
EXPECT_EQ(PERFECT_PRIMARY, GetResultType(kPrimary));
EXPECT_EQ(PERFECT_SECONDARY, GetResultType(kSecondary));
EXPECT_EQ(2u, GetKnownResults(kQueryPrefix));
EXPECT_EQ(PREFIX_PRIMARY, GetResultType(kPrimary));
EXPECT_EQ(PREFIX_SECONDARY, GetResultType(kSecondary));
}
TEST_F(SearchHistoryTest, StickyPrimary) {
const char kQuery[] = "cal";
const char kPrimary[] = "calendar";
const char kSecondary[] = "calculator";
const char kOther[] = "other";
// Add two launch events. kPrimary becomes primary.
history()->AddLaunchEvent(kQuery, kPrimary);
history()->AddLaunchEvent(kQuery, kSecondary);
EXPECT_EQ(2u, GetKnownResults(kQuery));
EXPECT_EQ(PERFECT_PRIMARY, GetResultType(kPrimary));
EXPECT_EQ(PERFECT_SECONDARY, GetResultType(kSecondary));
// These launch events should not change primary.
history()->AddLaunchEvent(kQuery, kPrimary);
history()->AddLaunchEvent(kQuery, kSecondary);
history()->AddLaunchEvent(kQuery, kPrimary);
history()->AddLaunchEvent(kQuery, kSecondary);
history()->AddLaunchEvent(kQuery, kPrimary);
history()->AddLaunchEvent(kQuery, kSecondary);
history()->AddLaunchEvent(kQuery, kOther);
history()->AddLaunchEvent(kQuery, kSecondary);
history()->AddLaunchEvent(kQuery, kOther);
history()->AddLaunchEvent(kQuery, kSecondary);
history()->AddLaunchEvent(kQuery, kOther);
EXPECT_EQ(3u, GetKnownResults(kQuery));
EXPECT_EQ(PERFECT_PRIMARY, GetResultType(kPrimary));
EXPECT_EQ(PERFECT_SECONDARY, GetResultType(kSecondary));
EXPECT_EQ(PERFECT_SECONDARY, GetResultType(kOther));
}
TEST_F(SearchHistoryTest, PromoteSecondary) {
const char kQuery[] = "cal";
const char kPrimary[] = "calendar";
const char kSecondary[] = "calculator";
history()->AddLaunchEvent(kQuery, kPrimary);
history()->AddLaunchEvent(kQuery, kSecondary);
EXPECT_EQ(2u, GetKnownResults(kQuery));
EXPECT_EQ(PERFECT_PRIMARY, GetResultType(kPrimary));
EXPECT_EQ(PERFECT_SECONDARY, GetResultType(kSecondary));
// The 2nd launch in a row promotes it to be primary.
history()->AddLaunchEvent(kQuery, kSecondary);
EXPECT_EQ(2u, GetKnownResults(kQuery));
EXPECT_EQ(PERFECT_PRIMARY, GetResultType(kSecondary));
EXPECT_EQ(PERFECT_SECONDARY, GetResultType(kPrimary));
}
TEST_F(SearchHistoryTest, MaxPrimary) {
for (size_t i = 0; i < kMaxPrimary; ++i) {
std::string query = base::SizeTToString(i);
history()->AddLaunchEvent(query, "app");
}
EXPECT_EQ(kMaxPrimary, associations().size());
// Oldest entries still exists.
EXPECT_TRUE(associations().find("0") != associations().end());
EXPECT_TRUE(associations().find("1") != associations().end());
// Primary entry trimming is based on time. The code could run here too fast
// and Time::Now has not changed on some platform. Sleep a bit here to ensure
// that some time has passed to get rid of the flake.
base::PlatformThread::Sleep(base::TimeDelta::FromMilliseconds(25));
// Touches the oldest and 2nd oldest becomes oldest now.
history()->AddLaunchEvent("0", "app");
// Adds one more
history()->AddLaunchEvent("extra", "app");
// Number of entries are capped to kMaxPrimary.
EXPECT_EQ(kMaxPrimary, associations().size());
// Oldest entry is trimmed.
EXPECT_FALSE(associations().find("1") != associations().end());
// The touched oldest survived.
EXPECT_TRUE(associations().find("0") != associations().end());
}
TEST_F(SearchHistoryTest, MaxSecondary) {
const char kQuery[] = "query";
history()->AddLaunchEvent(kQuery, "primary");
for (size_t i = 0; i < kMaxSecondary; ++i) {
std::string result_id = base::SizeTToString(i);
history()->AddLaunchEvent(kQuery, result_id);
}
EXPECT_EQ(kMaxSecondary + 1, GetKnownResults(kQuery));
EXPECT_EQ(PERFECT_SECONDARY, GetResultType("0"));
EXPECT_EQ(PERFECT_SECONDARY, GetResultType("1"));
// Touches the oldest secondary.
history()->AddLaunchEvent(kQuery, "0");
// Adds one more.
history()->AddLaunchEvent(kQuery, "extra");
// Total number of results is capped.
EXPECT_EQ(kMaxSecondary + 1, GetKnownResults(kQuery));
// The oldest secondary is gone.
EXPECT_EQ(UNKNOWN_RESULT, GetResultType("1"));
// Touched oldest survived.
EXPECT_EQ(PERFECT_SECONDARY, GetResultType("0"));
}
} // namespace test
} // namespace app_list