// 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 "base/bind.h" #include "base/macros.h" #include "base/run_loop.h" #include "components/filesystem/public/interfaces/directory.mojom.h" #include "components/filesystem/public/interfaces/file_system.mojom.h" #include "components/filesystem/public/interfaces/types.mojom.h" #include "components/leveldb/public/cpp/util.h" #include "components/leveldb/public/interfaces/leveldb.mojom.h" #include "mojo/public/cpp/bindings/binding_set.h" #include "services/service_manager/public/cpp/service_context.h" #include "services/service_manager/public/cpp/service_test.h" using filesystem::mojom::FileError; namespace leveldb { namespace { template void IgnoreAllArgs(Args&&...) {} template void DoCaptures(typename std::decay::type*... out_args, const base::Closure& quit_closure, Args... in_args) { IgnoreAllArgs((*out_args = std::move(in_args))...); quit_closure.Run(); } template base::Callback Capture(T1* t1, const base::Closure& quit_closure) { return base::Bind(&DoCaptures, t1, quit_closure); } template base::Callback Capture(T1* t1, T2* t2, const base::Closure& quit_closure) { return base::Bind(&DoCaptures, t1, t2, quit_closure); } template base::Callback CaptureConstRef( T1* t1, const base::Closure& quit_closure) { return base::Bind(&DoCaptures, t1, quit_closure); } template base::Callback CaptureConstRef(T1* t1, T2* t2, const base::Closure& quit_closure) { return base::Bind(&DoCaptures, t1, t2, quit_closure); } void DatabaseSyncPut(mojom::LevelDBDatabase* database, const std::string& key, const std::string& value, mojom::DatabaseError* out_error) { base::RunLoop run_loop; database->Put(StdStringToUint8Vector(key), StdStringToUint8Vector(value), Capture(out_error, run_loop.QuitClosure())); run_loop.Run(); } void DatabaseSyncGet(mojom::LevelDBDatabase* database, const std::string& key, mojom::DatabaseError* out_error, std::vector* out_value) { base::RunLoop run_loop; database->Get(StdStringToUint8Vector(key), CaptureConstRef(out_error, out_value, run_loop.QuitClosure())); run_loop.Run(); } void DatabaseSyncGetPrefixed(mojom::LevelDBDatabase* database, const std::string& key_prefix, mojom::DatabaseError* out_error, std::vector* out_key_values) { base::RunLoop run_loop; database->GetPrefixed( StdStringToUint8Vector(key_prefix), Capture(out_error, out_key_values, run_loop.QuitClosure())); run_loop.Run(); } void DatabaseSyncDeletePrefixed(mojom::LevelDBDatabase* database, const std::string& key_prefix, mojom::DatabaseError* out_error) { base::RunLoop run_loop; database->DeletePrefixed(StdStringToUint8Vector(key_prefix), Capture(out_error, run_loop.QuitClosure())); run_loop.Run(); } void LevelDBSyncOpenInMemory(mojom::LevelDBService* leveldb, mojom::LevelDBDatabaseAssociatedRequest database, mojom::DatabaseError* out_error) { base::RunLoop run_loop; leveldb->OpenInMemory(std::move(database), Capture(out_error, run_loop.QuitClosure())); run_loop.Run(); } class LevelDBServiceTest : public service_manager::test::ServiceTest { public: LevelDBServiceTest() : ServiceTest("leveldb_service_unittests") {} ~LevelDBServiceTest() override {} protected: // Overridden from mojo::test::ApplicationTestBase: void SetUp() override { ServiceTest::SetUp(); connector()->BindInterface("filesystem", &files_); connector()->BindInterface("leveldb", &leveldb_); } void TearDown() override { leveldb_.reset(); files_.reset(); ServiceTest::TearDown(); } // Note: This has an out parameter rather than returning the |DirectoryPtr|, // since |ASSERT_...()| doesn't work with return values. void GetTempDirectory(filesystem::mojom::DirectoryPtr* directory) { FileError error = FileError::FAILED; bool handled = files()->OpenTempDirectory(MakeRequest(directory), &error); ASSERT_TRUE(handled); ASSERT_EQ(FileError::OK, error); } filesystem::mojom::FileSystemPtr& files() { return files_; } mojom::LevelDBServicePtr& leveldb() { return leveldb_; } private: filesystem::mojom::FileSystemPtr files_; mojom::LevelDBServicePtr leveldb_; DISALLOW_COPY_AND_ASSIGN(LevelDBServiceTest); }; TEST_F(LevelDBServiceTest, Basic) { mojom::DatabaseError error; mojom::LevelDBDatabaseAssociatedPtr database; LevelDBSyncOpenInMemory(leveldb().get(), MakeRequest(&database), &error); EXPECT_EQ(mojom::DatabaseError::OK, error); // Write a key to the database. error = mojom::DatabaseError::INVALID_ARGUMENT; DatabaseSyncPut(database.get(), "key", "value", &error); EXPECT_EQ(mojom::DatabaseError::OK, error); // Read the key back from the database. error = mojom::DatabaseError::INVALID_ARGUMENT; std::vector value; DatabaseSyncGet(database.get(), "key", &error, &value); EXPECT_EQ(mojom::DatabaseError::OK, error); EXPECT_EQ("value", Uint8VectorToStdString(value)); // Delete the key from the database. error = mojom::DatabaseError::INVALID_ARGUMENT; base::RunLoop run_loop; database->Delete(StdStringToUint8Vector("key"), Capture(&error, run_loop.QuitClosure())); run_loop.Run(); EXPECT_EQ(mojom::DatabaseError::OK, error); // Read the key back from the database. error = mojom::DatabaseError::INVALID_ARGUMENT; value.clear(); DatabaseSyncGet(database.get(), "key", &error, &value); EXPECT_EQ(mojom::DatabaseError::NOT_FOUND, error); EXPECT_EQ("", Uint8VectorToStdString(value)); } TEST_F(LevelDBServiceTest, WriteBatch) { mojom::DatabaseError error; mojom::LevelDBDatabaseAssociatedPtr database; LevelDBSyncOpenInMemory(leveldb().get(), MakeRequest(&database), &error); EXPECT_EQ(mojom::DatabaseError::OK, error); // Write a key to the database. DatabaseSyncPut(database.get(), "key", "value", &error); EXPECT_EQ(mojom::DatabaseError::OK, error); // Create a batched operation which both deletes "key" and adds another write. std::vector operations; mojom::BatchedOperationPtr item = mojom::BatchedOperation::New(); item->type = mojom::BatchOperationType::DELETE_KEY; item->key = StdStringToUint8Vector("key"); operations.push_back(std::move(item)); item = mojom::BatchedOperation::New(); item->type = mojom::BatchOperationType::PUT_KEY; item->key = StdStringToUint8Vector("other"); item->value = StdStringToUint8Vector("more"); operations.push_back(std::move(item)); base::RunLoop run_loop; database->Write(std::move(operations), Capture(&error, run_loop.QuitClosure())); run_loop.Run(); EXPECT_EQ(mojom::DatabaseError::OK, error); // Reading "key" should be invalid now. error = mojom::DatabaseError::INVALID_ARGUMENT; std::vector value; DatabaseSyncGet(database.get(), "key", &error, &value); EXPECT_EQ(mojom::DatabaseError::NOT_FOUND, error); EXPECT_EQ("", Uint8VectorToStdString(value)); // Reading "other" should return "more" error = mojom::DatabaseError::INVALID_ARGUMENT; DatabaseSyncGet(database.get(), "other", &error, &value); EXPECT_EQ(mojom::DatabaseError::OK, error); EXPECT_EQ("more", Uint8VectorToStdString(value)); // Write a some prefixed keys to the database. DatabaseSyncPut(database.get(), "prefix-key1", "value", &error); EXPECT_EQ(mojom::DatabaseError::OK, error); DatabaseSyncPut(database.get(), "prefix-key2", "value", &error); EXPECT_EQ(mojom::DatabaseError::OK, error); // Create a batched operation to delete them. operations.clear(); item = mojom::BatchedOperation::New(); item->type = mojom::BatchOperationType::DELETE_PREFIXED_KEY; item->key = StdStringToUint8Vector("prefix"); operations.push_back(std::move(item)); base::RunLoop run_loop2; database->Write(std::move(operations), Capture(&error, run_loop2.QuitClosure())); run_loop2.Run(); EXPECT_EQ(mojom::DatabaseError::OK, error); // Reading all "prefix" keys should be invalid now. error = mojom::DatabaseError::INVALID_ARGUMENT; value.clear(); DatabaseSyncGet(database.get(), "prefix-key1", &error, &value); EXPECT_EQ(mojom::DatabaseError::NOT_FOUND, error); EXPECT_EQ("", Uint8VectorToStdString(value)); // Reading "key" should be invalid now. error = mojom::DatabaseError::INVALID_ARGUMENT; value.clear(); DatabaseSyncGet(database.get(), "prefix-key2", &error, &value); EXPECT_EQ(mojom::DatabaseError::NOT_FOUND, error); EXPECT_EQ("", Uint8VectorToStdString(value)); } TEST_F(LevelDBServiceTest, Reconnect) { mojom::DatabaseError error; filesystem::mojom::DirectoryPtr temp_directory; GetTempDirectory(&temp_directory); { filesystem::mojom::DirectoryPtr directory; temp_directory->Clone(MakeRequest(&directory)); mojom::LevelDBDatabaseAssociatedPtr database; leveldb::mojom::OpenOptionsPtr options = leveldb::mojom::OpenOptions::New(); options->error_if_exists = true; options->create_if_missing = true; base::RunLoop run_loop; leveldb()->OpenWithOptions(std::move(options), std::move(directory), "test", MakeRequest(&database), Capture(&error, run_loop.QuitClosure())); run_loop.Run(); EXPECT_EQ(mojom::DatabaseError::OK, error); // Write a key to the database. error = mojom::DatabaseError::INVALID_ARGUMENT; DatabaseSyncPut(database.get(), "key", "value", &error); EXPECT_EQ(mojom::DatabaseError::OK, error); // The database should go out of scope here. } { filesystem::mojom::DirectoryPtr directory; temp_directory->Clone(MakeRequest(&directory)); // Reconnect to the database. mojom::LevelDBDatabaseAssociatedPtr database; base::RunLoop run_loop; leveldb()->Open(std::move(directory), "test", MakeRequest(&database), Capture(&error, run_loop.QuitClosure())); run_loop.Run(); EXPECT_EQ(mojom::DatabaseError::OK, error); // We should still be able to read the key back from the database. error = mojom::DatabaseError::INVALID_ARGUMENT; std::vector value; DatabaseSyncGet(database.get(), "key", &error, &value); EXPECT_EQ(mojom::DatabaseError::OK, error); EXPECT_EQ("value", Uint8VectorToStdString(value)); } } TEST_F(LevelDBServiceTest, Destroy) { mojom::DatabaseError error; filesystem::mojom::DirectoryPtr temp_directory; GetTempDirectory(&temp_directory); { filesystem::mojom::DirectoryPtr directory; temp_directory->Clone(MakeRequest(&directory)); mojom::LevelDBDatabaseAssociatedPtr database; leveldb::mojom::OpenOptionsPtr options = leveldb::mojom::OpenOptions::New(); options->error_if_exists = true; options->create_if_missing = true; base::RunLoop run_loop; leveldb()->OpenWithOptions(std::move(options), std::move(directory), "test", MakeRequest(&database), Capture(&error, run_loop.QuitClosure())); run_loop.Run(); EXPECT_EQ(mojom::DatabaseError::OK, error); // Write a key to the database. error = mojom::DatabaseError::INVALID_ARGUMENT; DatabaseSyncPut(database.get(), "key", "value", &error); EXPECT_EQ(mojom::DatabaseError::OK, error); // The database should go out of scope here. } { filesystem::mojom::DirectoryPtr directory; temp_directory->Clone(MakeRequest(&directory)); // Destroy the database. base::RunLoop run_loop; leveldb()->Destroy(std::move(directory), "test", Capture(&error, run_loop.QuitClosure())); run_loop.Run(); EXPECT_EQ(mojom::DatabaseError::OK, error); } { filesystem::mojom::DirectoryPtr directory; temp_directory->Clone(MakeRequest(&directory)); // Reconnect to the database should fail. mojom::LevelDBDatabaseAssociatedPtr database; base::RunLoop run_loop; leveldb()->Open(std::move(directory), "test", MakeRequest(&database), Capture(&error, run_loop.QuitClosure())); run_loop.Run(); EXPECT_EQ(mojom::DatabaseError::INVALID_ARGUMENT, error); } { filesystem::mojom::DirectoryPtr directory; temp_directory->Clone(MakeRequest(&directory)); // Destroying a non-existant database should still succeed. base::RunLoop run_loop; leveldb()->Destroy(std::move(directory), "test", Capture(&error, run_loop.QuitClosure())); run_loop.Run(); EXPECT_EQ(mojom::DatabaseError::OK, error); } } TEST_F(LevelDBServiceTest, GetSnapshotSimple) { mojom::DatabaseError error; mojom::LevelDBDatabaseAssociatedPtr database; LevelDBSyncOpenInMemory(leveldb().get(), MakeRequest(&database), &error); EXPECT_EQ(mojom::DatabaseError::OK, error); base::UnguessableToken snapshot; base::RunLoop run_loop; database->GetSnapshot(CaptureConstRef(&snapshot, run_loop.QuitClosure())); run_loop.Run(); EXPECT_FALSE(snapshot.is_empty()); } TEST_F(LevelDBServiceTest, GetFromSnapshots) { mojom::DatabaseError error; mojom::LevelDBDatabaseAssociatedPtr database; LevelDBSyncOpenInMemory(leveldb().get(), MakeRequest(&database), &error); EXPECT_EQ(mojom::DatabaseError::OK, error); // Write a key to the database. error = mojom::DatabaseError::INVALID_ARGUMENT; DatabaseSyncPut(database.get(), "key", "value", &error); EXPECT_EQ(mojom::DatabaseError::OK, error); // Take a snapshot where key=value. base::UnguessableToken key_value_snapshot; base::RunLoop run_loop; database->GetSnapshot( CaptureConstRef(&key_value_snapshot, run_loop.QuitClosure())); run_loop.Run(); // Change key to "yek". error = mojom::DatabaseError::INVALID_ARGUMENT; DatabaseSyncPut(database.get(), "key", "yek", &error); EXPECT_EQ(mojom::DatabaseError::OK, error); // (Ensure this change is live on the database.) error = mojom::DatabaseError::INVALID_ARGUMENT; std::vector value; DatabaseSyncGet(database.get(), "key", &error, &value); EXPECT_EQ(mojom::DatabaseError::OK, error); EXPECT_EQ("yek", Uint8VectorToStdString(value)); // But if we were to read from the snapshot, we'd still get value. error = mojom::DatabaseError::INVALID_ARGUMENT; value.clear(); base::RunLoop run_loop2; database->GetFromSnapshot( key_value_snapshot, StdStringToUint8Vector("key"), CaptureConstRef(&error, &value, run_loop2.QuitClosure())); run_loop2.Run(); EXPECT_EQ(mojom::DatabaseError::OK, error); EXPECT_EQ("value", Uint8VectorToStdString(value)); } TEST_F(LevelDBServiceTest, InvalidArgumentOnInvalidSnapshot) { mojom::LevelDBDatabaseAssociatedPtr database; mojom::DatabaseError error = mojom::DatabaseError::INVALID_ARGUMENT; LevelDBSyncOpenInMemory(leveldb().get(), MakeRequest(&database), &error); EXPECT_EQ(mojom::DatabaseError::OK, error); base::UnguessableToken invalid_snapshot = base::UnguessableToken::Create(); error = mojom::DatabaseError::OK; std::vector value; base::RunLoop run_loop; database->GetFromSnapshot( invalid_snapshot, StdStringToUint8Vector("key"), CaptureConstRef(&error, &value, run_loop.QuitClosure())); run_loop.Run(); EXPECT_EQ(mojom::DatabaseError::INVALID_ARGUMENT, error); } TEST_F(LevelDBServiceTest, MemoryDBReadWrite) { mojom::LevelDBDatabaseAssociatedPtr database; mojom::DatabaseError error = mojom::DatabaseError::INVALID_ARGUMENT; LevelDBSyncOpenInMemory(leveldb().get(), MakeRequest(&database), &error); EXPECT_EQ(mojom::DatabaseError::OK, error); // Write a key to the database. error = mojom::DatabaseError::INVALID_ARGUMENT; DatabaseSyncPut(database.get(), "key", "value", &error); EXPECT_EQ(mojom::DatabaseError::OK, error); // Read the key back from the database. error = mojom::DatabaseError::INVALID_ARGUMENT; std::vector value; DatabaseSyncGet(database.get(), "key", &error, &value); EXPECT_EQ(mojom::DatabaseError::OK, error); EXPECT_EQ("value", Uint8VectorToStdString(value)); // Delete the key from the database. error = mojom::DatabaseError::INVALID_ARGUMENT; base::RunLoop run_loop; database->Delete(StdStringToUint8Vector("key"), Capture(&error, run_loop.QuitClosure())); run_loop.Run(); EXPECT_EQ(mojom::DatabaseError::OK, error); // Read the key back from the database. error = mojom::DatabaseError::INVALID_ARGUMENT; value.clear(); DatabaseSyncGet(database.get(), "key", &error, &value); EXPECT_EQ(mojom::DatabaseError::NOT_FOUND, error); EXPECT_EQ("", Uint8VectorToStdString(value)); } TEST_F(LevelDBServiceTest, Prefixed) { // Open an in memory database for speed. mojom::DatabaseError error = mojom::DatabaseError::INVALID_ARGUMENT; mojom::LevelDBDatabaseAssociatedPtr database; LevelDBSyncOpenInMemory(leveldb().get(), MakeRequest(&database), &error); EXPECT_EQ(mojom::DatabaseError::OK, error); const std::string prefix("prefix"); std::vector key_values; // Completely empty database. error = mojom::DatabaseError::INVALID_ARGUMENT; DatabaseSyncGetPrefixed(database.get(), prefix, &error, &key_values); EXPECT_EQ(mojom::DatabaseError::OK, error); EXPECT_TRUE(key_values.empty()); // No values with our prefix, but values before and after. error = mojom::DatabaseError::INVALID_ARGUMENT; DatabaseSyncPut(database.get(), "a-before-prefix", "value", &error); EXPECT_EQ(mojom::DatabaseError::OK, error); error = mojom::DatabaseError::INVALID_ARGUMENT; DatabaseSyncPut(database.get(), "z-after-prefix", "value", &error); EXPECT_EQ(mojom::DatabaseError::OK, error); key_values.clear(); error = mojom::DatabaseError::INVALID_ARGUMENT; DatabaseSyncGetPrefixed(database.get(), prefix, &error, &key_values); EXPECT_EQ(mojom::DatabaseError::OK, error); EXPECT_TRUE(key_values.empty()); // One value with the exact prefix. DatabaseSyncPut(database.get(), prefix, "value", &error); EXPECT_EQ(mojom::DatabaseError::OK, error); error = mojom::DatabaseError::INVALID_ARGUMENT; key_values.clear(); DatabaseSyncGetPrefixed(database.get(), prefix, &error, &key_values); EXPECT_EQ(mojom::DatabaseError::OK, error); EXPECT_EQ(1u, key_values.size()); EXPECT_EQ("prefix", Uint8VectorToStdString(key_values[0]->key)); EXPECT_EQ("value", Uint8VectorToStdString(key_values[0]->value)); // Multiple values with starting with the prefix. DatabaseSyncPut(database.get(), (prefix + "2"), "value2", &error); EXPECT_EQ(mojom::DatabaseError::OK, error); error = mojom::DatabaseError::INVALID_ARGUMENT; key_values.clear(); DatabaseSyncGetPrefixed(database.get(), prefix, &error, &key_values); EXPECT_EQ(mojom::DatabaseError::OK, error); EXPECT_EQ(2u, key_values.size()); EXPECT_EQ("prefix", Uint8VectorToStdString(key_values[0]->key)); EXPECT_EQ("value", Uint8VectorToStdString(key_values[0]->value)); EXPECT_EQ("prefix2", Uint8VectorToStdString(key_values[1]->key)); EXPECT_EQ("value2", Uint8VectorToStdString(key_values[1]->value)); // Delete the prefixed values. error = mojom::DatabaseError::INVALID_ARGUMENT; DatabaseSyncDeletePrefixed(database.get(), prefix, &error); EXPECT_EQ(mojom::DatabaseError::OK, error); error = mojom::DatabaseError::INVALID_ARGUMENT; key_values.clear(); DatabaseSyncGetPrefixed(database.get(), prefix, &error, &key_values); EXPECT_EQ(mojom::DatabaseError::OK, error); EXPECT_TRUE(key_values.empty()); // Make sure the others are not deleted. std::vector value; DatabaseSyncGet(database.get(), "a-before-prefix", &error, &value); EXPECT_EQ(mojom::DatabaseError::OK, error); EXPECT_EQ("value", Uint8VectorToStdString(value)); value.clear(); DatabaseSyncGet(database.get(), "z-after-prefix", &error, &value); EXPECT_EQ(mojom::DatabaseError::OK, error); EXPECT_EQ("value", Uint8VectorToStdString(value)); // A key having our prefix, but no key matching it exactly. // Even thought there is no exact matching key, GetPrefixed // and DeletePrefixed still operate on the values. error = mojom::DatabaseError::INVALID_ARGUMENT; DatabaseSyncPut(database.get(), (prefix + "2"), "value2", &error); EXPECT_EQ(mojom::DatabaseError::OK, error); error = mojom::DatabaseError::INVALID_ARGUMENT; key_values.clear(); DatabaseSyncGetPrefixed(database.get(), prefix, &error, &key_values); EXPECT_EQ(mojom::DatabaseError::OK, error); EXPECT_EQ(1u, key_values.size()); EXPECT_EQ("prefix2", Uint8VectorToStdString(key_values[0]->key)); EXPECT_EQ("value2", Uint8VectorToStdString(key_values[0]->value)); error = mojom::DatabaseError::INVALID_ARGUMENT; DatabaseSyncDeletePrefixed(database.get(), prefix, &error); EXPECT_EQ(mojom::DatabaseError::OK, error); error = mojom::DatabaseError::INVALID_ARGUMENT; key_values.clear(); DatabaseSyncGetPrefixed(database.get(), prefix, &error, &key_values); EXPECT_EQ(mojom::DatabaseError::OK, error); EXPECT_TRUE(key_values.empty()); } } // namespace } // namespace leveldb