Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
c72db70
RavenDB-27141 Add TestServerOptions and DriverCloseError
poissoncorp Aug 26, 2026
cd8f6c9
RavenDB-27141 Fix driver lifecycle and give test servers their own de…
poissoncorp Aug 26, 2026
99e51c8
RavenDB-27141 Document in-memory servers, secured embedded and the De…
poissoncorp Aug 26, 2026
7a6a4ed
RavenDB-27141 Add opt-in caller-name database naming
poissoncorp Aug 26, 2026
8b7918f
RavenDB-27141 Add opt-in per-process unique database names
poissoncorp Aug 26, 2026
754014d
RavenDB-27141 Add opt-in strict licence checking for test servers
poissoncorp Aug 26, 2026
89bd133
RavenDB-27141 Make internal helpers private with deprecated aliases
poissoncorp Aug 26, 2026
a0d7441
RavenDB-27141 Document opt-in switches and add the 7.2.6 changelog
poissoncorp Aug 26, 2026
95bfaee
RavenDB-27141 Document HTTP traffic inspection instead of porting Use…
poissoncorp Aug 26, 2026
0ddc458
RavenDB-27141 Prepare 7.2.5.post3 release
poissoncorp Aug 26, 2026
d136424
RavenDB-27141 Extract shared certificate and driver-reset test helpers
poissoncorp Aug 26, 2026
8bce1c0
RavenDB-27141 Cover error paths and make test-database deletion unit-…
poissoncorp Aug 26, 2026
783ca40
RavenDB-27141 Add end-to-end coverage against a real embedded server
poissoncorp Aug 26, 2026
70a5c88
RavenDB-27141 Keep release notes in GitHub Releases instead of a chan…
poissoncorp Aug 26, 2026
3e325df
RavenDB-27141 Add a secured embedded lab and cover the new hooks in l…
poissoncorp Aug 26, 2026
fbcd89e
RavenDB-27141 Trim comments to what the code cannot say itself
poissoncorp Aug 26, 2026
b36d8a2
RavenDB-27141 Fix scope defects and consolidate test setup found in r…
poissoncorp Aug 26, 2026
bd5c9a3
RavenDB-27141 Document the override hooks, name the indexing default,…
poissoncorp Aug 27, 2026
c1536b6
RavenDB-27141 Drop dead scaffolding in close and privatize the config…
poissoncorp Aug 27, 2026
a49c584
RavenDB-27141 Give the server bootstrap one store builder and two nam…
poissoncorp Aug 27, 2026
8fce25d
RavenDB-27141 Use the shared environment helper in the secured attach…
poissoncorp Aug 27, 2026
7e94fa4
RavenDB-27141 Cover the attach path locally with a second embedded se…
poissoncorp Aug 27, 2026
9db9aa1
RavenDB-27141 Document strict licensing as an options switch, not jus…
poissoncorp Aug 27, 2026
1c69de2
RavenDB-27141 Leave licensing alone: drop the strict-licence opt-in
poissoncorp Aug 27, 2026
161f5c9
RavenDB-27141 Stop the lifecycle examples from contradicting the life…
poissoncorp Aug 27, 2026
48ff88e
RavenDB-27141 Detect synthetic frame names by shape instead of listin…
poissoncorp Aug 27, 2026
497a228
RavenDB-27141 Wait for the user with no timeout unless one is given
poissoncorp Aug 27, 2026
db02942
RavenDB-27141 Put the indexing default in the signature and stop trim…
poissoncorp Aug 27, 2026
b1f1e63
RavenDB-27141 Name test databases after the calling test by default
poissoncorp Aug 27, 2026
c9b1e57
RavenDB-27141 Keep caller-name database naming opt-in
poissoncorp Aug 27, 2026
0bb3023
RavenDB-27141 Move the database counter next to its only user and giv…
poissoncorp Aug 27, 2026
f01f605
RavenDB-27141 Trim a comment and a docstring to what they need to say
poissoncorp Aug 27, 2026
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
3 changes: 3 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,9 @@ jobs:
- name: Run Lab 03 (seeding + indexes)
run: python labs/03_seeding_indexes.py

- name: Run Lab 05 (secured embedded)
run: python labs/05_secured_embedded.py

attach:
# Path C: attach to a RavenDB server running in Docker, with NO .NET installed. Proves the
# driver needs no runtime when it does not boot the embedded server.
Expand Down
169 changes: 155 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,15 +58,68 @@ upgrading to a new RavenDB minor version.

Runnable walkthrough: [Lab 02 — isolated embedded databases](labs/02-embedded-per-test.md).

#### Where test data lives

Embedded test servers run in memory, so the create-and-delete-a-database cycle behind every
`get_document_store()` call never lands on disk. Only the server log is written, to a scratch
directory the driver removes when the interpreter exits.

Two consequences worth knowing:

- A large fixture seeded in `setup_database` is held in RAM rather than spilled to disk.
- Nothing survives a server restart, and there are no files to inspect after a failing run.

To go back to disk-backed storage, either set the argument yourself, which the driver never
overrides:

```python
options = TestServerOptions()
options.command_line_args.append("--RunInMemory=false")
options.data_directory = "/path/you/choose"
RavenTestDriver.configure_server(options)
```

or switch it off on the options themselves:

```python
options = TestServerOptions()
options.run_in_memory = False
RavenTestDriver.configure_server(options)
```

The driver also redirects the data directory when you leave it at the `ravendb-embedded` default,
which otherwise points inside the installed package. Set `data_directory` explicitly and the
driver leaves your path alone.

Nothing closes the shared test server before interpreter exit. Call
`RavenTestDriver.stop_test_server()` from a session-scoped fixture teardown when you want that
cost inside your test run rather than after the runner prints its summary; the server starts
again on the next `get_document_store()`.

#### Secured embedded server

Pass a server certificate together with the client PEM the tests authenticate with, and the driver
wires that client material into every store it hands out:

```python
options = TestServerOptions()
options.secured("server.pfx", "client.pem", ca_certificate_path="ca.crt")
RavenTestDriver.configure_server(options)
```

The client PEM is required here: a secured server the test client cannot authenticate to is
rejected before the server starts.

Runnable walkthrough: [Lab 05 — secured embedded server](labs/05-secured-embedded.md).

### On-demand self-contained server

Let the driver download, cache, and manage the self-contained build for the current platform:

```python
from ravendb_embedded import ServerOptions
from ravendb_test_driver import RavenTestDriver
from ravendb_test_driver import RavenTestDriver, TestServerOptions

options = ServerOptions()
options = TestServerOptions()
options.with_auto_downloaded_server()
RavenTestDriver.configure_server(options)

Expand All @@ -75,6 +128,9 @@ with RavenTestDriver() as driver:
...
```

`TestServerOptions` is a `ravendb_embedded.ServerOptions` that names the intent. `configure_server`
still accepts a plain `ServerOptions`, and the driver applies the same test defaults either way.

The same test configuration works across supported Windows, Linux, and macOS machines because the
operating system and architecture are detected at runtime. The first run downloads 100 MB+;
later runs reuse `~/.cache/ravendb-embedded`. Pass `cache_root` to
Expand Down Expand Up @@ -128,29 +184,52 @@ The equivalent environment variables are:
`trust_store_path` or `RAVENDB_TEST_SERVER_CA` is needed when the server's CA is not already
trusted by the test machine.

Explicit configuration wins over the environment. If a test calls `configure_server()` and
`RAVENDB_TEST_SERVER_URL` is also set, the environment variable is ignored and a warning is
emitted, because the driver creates and hard-deletes databases on whichever server it uses. To let
the environment pick the server, do not call `configure_server()`.

Runnable walkthrough: [Lab 01 — Docker, Testcontainers, and shared servers](labs/01-attach-to-server.md).

## Test lifecycle

Create a `RavenTestDriver` for the test or fixture, then close every returned store. A context
manager handles both steps:
Create a `RavenTestDriver` for the test or fixture and register its cleanup once. Closing the driver
closes any store you left open and deletes its database, so nothing leaks if a test throws halfway
and you never have to close a store yourself:

```python
from unittest import TestCase
from ravendb_test_driver import RavenTestDriver


class TestPeople(TestCase):
def setUp(self):
self.driver = RavenTestDriver()
self.addCleanup(self.driver.close) # the only cleanup line you need

def test_stores_a_person(self):
with RavenTestDriver() as driver:
with driver.get_document_store() as store:
with store.open_session() as session:
session.store({"name": "John"}, "people/1")
session.save_changes()
store = self.driver.get_document_store()
with store.open_session() as session:
session.store({"name": "John"}, "people/1")
session.save_changes()
```

Closing stores yourself is still fine, and it is what you want when one test creates several
databases and the order they go away in matters:

```python
with RavenTestDriver() as driver:
with driver.get_document_store() as store:
...
```

Each `get_document_store()` call creates a new database. Closing the store deletes it, which keeps
tests independent even when they share one RavenDB server process.
tests independent even when they share one RavenDB server process. Database names are generated
(`test_1`, `test_2`, ...) from a process-wide counter; treat them as opaque and read
`store.database` rather than assuming a name, or pass `database="..."` to pick the stem yourself.

If closing the driver hits errors, it raises `DriverCloseError`, a `RuntimeError` subclass whose
`exceptions` attribute holds every original exception rather than a joined string.

## Seed data and wait for indexing

Expand All @@ -165,14 +244,75 @@ class PeopleTestDriver(RavenTestDriver):
session.save_changes()
```

Override `pre_configure_database(self, database_record)` to change the database itself before it is
created, for settings, revisions, expiration, encryption or topology:

```python
class PeopleTestDriver(RavenTestDriver):
def pre_configure_database(self, database_record):
database_record.settings["Indexing.MapTimeoutInSec"] = "30"
```

Use `GetDocumentStoreOptions.wait_for_indexing_timeout` when a store should not be returned until
indexing settles, or call `wait_for_indexing(store)` directly.
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.

`wait_for_user_to_continue_the_test(store)` opens RavenDB Studio and pauses the test for manual
inspection.
## Pausing for manual inspection

`wait_for_user_to_continue_the_test(store)` prints the Studio URL for that database, opens a
browser, and blocks until a document with the id `Debug/Done` shows up in the database. Store one
from Studio to continue; the driver deletes the marker so a later wait on the same store still
blocks.

The wait is unbounded, because you are the one looking at Studio. Pass a `timeout` to bound it and
get a `TimeoutException` instead. A CI job protects itself from a call left in committed code with
`RAVENDB_TEST_WAIT_FOR_USER=0`, which skips the wait entirely.

Runnable walkthrough: [Lab 03 — seeding and indexes](labs/03-seeding-indexes.md).

## Opt-in switches

Defaults are chosen so an existing suite keeps working. These are the knobs worth knowing:

| Switch | Default | What it does |
|--------|---------|--------------|
| `TestServerOptions.run_in_memory` | `True` | Runs embedded test servers in memory. Set `False` on the options you pass to `configure_server` to go back to disk |
| `RavenTestDriver.use_caller_name_for_database` | `False` | Names databases after the calling test (`test_stores_a_person_3`) instead of `test_3` |
| `RAVENDB_TEST_UNIQUE_DB_NAMES` | off | Adds the process id to database names, so parallel runners sharing one attached server stop colliding |
| `RAVENDB_TEST_WAIT_FOR_USER` | on | Set to `0` to skip `wait_for_user_to_continue_the_test` entirely |

Anything describing the server itself belongs on the options object; the environment variables exist
so a CI job can flip a switch without editing test code.

Caller-name databases are sanitized to `[A-Za-z0-9_.-]`, and fall back to `test` when the caller has
no usable name, such as a lambda or a module-level call.

## Inspecting HTTP traffic

The client sends its requests through `requests`, which honors `HTTP_PROXY`, so any interception
proxy works without driver support:

```bash
HTTP_PROXY=http://127.0.0.1:8080 python -m unittest
```

On Windows, proxy bypass rules skip loopback addresses, so traffic to `127.0.0.1` never reaches the
proxy. Bind the test server to the machine name instead, which also needs unsecured access to be
allowed on the private network:

```python
import socket

from ravendb_test_driver import RavenTestDriver, TestServerOptions

options = TestServerOptions()
options.server_url = f"http://{socket.gethostname()}:0"
options.command_line_args.append("--Security.UnsecuredAccessAllowed=PrivateNetwork")
RavenTestDriver.configure_server(options)
```

That pair is what `TestServerOptions.UseFiddler()` does in the .NET test driver.

## Labs

| Lab | Scenario | Needs system .NET? |
Expand All @@ -181,6 +321,7 @@ Runnable walkthrough: [Lab 03 — seeding and indexes](labs/03-seeding-indexes.m
| [02](labs/02-embedded-per-test.md) | Default embedded server and isolated databases | Yes |
| [03](labs/03-seeding-indexes.md) | Seed data and wait for real indexing | Yes |
| [04](labs/04-embedded-no-dotnet.md) | On-demand self-contained server | No |
| [05](labs/05-secured-embedded.md) | Secured embedded server with client certificates | Yes |

The runnable scripts live in this repository rather than `site-packages`. Clone or download the
repository, install the package, and run them from the repository root. See the
Expand Down
34 changes: 30 additions & 4 deletions labs/02-embedded-per-test.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,43 @@ from ravendb_test_driver import RavenTestDriver
class TestThings(TestCase):
def setUp(self):
self.driver = RavenTestDriver()
self.addCleanup(self.driver.close) # the only cleanup line you need

def test_it(self):
with self.driver.get_document_store() as store: # fresh isolated database
with store.open_session() as session:
session.store({"name": "John"}, "people/1")
session.save_changes()
store = self.driver.get_document_store() # fresh isolated database, never closed by hand
with store.open_session() as session:
session.store({"name": "John"}, "people/1")
session.save_changes()
```

Two `get_document_store()` calls give two different databases, so data written to one is invisible
to the other. That isolation is what keeps tests independent.

You do not have to close the stores. Closing the driver closes whatever is still open and deletes
those databases, so registering the driver's cleanup once covers every test in the class, including
the ones that throw halfway through.

Outside a test class, a `with` block does the same thing:

```python
with RavenTestDriver() as driver:
store = driver.get_document_store()
... # no store.close() anywhere
# leaving the block closed the store and deleted its database
```

Closing stores yourself is still fine, and it is what you want when a single test creates several
databases and you care about the order they go away in.

The embedded server is shared by every driver in the process and runs in memory. Nothing closes it
before the interpreter exits, so call `RavenTestDriver.stop_test_server()` from your runner's
teardown when you want that cost inside the run:

```python
def pytest_sessionfinish(session, exitstatus):
RavenTestDriver.stop_test_server()
```

## Takeaway

No server to manage in your tests: the driver runs one and gives each test its own database. To
Expand Down
14 changes: 13 additions & 1 deletion labs/02_embedded_per_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,19 @@ def main() -> None:
with second.open_session() as session:
assert session.load("people/1", dict) is None

print("Lab 02 OK: two stores, two isolated databases, no cross-test leakage.")
# You do not have to close the stores. Leaving the driver's `with` block closes whatever is
# still open and deletes those databases, so a test that throws halfway still cleans up.
with RavenTestDriver() as driver:
forgotten = driver.get_document_store() # no forgotten.close() anywhere
assert forgotten.database.startswith("test_"), forgotten.database

assert driver.disposed

# The server is shared by every driver in the process, and nothing closes it before the
# interpreter exits. Call this from your runner's teardown to keep that cost inside the run.
RavenTestDriver.stop_test_server()

print("Lab 02 OK: two stores, two isolated databases, driver-level cleanup, server stopped.")


if __name__ == "__main__":
Expand Down
20 changes: 18 additions & 2 deletions labs/03-seeding-indexes.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,23 @@ RavenDB indexes are updated asynchronously, so right after you write, an index q
stale (empty) results. `wait_for_indexing()` blocks until no index is stale, making index-backed
assertions deterministic instead of flaky.

## Configure the database itself

`setup_database()` runs against a database that already exists. To change the database before it is
created, override `pre_configure_database()` and edit the `DatabaseRecord`: settings, revisions,
expiration, encryption and topology are all in reach.

```python
class SeedingDriver(RavenTestDriver):
def pre_configure_database(self, database_record): # before the database is created
database_record.settings["Indexing.MapTimeoutInSec"] = "30"
```

`wait_for_indexing()` also waits for a side-by-side index deployment to finish, so a test that
redeploys an index definition queries the new one rather than the index it replaced.

## Takeaway

`setup_database()` is the single place to seed data and register indexes for every test database;
`wait_for_indexing()` removes the race between writing and querying an index.
`pre_configure_database()` shapes the database, `setup_database()` is the single place to seed data
and register indexes for every test database, and `wait_for_indexing()` removes the race between
writing and querying an index.
14 changes: 11 additions & 3 deletions labs/03_seeding_indexes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@

For: tests that need pre-seeded data and a defined index, and must wait for indexing to settle
before asserting. Override setup_database() to seed and create the index for every database the
driver hands out; call wait_for_indexing() before querying so the assertion is not racing the
indexer. Boots the embedded server (needs .NET).
driver hands out, pre_configure_database() to change the database itself before it is created,
and call wait_for_indexing() before querying so the assertion is not racing the indexer. Boots
the embedded server (needs .NET).

Run: python labs/03_seeding_indexes.py
"""

from ravendb import GetDatabaseRecordOperation
from ravendb.documents.indexes.abstract_index_creation_tasks import AbstractIndexCreationTask

from ravendb_test_driver import RavenTestDriver
Expand All @@ -26,6 +28,9 @@ def __init__(self):


class SeedingDriver(RavenTestDriver):
def pre_configure_database(self, database_record) -> None: # before the database is created
database_record.settings["Indexing.MapTimeoutInSec"] = "30"

def setup_database(self, store) -> None: # runs for every database the driver creates
store.execute_index(People_ByName())
with store.open_session() as session:
Expand All @@ -36,12 +41,15 @@ def setup_database(self, store) -> None: # runs for every database the driver c
def main() -> None:
with SeedingDriver() as driver:
with driver.get_document_store() as store:
record = store.maintenance.server.send(GetDatabaseRecordOperation(store.database))
assert record.settings["Indexing.MapTimeoutInSec"] == "30" # set before creation

driver.wait_for_indexing(store) # block until the index is no longer stale
with store.open_session() as session:
hits = list(session.query_index_type(People_ByName, Person).where_equals("name", "Seeded"))
assert len(hits) == 1 and hits[0].name == "Seeded", hits

print("Lab 03 OK: setup_database seeded data + index, wait_for_indexing settled, query returned it.")
print("Lab 03 OK: database pre-configured, data + index seeded, wait_for_indexing settled, query returned it.")


if __name__ == "__main__":
Expand Down
Loading
Loading