Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Package up base db and atomic db test suites into re-usable classes #1813

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion eth/db/backends/base.py
Expand Up @@ -40,7 +40,7 @@ def delete(self, key: bytes) -> None:
try:
del self[key]
except KeyError:
return None
pass

def __iter__(self) -> Iterator[bytes]:
raise NotImplementedError("By default, DB classes cannot be iterated.")
Expand Down
2 changes: 2 additions & 0 deletions eth/db/backends/level.py
Expand Up @@ -62,6 +62,8 @@ def _exists(self, key: bytes) -> bool:
return self.db.get(key) is not None

def __delitem__(self, key: bytes) -> None:
if self.db.get(key) is None:
raise KeyError(key)
self.db.delete(key)

@contextmanager
Expand Down
Empty file added eth/tools/db/__init__.py
Empty file.
158 changes: 158 additions & 0 deletions eth/tools/db/atomic.py
@@ -0,0 +1,158 @@
import pytest

from eth_utils import ValidationError

from eth.abc import AtomicDatabaseAPI


class AtomicDatabaseBatchAPITestSuite:
def test_atomic_batch_set_and_get(self, atomic_db: AtomicDatabaseAPI) -> None:
with atomic_db.atomic_batch() as batch:
batch.set(b'1', b'2')
assert batch.get(b'1') == b'2'

assert atomic_db.get(b'1') == b'2'

def test_atomic_db_cannot_recursively_batch(self, atomic_db: AtomicDatabaseAPI) -> None:
with atomic_db.atomic_batch() as batch:
assert not hasattr(batch, 'atomic_batch')

def test_atomic_db_with_set_and_delete_batch(self, atomic_db: AtomicDatabaseAPI) -> None:
atomic_db[b'key-1'] = b'origin'

with atomic_db.atomic_batch() as batch:
batch.delete(b'key-1')

assert b'key-1' not in batch
with pytest.raises(KeyError):
assert batch[b'key-1']

with pytest.raises(KeyError):
atomic_db[b'key-1']

def test_atomic_db_unbatched_sets_are_immediate(self, atomic_db: AtomicDatabaseAPI) -> None:
atomic_db[b'1'] = b'A'

with atomic_db.atomic_batch() as batch:
# Unbatched changes are immediate, and show up in batch reads
atomic_db[b'1'] = b'B'
assert batch[b'1'] == b'B'

batch[b'1'] = b'C1'

# It doesn't matter what changes happen underlying, all reads now
# show the write applied to the batch db handle
atomic_db[b'1'] = b'C2'
assert batch[b'1'] == b'C1'

# the batch write should overwrite any intermediate changes
assert atomic_db[b'1'] == b'C1'

def test_atomic_db_unbatched_deletes_are_immediate(self, atomic_db: AtomicDatabaseAPI) -> None:
atomic_db[b'1'] = b'A'

with atomic_db.atomic_batch() as batch:
assert b'1' in batch

# Unbatched changes are immediate, and show up in batch reads
del atomic_db[b'1']

assert b'1' not in batch

batch[b'1'] = b'C1'

# It doesn't matter what changes happen underlying, all reads now
# show the write applied to the batch db handle
atomic_db[b'1'] = b'C2'
assert batch[b'1'] == b'C1'

# the batch write should overwrite any intermediate changes
assert atomic_db[b'1'] == b'C1'

def test_atomic_db_cannot_use_batch_after_context(self, atomic_db: AtomicDatabaseAPI) -> None:
atomic_db[b'1'] = b'A'

with atomic_db.atomic_batch() as batch:
batch[b'1'] = b'B'

# set
with pytest.raises(ValidationError):
batch[b'1'] = b'C'

with pytest.raises(ValidationError):
batch.set(b'1', b'C')

# get
with pytest.raises(ValidationError):
batch[b'1']

with pytest.raises(ValidationError):
batch.get(b'1')

# exists
with pytest.raises(ValidationError):
b'1' in batch

with pytest.raises(ValidationError):
batch.exists(b'1')

# delete
with pytest.raises(ValidationError):
del batch[b'1']

with pytest.raises(ValidationError):
batch.delete(b'1')

# none of the invalid changes above should change the original db
assert atomic_db[b'1'] == b'B'

def test_atomic_db_with_reverted_delete_batch(self, atomic_db: AtomicDatabaseAPI) -> None:
class CustomException(Exception):
pass

atomic_db[b'key-1'] = b'origin'

with pytest.raises(CustomException):
with atomic_db.atomic_batch() as batch:
batch.delete(b'key-1')

assert b'key-1' not in batch
with pytest.raises(KeyError):
assert batch[b'key-1']

raise CustomException('pretend something went wrong')

assert atomic_db[b'key-1'] == b'origin'

def test_atomic_db_temporary_state_dropped_across_batches(self,
atomic_db: AtomicDatabaseAPI) -> None:
class CustomException(Exception):
pass

atomic_db[b'key-1'] = b'origin'

with pytest.raises(CustomException):
with atomic_db.atomic_batch() as batch:
batch.delete(b'key-1')
batch.set(b'key-2', b'val-2')
raise CustomException('pretend something went wrong')

with atomic_db.atomic_batch() as batch:
assert batch[b'key-1'] == b'origin'
assert b'key-2' not in batch

def test_atomic_db_with_exception_batch(self, atomic_db: AtomicDatabaseAPI) -> None:
atomic_db.set(b'key-1', b'value-1')

try:
with atomic_db.atomic_batch() as batch:
batch.set(b'key-1', b'new-value-1')
batch.set(b'key-2', b'value-2')
raise Exception
except Exception:
pass

assert atomic_db.get(b'key-1') == b'value-1'

with pytest.raises(KeyError):
atomic_db[b'key-2']
76 changes: 76 additions & 0 deletions eth/tools/db/base.py
@@ -0,0 +1,76 @@
import pytest

from eth.abc import DatabaseAPI


class DatabaseAPITestSuite:
def test_database_api_get(self, db: DatabaseAPI) -> None:
db[b'key-1'] = b'value-1'
assert db.get(b'key-1') == b'value-1'

def test_database_api_item_getter(self, db: DatabaseAPI) -> None:
db[b'key-1'] = b'value-1'
assert db[b'key-1'] == b'value-1'

def test_database_api_get_missing_key(self, db: DatabaseAPI) -> None:
assert b'key-1' not in db
assert db.get(b'key-1') is None

def test_database_api_item_getter_missing_key(self, db: DatabaseAPI) -> None:
assert b'key-1' not in db
with pytest.raises(KeyError):
db[b'key-1']

def test_database_api_set(self, db: DatabaseAPI) -> None:
db[b'key-1'] = b'value-1'
assert db[b'key-1'] == b'value-1'
db[b'key-1'] = b'value-2'
assert db[b'key-1'] == b'value-2'

def test_database_api_item_setter(self, db: DatabaseAPI) -> None:
db.set(b'key-1', b'value-1')
assert db[b'key-1'] == b'value-1'
db.set(b'key-1', b'value-2')
assert db[b'key-1'] == b'value-2'

def test_database_api_exists(self, db: DatabaseAPI) -> None:
assert db.exists(b'key-1') is False

db[b'key-1'] = b'value-1'

assert db.exists(b'key-1') is True

def test_database_api_contains_checking(self, db: DatabaseAPI) -> None:
assert b'key-1' not in db

db[b'key-1'] = b'value-1'

assert b'key-1' in db

def test_database_api_delete(self, db: DatabaseAPI) -> None:
db[b'key-1'] = b'value-1'

assert b'key-1' in db

db.delete(b'key-1')

assert not db.exists(b'key-1')
assert b'key-1' not in db

def test_database_api_item_delete(self, db: DatabaseAPI) -> None:
db[b'key-1'] = b'value-1'

assert b'key-1' in db

del db[b'key-1']

assert b'key-1' not in db

def test_database_api_delete_missing_key(self, db: DatabaseAPI) -> None:
assert b'key-1' not in db
db.delete(b'key-1')

def test_database_api_item_delete_missing_key(self, db: DatabaseAPI) -> None:
assert b'key-1' not in db
with pytest.raises(KeyError):
del db[b'key-1']
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised that .delete() doesn't throw an exception but __delitem__ does! It seems like those two should have the same behavior.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized that MutableMapping doesn't have a set, delete or exists API....

1 change: 1 addition & 0 deletions newsfragments/1813.feature.rst
@@ -0,0 +1 @@
Package up test suites for the ``DatabaseAPI`` and ``AtomicDatabaseAPI`` to be class-based to make them reusable by other libaries.
30 changes: 30 additions & 0 deletions tests/database/test_atomic_database_api.py
@@ -0,0 +1,30 @@
import pytest

from eth.db.atomic import AtomicDB
from eth.db.backends.level import LevelDB

from eth.tools.db.base import DatabaseAPITestSuite
from eth.tools.db.atomic import AtomicDatabaseBatchAPITestSuite


@pytest.fixture(params=['atomic', 'level'])
def atomic_db(request, tmpdir):
if request.param == 'atomic':
return AtomicDB()
elif request.param == 'level':
return LevelDB(db_path=tmpdir.mkdir("level_db_path"))
else:
raise ValueError("Unexpected database type: {}".format(request.param))


@pytest.fixture
def db(atomic_db):
return atomic_db


class TestAtomicDatabaseBatchAPI(AtomicDatabaseBatchAPITestSuite):
pass


class TestAtomicDatabaseAPI(DatabaseAPITestSuite):
pass
112 changes: 0 additions & 112 deletions tests/database/test_atomic_db.py

This file was deleted.