forked from organicmaps/organicmaps
[search] Fixed search integration tests.
This commit is contained in:
parent
9ca265e281
commit
1c0c7b7a3a
5 changed files with 205 additions and 201 deletions
66
search/search_integration_tests/helpers.cpp
Normal file
66
search/search_integration_tests/helpers.cpp
Normal file
|
@ -0,0 +1,66 @@
|
|||
#include "search/search_integration_tests/helpers.hpp"
|
||||
|
||||
#include "search/search_query_factory.hpp"
|
||||
#include "search/search_tests_support/test_search_request.hpp"
|
||||
|
||||
#include "indexer/scales.hpp"
|
||||
|
||||
#include "platform/platform.hpp"
|
||||
|
||||
namespace search
|
||||
{
|
||||
SearchTest::SearchTest()
|
||||
: m_platform(GetPlatform())
|
||||
, m_scopedLog(LDEBUG)
|
||||
, m_engine(make_unique<storage::CountryInfoGetterForTesting>(), make_unique<SearchQueryFactory>(),
|
||||
Engine::Params())
|
||||
{
|
||||
}
|
||||
|
||||
SearchTest::~SearchTest()
|
||||
{
|
||||
for (auto const & file : m_files)
|
||||
Cleanup(file);
|
||||
}
|
||||
|
||||
void SearchTest::RegisterCountry(string const & name, m2::RectD const & rect)
|
||||
{
|
||||
auto & infoGetter =
|
||||
static_cast<storage::CountryInfoGetterForTesting &>(m_engine.GetCountryInfoGetter());
|
||||
infoGetter.AddCountry(storage::CountryDef(name, rect));
|
||||
}
|
||||
|
||||
bool SearchTest::ResultsMatch(string const & query,
|
||||
vector<shared_ptr<tests_support::MatchingRule>> const & rules)
|
||||
{
|
||||
tests_support::TestSearchRequest request(m_engine, query, "en", Mode::Everywhere, m_viewport);
|
||||
request.Wait();
|
||||
return MatchResults(m_engine, rules, request.Results());
|
||||
}
|
||||
|
||||
bool SearchTest::ResultsMatch(string const & query, Mode mode,
|
||||
vector<shared_ptr<tests_support::MatchingRule>> const & rules)
|
||||
{
|
||||
tests_support::TestSearchRequest request(m_engine, query, "en", mode, m_viewport);
|
||||
request.Wait();
|
||||
return MatchResults(m_engine, rules, request.Results());
|
||||
}
|
||||
|
||||
size_t SearchTest::CountFeatures(m2::RectD const & rect)
|
||||
{
|
||||
size_t count = 0;
|
||||
auto counter = [&count](const FeatureType & /* ft */)
|
||||
{
|
||||
++count;
|
||||
};
|
||||
m_engine.ForEachInRect(counter, rect, scales::GetUpperScale());
|
||||
return count;
|
||||
}
|
||||
|
||||
// static
|
||||
void SearchTest::Cleanup(platform::LocalCountryFile const & map)
|
||||
{
|
||||
platform::CountryIndexes::DeleteFromDisk(map);
|
||||
map.DeleteFromDisk(MapOptions::Map);
|
||||
}
|
||||
} // namespace search
|
79
search/search_integration_tests/helpers.hpp
Normal file
79
search/search_integration_tests/helpers.hpp
Normal file
|
@ -0,0 +1,79 @@
|
|||
#pragma once
|
||||
|
||||
#include "search/search_tests_support/test_mwm_builder.hpp"
|
||||
#include "search/search_tests_support/test_results_matching.hpp"
|
||||
#include "search/search_tests_support/test_search_engine.hpp"
|
||||
|
||||
#include "storage/country_decl.hpp"
|
||||
#include "storage/country_info_getter.hpp"
|
||||
|
||||
#include "indexer/classificator_loader.hpp"
|
||||
#include "indexer/mwm_set.hpp"
|
||||
|
||||
#include "geometry/rect2d.hpp"
|
||||
|
||||
#include "platform/country_file.hpp"
|
||||
#include "platform/local_country_file.hpp"
|
||||
#include "platform/local_country_file_utils.hpp"
|
||||
|
||||
#include "base/logging.hpp"
|
||||
|
||||
#include "std/unique_ptr.hpp"
|
||||
#include "std/vector.hpp"
|
||||
|
||||
class Platform;
|
||||
|
||||
namespace search
|
||||
{
|
||||
class TestWithClassificator
|
||||
{
|
||||
public:
|
||||
TestWithClassificator() { classificator::Load(); }
|
||||
};
|
||||
|
||||
class SearchTest : public TestWithClassificator
|
||||
{
|
||||
public:
|
||||
SearchTest();
|
||||
~SearchTest();
|
||||
|
||||
void RegisterCountry(string const & name, m2::RectD const & rect);
|
||||
|
||||
template <typename TBuildFn>
|
||||
MwmSet::MwmId BuildMwm(string const & name, feature::DataHeader::MapType type, TBuildFn && fn)
|
||||
{
|
||||
m_files.emplace_back(m_platform.WritableDir(), platform::CountryFile(name), 0 /* version */);
|
||||
auto & file = m_files.back();
|
||||
Cleanup(file);
|
||||
|
||||
{
|
||||
tests_support::TestMwmBuilder builder(file, type);
|
||||
fn(builder);
|
||||
}
|
||||
auto result = m_engine.RegisterMap(file);
|
||||
ASSERT_EQUAL(result.second, MwmSet::RegResult::Success, ());
|
||||
return result.first;
|
||||
}
|
||||
|
||||
inline void SetViewport(m2::RectD const & viewport) { m_viewport = viewport; }
|
||||
|
||||
bool ResultsMatch(string const & query,
|
||||
vector<shared_ptr<tests_support::MatchingRule>> const & rules);
|
||||
|
||||
bool ResultsMatch(string const & query, Mode mode,
|
||||
vector<shared_ptr<tests_support::MatchingRule>> const & rules);
|
||||
|
||||
size_t CountFeatures(m2::RectD const & rect);
|
||||
|
||||
protected:
|
||||
static void Cleanup(platform::LocalCountryFile const & map);
|
||||
|
||||
Platform & m_platform;
|
||||
my::ScopedLogLevelChanger m_scopedLog;
|
||||
vector<platform::LocalCountryFile> m_files;
|
||||
vector<storage::CountryDef> m_countries;
|
||||
unique_ptr<storage::CountryInfoGetterForTesting> m_infoGetter;
|
||||
tests_support::TestSearchEngine m_engine;
|
||||
m2::RectD m_viewport;
|
||||
};
|
||||
} // namespace search
|
|
@ -21,5 +21,9 @@ macx-*: LIBS *= "-framework IOKit"
|
|||
|
||||
SOURCES += \
|
||||
../../testing/testingmain.cpp \
|
||||
helpers.cpp \
|
||||
search_query_v2_test.cpp \
|
||||
smoke_test.cpp \
|
||||
|
||||
HEADERS += \
|
||||
helpers.hpp \
|
||||
|
|
|
@ -1,130 +1,29 @@
|
|||
#include "testing/testing.hpp"
|
||||
|
||||
#include "search/search_integration_tests/helpers.hpp"
|
||||
#include "search/search_tests_support/test_feature.hpp"
|
||||
#include "search/search_tests_support/test_mwm_builder.hpp"
|
||||
#include "search/search_tests_support/test_results_matching.hpp"
|
||||
#include "search/search_tests_support/test_search_engine.hpp"
|
||||
#include "search/search_tests_support/test_search_request.hpp"
|
||||
|
||||
#include "search/search_query_factory.hpp"
|
||||
#include "search/v2/search_query_v2.hpp"
|
||||
|
||||
#include "indexer/classificator_loader.hpp"
|
||||
#include "indexer/index.hpp"
|
||||
#include "indexer/mwm_set.hpp"
|
||||
|
||||
#include "storage/country_decl.hpp"
|
||||
#include "storage/country_info_getter.hpp"
|
||||
|
||||
#include "geometry/point2d.hpp"
|
||||
#include "geometry/rect2d.hpp"
|
||||
|
||||
#include "platform/country_file.hpp"
|
||||
#include "platform/local_country_file.hpp"
|
||||
#include "platform/local_country_file_utils.hpp"
|
||||
#include "platform/platform.hpp"
|
||||
|
||||
#include "base/scope_guard.hpp"
|
||||
|
||||
#include "std/shared_ptr.hpp"
|
||||
#include "std/vector.hpp"
|
||||
|
||||
using namespace search::tests_support;
|
||||
|
||||
using TRules = vector<shared_ptr<MatchingRule>>;
|
||||
|
||||
namespace search
|
||||
{
|
||||
namespace
|
||||
{
|
||||
class TestSearchQueryFactory : public search::SearchQueryFactory
|
||||
class SearchQueryV2Test : public SearchTest
|
||||
{
|
||||
// search::SearchQueryFactory overrides:
|
||||
unique_ptr<search::Query> BuildSearchQuery(Index & index, CategoriesHolder const & categories,
|
||||
vector<search::Suggest> const & suggests,
|
||||
storage::CountryInfoGetter const & infoGetter) override
|
||||
{
|
||||
return make_unique<search::v2::SearchQueryV2>(index, categories, suggests, infoGetter);
|
||||
}
|
||||
};
|
||||
|
||||
class TestWithClassificator
|
||||
{
|
||||
public:
|
||||
TestWithClassificator() { classificator::Load(); }
|
||||
};
|
||||
|
||||
class SearchQueryV2Test : public TestWithClassificator
|
||||
{
|
||||
public:
|
||||
SearchQueryV2Test()
|
||||
: m_platform(GetPlatform())
|
||||
, m_scopedLog(LDEBUG)
|
||||
, m_engine(make_unique<storage::CountryInfoGetterForTesting>(),
|
||||
make_unique<TestSearchQueryFactory>(), search::Engine::Params())
|
||||
{
|
||||
}
|
||||
|
||||
~SearchQueryV2Test()
|
||||
{
|
||||
for (auto const & file : m_files)
|
||||
Cleanup(file);
|
||||
}
|
||||
|
||||
void RegisterCountry(string const & name, m2::RectD const & rect)
|
||||
{
|
||||
auto & infoGetter =
|
||||
static_cast<storage::CountryInfoGetterForTesting &>(m_engine.GetCountryInfoGetter());
|
||||
infoGetter.AddCountry(storage::CountryDef(name, rect));
|
||||
}
|
||||
|
||||
template <typename TBuildFn>
|
||||
MwmSet::MwmId BuildMwm(string const & name, feature::DataHeader::MapType type, TBuildFn && fn)
|
||||
{
|
||||
m_files.emplace_back(m_platform.WritableDir(), platform::CountryFile(name), 0 /* version */);
|
||||
auto & file = m_files.back();
|
||||
Cleanup(file);
|
||||
|
||||
{
|
||||
TestMwmBuilder builder(file, type);
|
||||
fn(builder);
|
||||
}
|
||||
auto result = m_engine.RegisterMap(file);
|
||||
ASSERT_EQUAL(result.second, MwmSet::RegResult::Success, ());
|
||||
return result.first;
|
||||
}
|
||||
|
||||
void SetViewport(m2::RectD const & viewport) { m_viewport = viewport; }
|
||||
|
||||
bool ResultsMatch(string const & query, vector<shared_ptr<MatchingRule>> const & rules)
|
||||
{
|
||||
TestSearchRequest request(m_engine, query, "en", search::Mode::Everywhere, m_viewport);
|
||||
request.Wait();
|
||||
return MatchResults(m_engine, rules, request.Results());
|
||||
}
|
||||
|
||||
bool ResultsMatch(string const & query, search::Mode mode,
|
||||
vector<shared_ptr<MatchingRule>> const & rules)
|
||||
{
|
||||
TestSearchRequest request(m_engine, query, "en", mode, m_viewport);
|
||||
request.Wait();
|
||||
return MatchResults(m_engine, rules, request.Results());
|
||||
}
|
||||
|
||||
protected:
|
||||
Platform & m_platform;
|
||||
my::ScopedLogLevelChanger m_scopedLog;
|
||||
vector<platform::LocalCountryFile> m_files;
|
||||
vector<storage::CountryDef> m_countries;
|
||||
unique_ptr<storage::CountryInfoGetterForTesting> m_infoGetter;
|
||||
TestSearchEngine m_engine;
|
||||
m2::RectD m_viewport;
|
||||
|
||||
private:
|
||||
static void Cleanup(platform::LocalCountryFile const & map)
|
||||
{
|
||||
platform::CountryIndexes::DeleteFromDisk(map);
|
||||
map.DeleteFromDisk(MapOptions::Map);
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
UNIT_CLASS_TEST(SearchQueryV2Test, Smoke)
|
||||
{
|
||||
TestCountry wonderlandCountry(m2::PointD(10, 10), "Wonderland", "en");
|
||||
|
@ -328,11 +227,11 @@ UNIT_CLASS_TEST(SearchQueryV2Test, SearchByName)
|
|||
SetViewport(m2::RectD(m2::PointD(0.5, 0.5), m2::PointD(1.5, 1.5)));
|
||||
{
|
||||
TRules rules = {ExactMatch(worldId, london)};
|
||||
TEST(ResultsMatch("london", search::Mode::World, rules), ());
|
||||
TEST(ResultsMatch("london", Mode::World, rules), ());
|
||||
}
|
||||
{
|
||||
TRules rules = {ExactMatch(worldId, london), ExactMatch(wonderlandId, cafe)};
|
||||
TEST(ResultsMatch("london", search::Mode::Everywhere, rules), ());
|
||||
TEST(ResultsMatch("london", Mode::Everywhere, rules), ());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -349,10 +248,10 @@ UNIT_CLASS_TEST(SearchQueryV2Test, DisableSuggests)
|
|||
RegisterCountry("Wonderland", m2::RectD(m2::PointD(-2, -2), m2::PointD(2, 2)));
|
||||
SetViewport(m2::RectD(m2::PointD(0.5, 0.5), m2::PointD(1.5, 1.5)));
|
||||
{
|
||||
search::SearchParams params;
|
||||
SearchParams params;
|
||||
params.m_query = "londo";
|
||||
params.m_inputLocale = "en";
|
||||
params.SetMode(search::Mode::World);
|
||||
params.SetMode(Mode::World);
|
||||
params.SetSuggestsEnabled(false);
|
||||
|
||||
TestSearchRequest request(m_engine, params, m_viewport);
|
||||
|
@ -362,3 +261,5 @@ UNIT_CLASS_TEST(SearchQueryV2Test, DisableSuggests)
|
|||
TEST(MatchResults(m_engine, rules, request.Results()), ());
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
} // namespace search
|
||||
|
|
|
@ -1,140 +1,94 @@
|
|||
#include "testing/testing.hpp"
|
||||
|
||||
#include "search/search_integration_tests/helpers.hpp"
|
||||
#include "search/search_tests_support/test_feature.hpp"
|
||||
#include "search/search_tests_support/test_mwm_builder.hpp"
|
||||
#include "search/search_tests_support/test_search_engine.hpp"
|
||||
#include "search/search_tests_support/test_results_matching.hpp"
|
||||
#include "search/search_tests_support/test_search_request.hpp"
|
||||
|
||||
#include "indexer/classificator_loader.hpp"
|
||||
#include "indexer/scales.hpp"
|
||||
#include "geometry/point2d.hpp"
|
||||
#include "geometry/rect2d.hpp"
|
||||
|
||||
#include "indexer/classificator_loader.hpp"
|
||||
#include "indexer/scales.hpp"
|
||||
|
||||
#include "platform/country_defines.hpp"
|
||||
#include "platform/country_file.hpp"
|
||||
#include "platform/local_country_file.hpp"
|
||||
#include "platform/local_country_file_utils.hpp"
|
||||
#include "platform/platform.hpp"
|
||||
#include "std/shared_ptr.hpp"
|
||||
#include "std/vector.hpp"
|
||||
|
||||
using namespace search::tests_support;
|
||||
|
||||
namespace
|
||||
{
|
||||
class ScopedMapFile
|
||||
{
|
||||
public:
|
||||
explicit ScopedMapFile(string const & name)
|
||||
: m_file(GetPlatform().TmpDir(), platform::CountryFile(name), 0)
|
||||
{
|
||||
platform::CountryIndexes::DeleteFromDisk(m_file);
|
||||
}
|
||||
|
||||
~ScopedMapFile()
|
||||
{
|
||||
platform::CountryIndexes::DeleteFromDisk(m_file);
|
||||
m_file.DeleteFromDisk(MapOptions::Map);
|
||||
}
|
||||
|
||||
inline platform::LocalCountryFile & GetFile() { return m_file; }
|
||||
|
||||
private:
|
||||
platform::LocalCountryFile m_file;
|
||||
};
|
||||
} // namespace
|
||||
using TRules = vector<shared_ptr<MatchingRule>>;
|
||||
|
||||
namespace search
|
||||
{
|
||||
void TestFeaturesCount(TestSearchEngine const & engine, m2::RectD const & rect,
|
||||
size_t expectedCount)
|
||||
namespace
|
||||
{
|
||||
size_t actualCount = 0;
|
||||
auto counter = [&actualCount](const FeatureType & /* ft */)
|
||||
{
|
||||
++actualCount;
|
||||
};
|
||||
engine.ForEachInRect(counter, rect, scales::GetUpperScale());
|
||||
TEST_EQUAL(expectedCount, actualCount, ());
|
||||
}
|
||||
|
||||
UNIT_TEST(GenerateTestMwm_Smoke)
|
||||
class SmokeTest : public SearchTest
|
||||
{
|
||||
classificator::Load();
|
||||
ScopedMapFile scopedFile("BuzzTown");
|
||||
platform::LocalCountryFile & file = scopedFile.GetFile();
|
||||
};
|
||||
|
||||
UNIT_CLASS_TEST(SmokeTest, Smoke)
|
||||
{
|
||||
char const kCountryName[] = "BuzzTown";
|
||||
|
||||
TestPOI wineShop(m2::PointD(0, 0), "Wine shop", "en");
|
||||
TestPOI tequilaShop(m2::PointD(1, 0), "Tequila shop", "en");
|
||||
TestPOI brandyShop(m2::PointD(0, 1), "Brandy shop", "en");
|
||||
TestPOI vodkaShop(m2::PointD(1, 1), "Russian vodka shop", "en");
|
||||
|
||||
auto id = BuildMwm(kCountryName, feature::DataHeader::country, [&](TestMwmBuilder & builder)
|
||||
{
|
||||
TestMwmBuilder builder(file, feature::DataHeader::country);
|
||||
builder.Add(TestPOI(m2::PointD(0, 0), "Wine shop", "en"));
|
||||
builder.Add(TestPOI(m2::PointD(1, 0), "Tequila shop", "en"));
|
||||
builder.Add(TestPOI(m2::PointD(0, 1), "Brandy shop", "en"));
|
||||
builder.Add(TestPOI(m2::PointD(1, 1), "Russian vodka shop", "en"));
|
||||
}
|
||||
TEST_EQUAL(MapOptions::MapWithCarRouting, file.GetFiles(), ());
|
||||
builder.Add(wineShop);
|
||||
builder.Add(tequilaShop);
|
||||
builder.Add(brandyShop);
|
||||
builder.Add(vodkaShop);
|
||||
});
|
||||
|
||||
TestSearchEngine engine{Engine::Params{}};
|
||||
auto ret = engine.RegisterMap(file);
|
||||
TEST_EQUAL(MwmSet::RegResult::Success, ret.second, ("Can't register generated map."));
|
||||
TEST(ret.first.IsAlive(), ("Can't get lock on a generated map."));
|
||||
|
||||
TestFeaturesCount(engine, m2::RectD(m2::PointD(0, 0), m2::PointD(1, 1)), 4);
|
||||
TestFeaturesCount(engine, m2::RectD(m2::PointD(-0.5, -0.5), m2::PointD(0.5, 1.5)), 2);
|
||||
TEST_EQUAL(4, CountFeatures(m2::RectD(m2::PointD(0, 0), m2::PointD(1, 1))), ());
|
||||
TEST_EQUAL(2, CountFeatures(m2::RectD(m2::PointD(-0.5, -0.5), m2::PointD(0.5, 1.5))), ());
|
||||
|
||||
SetViewport(m2::RectD(m2::PointD(0, 0), m2::PointD(100, 100)));
|
||||
{
|
||||
TestSearchRequest request(engine, "wine ", "en", search::Mode::Viewport,
|
||||
m2::RectD(m2::PointD(0, 0), m2::PointD(100, 100)));
|
||||
request.Wait();
|
||||
TEST_EQUAL(1, request.Results().size(), ());
|
||||
TRules rules = {ExactMatch(id, wineShop)};
|
||||
TEST(ResultsMatch("wine ", rules), ());
|
||||
}
|
||||
|
||||
{
|
||||
TestSearchRequest request(engine, "shop ", "en", search::Mode::Viewport,
|
||||
m2::RectD(m2::PointD(0, 0), m2::PointD(100, 100)));
|
||||
request.Wait();
|
||||
TEST_EQUAL(4, request.Results().size(), ());
|
||||
TRules rules = {ExactMatch(id, wineShop), ExactMatch(id, tequilaShop),
|
||||
ExactMatch(id, brandyShop), ExactMatch(id, vodkaShop)};
|
||||
TEST(ResultsMatch("shop ", rules), ());
|
||||
}
|
||||
}
|
||||
|
||||
UNIT_TEST(GenerateTestMwm_NotPrefixFreeNames)
|
||||
UNIT_CLASS_TEST(SmokeTest, NotPrefixFreeNames)
|
||||
{
|
||||
classificator::Load();
|
||||
ScopedMapFile scopedFile("ATown");
|
||||
platform::LocalCountryFile & file = scopedFile.GetFile();
|
||||
char const kCountryName[] = "ATown";
|
||||
|
||||
auto id = BuildMwm(kCountryName, feature::DataHeader::country, [&](TestMwmBuilder & builder)
|
||||
{
|
||||
TestMwmBuilder builder(file, feature::DataHeader::country);
|
||||
builder.Add(TestPOI(m2::PointD(0, 0), "a", "en"));
|
||||
builder.Add(TestPOI(m2::PointD(0, 1), "aa", "en"));
|
||||
builder.Add(TestPOI(m2::PointD(1, 1), "aa", "en"));
|
||||
builder.Add(TestPOI(m2::PointD(1, 0), "aaa", "en"));
|
||||
builder.Add(TestPOI(m2::PointD(2, 0), "aaa", "en"));
|
||||
builder.Add(TestPOI(m2::PointD(2, 1), "aaa", "en"));
|
||||
}
|
||||
TEST_EQUAL(MapOptions::MapWithCarRouting, file.GetFiles(), ());
|
||||
});
|
||||
|
||||
TestSearchEngine engine{Engine::Params{}};
|
||||
auto ret = engine.RegisterMap(file);
|
||||
TEST_EQUAL(MwmSet::RegResult::Success, ret.second, ("Can't register generated map."));
|
||||
TEST(ret.first.IsAlive(), ("Can't get lock on a generated map."));
|
||||
|
||||
TestFeaturesCount(engine, m2::RectD(m2::PointD(0, 0), m2::PointD(2, 2)), 6);
|
||||
TEST_EQUAL(6, CountFeatures(m2::RectD(m2::PointD(0, 0), m2::PointD(2, 2))), ());
|
||||
|
||||
m2::RectD const viewport(m2::PointD(0, 0), m2::PointD(100, 100));
|
||||
{
|
||||
TestSearchRequest request(engine, "a ", "en", search::Mode::Viewport,
|
||||
m2::RectD(m2::PointD(0, 0), m2::PointD(100, 100)));
|
||||
TestSearchRequest request(m_engine, "a ", "en", Mode::Viewport, viewport);
|
||||
request.Wait();
|
||||
TEST_EQUAL(1, request.Results().size(), ());
|
||||
}
|
||||
{
|
||||
TestSearchRequest request(engine, "aa ", "en", search::Mode::Viewport,
|
||||
m2::RectD(m2::PointD(0, 0), m2::PointD(100, 100)));
|
||||
TestSearchRequest request(m_engine, "aa ", "en", Mode::Viewport, viewport);
|
||||
request.Wait();
|
||||
TEST_EQUAL(2, request.Results().size(), ());
|
||||
}
|
||||
{
|
||||
TestSearchRequest request(engine, "aaa ", "en", search::Mode::Viewport,
|
||||
m2::RectD(m2::PointD(0, 0), m2::PointD(100, 100)));
|
||||
TestSearchRequest request(m_engine, "aaa ", "en", search::Mode::Viewport, viewport);
|
||||
request.Wait();
|
||||
TEST_EQUAL(3, request.Results().size(), ());
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
} // namespace search
|
||||
|
|
Loading…
Add table
Reference in a new issue