Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,30 @@ class PeopleTestDriver(RavenTestDriver):
database_record.settings["Indexing.MapTimeoutInSec"] = "30"
```

Override `database_dump_file_path(self)` to seed every database from a `.ravendbdump` instead of
writing the documents by hand. The import runs before `setup_database`:

```python
class PeopleTestDriver(RavenTestDriver):
def database_dump_file_path(self):
return "fixtures/people.ravendbdump"
```

`database_dump_file_stream(self)` takes an open binary stream instead of a path. The driver asks
for it once, rewinds it between databases, and closes it when the driver closes:

```python
class PeopleTestDriver(RavenTestDriver):
def database_dump_file_stream(self):
return importlib.resources.files("tests.fixtures").joinpath("people.ravendbdump").open("rb")
```

Produce a dump from a database you already have with the client's smuggler:

```python
store.smuggler.for_database(store.database).export(DatabaseSmugglerExportOptions(), "people.ravendbdump")
```

Use `GetDocumentStoreOptions.wait_for_indexing_timeout` when a store should not be returned until
indexing settles, or call `wait_for_indexing(store)` directly. It waits until every applicable
index is non-stale and any side-by-side replacement has been swapped in.
Expand Down
37 changes: 36 additions & 1 deletion ravendb_test_driver/raven_test_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,11 @@
import warnings
import webbrowser
from datetime import timedelta
from typing import Optional, Dict, Any, Callable
from typing import IO, Optional, Dict, Any, Callable
from urllib.parse import quote

from ravendb import (
DatabaseSmugglerImportOptions,
DocumentStore,
Lazy,
CreateDatabaseOperation,
Expand Down Expand Up @@ -57,6 +58,7 @@ def __init__(self) -> None:
self.disposed = False
self._document_stores: Dict[DocumentStore, bool] = {}
self.on_driver_closed: Optional[Callable[[RavenTestDriver], None]] = None
self._dump_stream: Optional[IO[bytes]] = None

def __enter__(self) -> "RavenTestDriver":
return self
Expand Down Expand Up @@ -147,6 +149,7 @@ def __close_event_callback():

store.add_after_close(__close_event_callback)

self._import_database_dump(store, database_record.database_name)
self.setup_database(store)

if options.wait_for_indexing_timeout is not None:
Expand All @@ -156,6 +159,32 @@ def __close_event_callback():

return store

def database_dump_file_path(self) -> Optional[str]:
"""Override to seed every database the driver creates from a .ravendbdump file."""
return None

def database_dump_file_stream(self) -> Optional[IO[bytes]]:
"""Override to seed from an open binary stream. The driver closes it on close()."""
return None

def _import_database_dump(self, store: DocumentStore, database_name: str) -> None:
source = self.database_dump_file_path() or self._owned_dump_stream()
if source is None:
return

operation = store.smuggler.for_database(database_name).import_data(DatabaseSmugglerImportOptions(), source)
operation.wait_for_completion()

def _owned_dump_stream(self) -> Optional[IO[bytes]]:
if self._dump_stream is None:
# Asked once: re-reading the hook per database would leak a stream per store.
self._dump_stream = self.database_dump_file_stream()

if self._dump_stream is not None and self._dump_stream.seekable():
self._dump_stream.seek(0)

return self._dump_stream

@staticmethod
def _delete_test_database(store: DocumentStore, database_name: str) -> None:
"""Hard-delete a test database, ignoring the failures that are not the test's problem."""
Expand Down Expand Up @@ -332,6 +361,12 @@ def close(self) -> None:
except Exception as e:
exceptions.append(e)

if self._dump_stream is not None:
try:
self._dump_stream.close()
except Exception as e:
exceptions.append(e)

if self.on_driver_closed:
# Collected, so a raising callback cannot discard the store-close errors.
try:
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
setup(
name="ravendb-test-driver",
packages=find_packages(exclude=["*.tests.*", "tests", "*.tests", "tests.*"]),
version="7.2.5.post4",
version="7.2.6",
description="RavenDB package for writing integration tests against RavenDB server",
long_description_content_type="text/markdown",
long_description=open("README.md").read(),
Expand All @@ -14,5 +14,5 @@
keywords=["ravendb", "nosql", "database", "test", "driver"],
python_requires=">=3.10",
license_files=["LICENSE"],
install_requires=["ravendb-embedded~=7.2.5", "ravendb~=7.2.3"],
install_requires=["ravendb-embedded~=7.2.5", "ravendb~=7.2.6"],
)
111 changes: 111 additions & 0 deletions tests/test_dump_seeding.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"""Seeding every test database from a .ravendbdump, the way the .NET driver does."""

import io
import tempfile
from pathlib import Path
from unittest import TestCase, skipIf

from ravendb import DatabaseSmugglerExportOptions

from ravendb_test_driver import RavenTestDriver
from tests.support import attach_mode_is_active

skip_without_own_server = skipIf(attach_mode_is_active(), "attach mode: the driver does not own the server")


class Person:
def __init__(self, Id=None, name=None):
self.Id = Id
self.name = name


def _dump_of_two_people(directory: str) -> str:
"""Build a real dump by seeding a database and exporting it."""
dump = str(Path(directory, "people.ravendbdump"))

with RavenTestDriver() as driver:
with driver.get_document_store() as store:
with store.open_session() as session:
session.store(Person(name="Ayende"), "people/1")
session.store(Person(name="Oren"), "people/2")
session.save_changes()

store.smuggler.for_database(store.database).export(DatabaseSmugglerExportOptions(), dump)

return dump


class _FilePathDriver(RavenTestDriver):
dump_path = None

def database_dump_file_path(self):
return self.dump_path


class _StreamDriver(RavenTestDriver):
def __init__(self, dump_path):
super().__init__()
self._path = dump_path
self.opened = 0

def database_dump_file_stream(self):
self.opened += 1
return open(self._path, "rb")


@skip_without_own_server
class TestDumpSeeding(TestCase):
def test_a_dump_path_seeds_every_database(self):
with tempfile.TemporaryDirectory() as directory:
_FilePathDriver.dump_path = _dump_of_two_people(directory)
self.addCleanup(setattr, _FilePathDriver, "dump_path", None)

with _FilePathDriver() as driver:
for _ in range(2):
with driver.get_document_store() as store:
with store.open_session() as session:
self.assertEqual("Ayende", session.load("people/1", Person).name)
self.assertEqual("Oren", session.load("people/2", Person).name)

def test_a_dump_stream_is_read_once_and_reused(self):
with tempfile.TemporaryDirectory() as directory:
dump = _dump_of_two_people(directory)

driver = _StreamDriver(dump)
with driver.get_document_store() as first, driver.get_document_store() as second:
for store in (first, second):
with store.open_session() as session:
self.assertEqual("Ayende", session.load("people/1", Person).name)

# The hook is asked once; the stream is rewound for the second database.
self.assertEqual(1, driver.opened)

driver.close()
self.assertTrue(driver._dump_stream.closed)

def test_no_dump_means_an_empty_database(self):
with RavenTestDriver() as driver:
with driver.get_document_store() as store:
with store.open_session() as session:
self.assertIsNone(session.load("people/1", Person))


class TestDumpHooksAreEmptyByDefault(TestCase):
def test_both_hooks_return_none(self):
driver = RavenTestDriver()

self.assertIsNone(driver.database_dump_file_path())
self.assertIsNone(driver.database_dump_file_stream())

def test_closing_without_a_dump_stream_is_fine(self):
driver = RavenTestDriver()
driver.close()

self.assertIsNone(driver._dump_stream)

def test_a_stream_the_driver_never_used_is_left_alone(self):
driver = RavenTestDriver()
driver._dump_stream = io.BytesIO(b"")
driver.close()

self.assertTrue(driver._dump_stream.closed)
Loading