detquantlib 5.8.0__tar.gz → 5.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {detquantlib-5.8.0 → detquantlib-5.9.0}/PKG-INFO +34 -1
- {detquantlib-5.8.0 → detquantlib-5.9.0}/README.md +33 -0
- detquantlib-5.9.0/detquantlib/snapshot_testing/README.md +502 -0
- detquantlib-5.9.0/detquantlib/snapshot_testing/__init__.py +24 -0
- detquantlib-5.9.0/detquantlib/snapshot_testing/cases.py +195 -0
- detquantlib-5.9.0/detquantlib/snapshot_testing/comparison.py +183 -0
- detquantlib-5.9.0/detquantlib/snapshot_testing/config.py +90 -0
- detquantlib-5.9.0/detquantlib/snapshot_testing/errors.py +11 -0
- detquantlib-5.9.0/detquantlib/snapshot_testing/isolation.py +172 -0
- detquantlib-5.9.0/detquantlib/snapshot_testing/pytest_plugin.py +195 -0
- detquantlib-5.9.0/detquantlib/snapshot_testing/recording.py +492 -0
- detquantlib-5.9.0/detquantlib/snapshot_testing/tables.py +366 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/pyproject.toml +2 -1
- {detquantlib-5.8.0 → detquantlib-5.9.0}/LICENSE.txt +0 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/detquantlib/__init__.py +0 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/detquantlib/assets/__init__.py +0 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/detquantlib/assets/battery.py +0 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/detquantlib/assets/wind_turbine.py +0 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/detquantlib/converters/__init__.py +0 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/detquantlib/converters/definitions.py +0 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/detquantlib/converters/energy.py +0 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/detquantlib/converters/helpers.py +0 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/detquantlib/converters/price.py +0 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/detquantlib/data/__init__.py +0 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/detquantlib/data/databases/azure_client.py +0 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/detquantlib/data/databases/detdatabase.py +0 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/detquantlib/data/databases/helpers.py +0 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/detquantlib/data/databases/snowflake_client.py +0 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/detquantlib/data/entsoe/entsoe.py +0 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/detquantlib/data/sftp/sftp.py +0 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/detquantlib/data/tennet/tennet_client.py +0 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/detquantlib/dates/__init__.py +0 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/detquantlib/dates/dates.py +0 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/detquantlib/figures/__init__.py +0 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/detquantlib/figures/plotly_figures.py +0 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/detquantlib/forecasting/__init__.py +0 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/detquantlib/forecasting/forecasting.py +0 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/detquantlib/outputs/__init__.py +0 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/detquantlib/outputs/outputs_interface.py +0 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/detquantlib/stats/__init__.py +0 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/detquantlib/stats/data_analysis.py +0 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/detquantlib/tradable_products/__init__.py +0 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/detquantlib/tradable_products/tradable_products.py +0 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/detquantlib/utils/__init__.py +0 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/detquantlib/utils/logging.py +0 -0
- {detquantlib-5.8.0 → detquantlib-5.9.0}/detquantlib/utils/utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: detquantlib
|
|
3
|
-
Version: 5.
|
|
3
|
+
Version: 5.9.0
|
|
4
4
|
Summary: An internal library containing functions and classes that can be used across Quant models.
|
|
5
5
|
License-File: LICENSE.txt
|
|
6
6
|
Author: Dynamic Energy Trading
|
|
@@ -37,6 +37,7 @@ Description-Content-Type: text/markdown
|
|
|
37
37
|
- [DET Quant Library](#det-quant-library)
|
|
38
38
|
- [Table of Contents](#table-of-contents)
|
|
39
39
|
- [Overview](#overview)
|
|
40
|
+
- [Snapshot Testing](#snapshot-testing)
|
|
40
41
|
- [Exposed Symbols](#exposed-symbols)
|
|
41
42
|
- [List of Exposed Symbols](#list-of-exposed-symbols)
|
|
42
43
|
- [Configuration](#configuration)
|
|
@@ -62,6 +63,17 @@ Description-Content-Type: text/markdown
|
|
|
62
63
|
The DET Quant Library is an internal library containing functions and classes that can be used
|
|
63
64
|
across Quant models.
|
|
64
65
|
|
|
66
|
+
## Snapshot Testing
|
|
67
|
+
|
|
68
|
+
The `detquantlib.snapshot_testing` package provides snapshot (regression) tests for Quant models:
|
|
69
|
+
each test runs a model end to end on a fixed set of inputs, and compares its outputs with a
|
|
70
|
+
reviewed reference copy. Database calls are recorded once, locally, and replayed from disk in CI,
|
|
71
|
+
which has no database access. The `SnowflakeClient` and `AzureClient` classes are supported out of
|
|
72
|
+
the box.
|
|
73
|
+
|
|
74
|
+
See [the snapshot testing guide](detquantlib/snapshot_testing/README.md) for how the framework
|
|
75
|
+
works and how to set it up in a model repository.
|
|
76
|
+
|
|
65
77
|
## Exposed Symbols
|
|
66
78
|
|
|
67
79
|
Some of the package's symbols (i.e. functions, classes, modules, etc.) are exposed via
|
|
@@ -93,12 +105,18 @@ Classes:
|
|
|
93
105
|
- `BatteryState`:
|
|
94
106
|
- `from detquantlib.assets import BatteryState`
|
|
95
107
|
- `from detquantlib.assets.battery import BatteryState`
|
|
108
|
+
- `DbCacheMissError`:
|
|
109
|
+
- `from detquantlib.snapshot_testing import DbCacheMissError`
|
|
110
|
+
- `from detquantlib.snapshot_testing.errors import DbCacheMissError`
|
|
96
111
|
- `DetDatabase`:
|
|
97
112
|
- `from detquantlib.data import DetDatabase`
|
|
98
113
|
- `from detquantlib.data.databases.detdatabase import DetDatabase`
|
|
99
114
|
- `Entsoe`:
|
|
100
115
|
- `from detquantlib.data import Entsoe`
|
|
101
116
|
- `from detquantlib.data.entsoe.entsoe import Entsoe`
|
|
117
|
+
- `NetworkAccessBlockedError`:
|
|
118
|
+
- `from detquantlib.snapshot_testing import NetworkAccessBlockedError`
|
|
119
|
+
- `from detquantlib.snapshot_testing.errors import NetworkAccessBlockedError`
|
|
102
120
|
- `OutputItem`:
|
|
103
121
|
- `from detquantlib.outputs import OutputItem`
|
|
104
122
|
- `from detquantlib.outputs.outputs_interface import OutputItem`
|
|
@@ -111,6 +129,12 @@ Classes:
|
|
|
111
129
|
- `Sftp`:
|
|
112
130
|
- `from detquantlib.data import Sftp`
|
|
113
131
|
- `from detquantlib.data.sftp.sftp import Sftp`
|
|
132
|
+
- `SnapshotCase`:
|
|
133
|
+
- `from detquantlib.snapshot_testing import SnapshotCase`
|
|
134
|
+
- `from detquantlib.snapshot_testing.cases import SnapshotCase`
|
|
135
|
+
- `SnapshotConfig`:
|
|
136
|
+
- `from detquantlib.snapshot_testing import SnapshotConfig`
|
|
137
|
+
- `from detquantlib.snapshot_testing.config import SnapshotConfig`
|
|
114
138
|
- `SnowflakeClient`:
|
|
115
139
|
- `from detquantlib.data import SnowflakeClient`
|
|
116
140
|
- `from detquantlib.data.databases.snowflake_client import SnowflakeClient`
|
|
@@ -132,12 +156,21 @@ Classes:
|
|
|
132
156
|
|
|
133
157
|
Functions:
|
|
134
158
|
|
|
159
|
+
- `create_case`:
|
|
160
|
+
- `from detquantlib.snapshot_testing import create_case`
|
|
161
|
+
- `from detquantlib.snapshot_testing.cases import create_case`
|
|
162
|
+
- `discover_cases`:
|
|
163
|
+
- `from detquantlib.snapshot_testing import discover_cases`
|
|
164
|
+
- `from detquantlib.snapshot_testing.cases import discover_cases`
|
|
135
165
|
- `list_to_sql_columns`:
|
|
136
166
|
- `from detquantlib.data import list_to_sql_columns`
|
|
137
167
|
- `from detquantlib.data.databases.helpers import list_to_sql_columns`
|
|
138
168
|
- `list_to_sql_set`:
|
|
139
169
|
- `from detquantlib.data import list_to_sql_set`
|
|
140
170
|
- `from detquantlib.data.databases.helpers import list_to_sql_set`
|
|
171
|
+
- `print_cases`:
|
|
172
|
+
- `from detquantlib.snapshot_testing import print_cases`
|
|
173
|
+
- `from detquantlib.snapshot_testing.cases import print_cases`
|
|
141
174
|
|
|
142
175
|
<!-- END EXPOSED SYMBOLS AUTO-GENERATED -->
|
|
143
176
|
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
- [DET Quant Library](#det-quant-library)
|
|
8
8
|
- [Table of Contents](#table-of-contents)
|
|
9
9
|
- [Overview](#overview)
|
|
10
|
+
- [Snapshot Testing](#snapshot-testing)
|
|
10
11
|
- [Exposed Symbols](#exposed-symbols)
|
|
11
12
|
- [List of Exposed Symbols](#list-of-exposed-symbols)
|
|
12
13
|
- [Configuration](#configuration)
|
|
@@ -32,6 +33,17 @@
|
|
|
32
33
|
The DET Quant Library is an internal library containing functions and classes that can be used
|
|
33
34
|
across Quant models.
|
|
34
35
|
|
|
36
|
+
## Snapshot Testing
|
|
37
|
+
|
|
38
|
+
The `detquantlib.snapshot_testing` package provides snapshot (regression) tests for Quant models:
|
|
39
|
+
each test runs a model end to end on a fixed set of inputs, and compares its outputs with a
|
|
40
|
+
reviewed reference copy. Database calls are recorded once, locally, and replayed from disk in CI,
|
|
41
|
+
which has no database access. The `SnowflakeClient` and `AzureClient` classes are supported out of
|
|
42
|
+
the box.
|
|
43
|
+
|
|
44
|
+
See [the snapshot testing guide](detquantlib/snapshot_testing/README.md) for how the framework
|
|
45
|
+
works and how to set it up in a model repository.
|
|
46
|
+
|
|
35
47
|
## Exposed Symbols
|
|
36
48
|
|
|
37
49
|
Some of the package's symbols (i.e. functions, classes, modules, etc.) are exposed via
|
|
@@ -63,12 +75,18 @@ Classes:
|
|
|
63
75
|
- `BatteryState`:
|
|
64
76
|
- `from detquantlib.assets import BatteryState`
|
|
65
77
|
- `from detquantlib.assets.battery import BatteryState`
|
|
78
|
+
- `DbCacheMissError`:
|
|
79
|
+
- `from detquantlib.snapshot_testing import DbCacheMissError`
|
|
80
|
+
- `from detquantlib.snapshot_testing.errors import DbCacheMissError`
|
|
66
81
|
- `DetDatabase`:
|
|
67
82
|
- `from detquantlib.data import DetDatabase`
|
|
68
83
|
- `from detquantlib.data.databases.detdatabase import DetDatabase`
|
|
69
84
|
- `Entsoe`:
|
|
70
85
|
- `from detquantlib.data import Entsoe`
|
|
71
86
|
- `from detquantlib.data.entsoe.entsoe import Entsoe`
|
|
87
|
+
- `NetworkAccessBlockedError`:
|
|
88
|
+
- `from detquantlib.snapshot_testing import NetworkAccessBlockedError`
|
|
89
|
+
- `from detquantlib.snapshot_testing.errors import NetworkAccessBlockedError`
|
|
72
90
|
- `OutputItem`:
|
|
73
91
|
- `from detquantlib.outputs import OutputItem`
|
|
74
92
|
- `from detquantlib.outputs.outputs_interface import OutputItem`
|
|
@@ -81,6 +99,12 @@ Classes:
|
|
|
81
99
|
- `Sftp`:
|
|
82
100
|
- `from detquantlib.data import Sftp`
|
|
83
101
|
- `from detquantlib.data.sftp.sftp import Sftp`
|
|
102
|
+
- `SnapshotCase`:
|
|
103
|
+
- `from detquantlib.snapshot_testing import SnapshotCase`
|
|
104
|
+
- `from detquantlib.snapshot_testing.cases import SnapshotCase`
|
|
105
|
+
- `SnapshotConfig`:
|
|
106
|
+
- `from detquantlib.snapshot_testing import SnapshotConfig`
|
|
107
|
+
- `from detquantlib.snapshot_testing.config import SnapshotConfig`
|
|
84
108
|
- `SnowflakeClient`:
|
|
85
109
|
- `from detquantlib.data import SnowflakeClient`
|
|
86
110
|
- `from detquantlib.data.databases.snowflake_client import SnowflakeClient`
|
|
@@ -102,12 +126,21 @@ Classes:
|
|
|
102
126
|
|
|
103
127
|
Functions:
|
|
104
128
|
|
|
129
|
+
- `create_case`:
|
|
130
|
+
- `from detquantlib.snapshot_testing import create_case`
|
|
131
|
+
- `from detquantlib.snapshot_testing.cases import create_case`
|
|
132
|
+
- `discover_cases`:
|
|
133
|
+
- `from detquantlib.snapshot_testing import discover_cases`
|
|
134
|
+
- `from detquantlib.snapshot_testing.cases import discover_cases`
|
|
105
135
|
- `list_to_sql_columns`:
|
|
106
136
|
- `from detquantlib.data import list_to_sql_columns`
|
|
107
137
|
- `from detquantlib.data.databases.helpers import list_to_sql_columns`
|
|
108
138
|
- `list_to_sql_set`:
|
|
109
139
|
- `from detquantlib.data import list_to_sql_set`
|
|
110
140
|
- `from detquantlib.data.databases.helpers import list_to_sql_set`
|
|
141
|
+
- `print_cases`:
|
|
142
|
+
- `from detquantlib.snapshot_testing import print_cases`
|
|
143
|
+
- `from detquantlib.snapshot_testing.cases import print_cases`
|
|
111
144
|
|
|
112
145
|
<!-- END EXPOSED SYMBOLS AUTO-GENERATED -->
|
|
113
146
|
|
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
# Snapshot Testing Framework
|
|
2
|
+
|
|
3
|
+
The `detquantlib.snapshot_testing` package provides snapshot (regression) tests for Quant models.
|
|
4
|
+
Each test runs a model end to end, through its real entry point `run.run(input_dir, output_dir)`,
|
|
5
|
+
on a fixed set of inputs. It then compares every file of the produced outputs folder with a
|
|
6
|
+
reviewed reference copy (the _snapshot_) committed in the model repository.
|
|
7
|
+
|
|
8
|
+
Snapshot tests show whether outputs _changed_, not whether they are _correct_. Any change must
|
|
9
|
+
either be fixed or explicitly accepted, and is reviewed in the pull request diff.
|
|
10
|
+
|
|
11
|
+
Key properties:
|
|
12
|
+
|
|
13
|
+
- **No database access needed in CI.** Database calls are recorded once, locally, with database
|
|
14
|
+
access, and replayed from disk in all other runs.
|
|
15
|
+
- **Diffable recordings.** Recorded tables are stored as CSV + JSON files, so changes to recorded
|
|
16
|
+
data or queries show up as line diffs in pull requests.
|
|
17
|
+
- **Explicit failures.** A changed query, an unrecorded database call or an attempt to reach the
|
|
18
|
+
network fails immediately with a clear error, instead of timing out or returning other data.
|
|
19
|
+
- **Reproducible runs.** The time can be frozen per test case, so models that use the current date
|
|
20
|
+
issue the same queries every day.
|
|
21
|
+
- **Isolated.** Every patch is undone after each test: unit tests in the same pytest process see
|
|
22
|
+
no leftovers.
|
|
23
|
+
|
|
24
|
+
## Table of Contents
|
|
25
|
+
|
|
26
|
+
- [How It Works](#how-it-works)
|
|
27
|
+
- [Supported Database Clients](#supported-database-clients)
|
|
28
|
+
- [Setting Up a Model Repository](#setting-up-a-model-repository)
|
|
29
|
+
- [Test Cases](#test-cases)
|
|
30
|
+
- [Day-to-Day Usage](#day-to-day-usage)
|
|
31
|
+
- [The DB Cache](#the-db-cache)
|
|
32
|
+
- [Configuration Reference](#configuration-reference)
|
|
33
|
+
- [Troubleshooting](#troubleshooting)
|
|
34
|
+
- [Limitations and Pitfalls](#limitations-and-pitfalls)
|
|
35
|
+
- [Package Structure](#package-structure)
|
|
36
|
+
|
|
37
|
+
## How It Works
|
|
38
|
+
|
|
39
|
+
A snapshot test runs one test case in one of three modes:
|
|
40
|
+
|
|
41
|
+
| Mode | Command | Databases | Effect |
|
|
42
|
+
|---------|-------------------------------------------|--------------------|--------------------------------------------------------------|
|
|
43
|
+
| replay | `pytest tests/snapshot` | replayed from disk | Compares the outputs with the snapshot (what CI runs) |
|
|
44
|
+
| update | `pytest tests/snapshot --snapshot-update` | replayed from disk | Overwrites the snapshot with the outputs |
|
|
45
|
+
| record | `pytest tests/snapshot --snapshot-record` | live (local only) | Re-records the database calls, and overwrites the snapshot |
|
|
46
|
+
|
|
47
|
+
A run goes through the following steps (see `run_snapshot_case()` in `pytest_plugin.py`):
|
|
48
|
+
|
|
49
|
+
1. The outputs folder `tests/snapshot/.actual/CaseXX/Outputs` is emptied. It is kept after the run
|
|
50
|
+
for inspection, and uploaded as an artifact by CI when the test fails.
|
|
51
|
+
2. The database functions are patched:
|
|
52
|
+
- record mode: they run normally, and their results are saved to the case's `db_cache` folder;
|
|
53
|
+
- replay and update modes: they never run; their recorded results are returned instead.
|
|
54
|
+
3. In replay and update modes, the network is blocked and the local `.env` file is ignored, so
|
|
55
|
+
that local runs behave exactly like CI (which has no credentials).
|
|
56
|
+
4. The model runs through its real entry point, with the time frozen if the test case says so,
|
|
57
|
+
and with its logging isolated from the rest of the pytest process.
|
|
58
|
+
5. The outputs are compared with the snapshot (replay mode), or replace it (update and record
|
|
59
|
+
modes).
|
|
60
|
+
|
|
61
|
+
### Record and Replay of Database Calls
|
|
62
|
+
|
|
63
|
+
The framework intercepts database calls at the level of functions, listed as _targets_ in the
|
|
64
|
+
configuration (e.g. `SnowflakeClient.query`). Every call gets a cache key: a hash of the function
|
|
65
|
+
name and of its meaningful arguments.
|
|
66
|
+
|
|
67
|
+
- SQL text is whitespace-normalized: reformatting a query does not invalidate the recording.
|
|
68
|
+
SQLAlchemy `text()` objects are keyed on their SQL.
|
|
69
|
+
- Parameters, dates and other plain values are part of the key.
|
|
70
|
+
- DataFrames (e.g. the data passed to `write_table()`) are keyed on their shape, column names,
|
|
71
|
+
data types and content.
|
|
72
|
+
- Clients, connections and engines are replaced by a constant placeholder, so that a real client
|
|
73
|
+
(record mode) and a client without credentials (replay mode) produce the same key.
|
|
74
|
+
|
|
75
|
+
In replay mode, a call whose key was not recorded raises a `DbCacheMissError`, showing the call
|
|
76
|
+
and the command to re-record. It never silently returns other data.
|
|
77
|
+
|
|
78
|
+
Functions that only create connections (e.g. `SnowflakeClient.connect`, called when entering a
|
|
79
|
+
`with SnowflakeClient() as client:` block) are _connection targets_: in replay mode they return an
|
|
80
|
+
offline stub instead of connecting. The stub supports `with` and `close()`. Any other use of it
|
|
81
|
+
means that a database call is not recorded, and raises a `NetworkAccessBlockedError`.
|
|
82
|
+
|
|
83
|
+
Every module-level alias of a patched function is patched too (e.g. created by
|
|
84
|
+
`from pkg.db import query`), so a model cannot bypass the recording by importing a function by
|
|
85
|
+
name.
|
|
86
|
+
|
|
87
|
+
### Network Guard
|
|
88
|
+
|
|
89
|
+
In replay and update modes, any connection to a non-local address fails immediately with a
|
|
90
|
+
`NetworkAccessBlockedError`, including the connect functions of drivers that open sockets in C code
|
|
91
|
+
(`pyodbc`, `pymssql`, `turbodbc`). A forgotten data source therefore fails explicitly, instead of
|
|
92
|
+
timing out in CI.
|
|
93
|
+
|
|
94
|
+
### Frozen Time
|
|
95
|
+
|
|
96
|
+
A test case can freeze the time (`frozen_time` in `case.toml`), with
|
|
97
|
+
[time-machine](https://pypi.org/project/time-machine/): `datetime.now()`, `date.today()`,
|
|
98
|
+
`time.time()` and `pd.Timestamp.now()` then return that moment during the whole run. This is
|
|
99
|
+
required whenever the model derives dates, and therefore queries, from "today".
|
|
100
|
+
|
|
101
|
+
In record mode, the live database calls run on the real clock, even when the case freezes the
|
|
102
|
+
time: database drivers use the clock, e.g. to sign authentication tokens. Only the model sees the
|
|
103
|
+
frozen time.
|
|
104
|
+
|
|
105
|
+
`datetime.now()` returns the frozen moment in the _local_ time zone. The local time zone of CI
|
|
106
|
+
runners must therefore be the one of the developer machines that recorded the test cases (see
|
|
107
|
+
[CI Workflow](#ci-workflow)).
|
|
108
|
+
|
|
109
|
+
### Isolation
|
|
110
|
+
|
|
111
|
+
`run.py` calls `logging.shutdown()`, which normally closes every logging handler in the process,
|
|
112
|
+
including those of pytest and of unit tests. During a snapshot run, it only closes the handlers
|
|
113
|
+
created by that run, which are also detached afterwards. The exception hooks replaced by
|
|
114
|
+
`detquantlib.utils.setup_logger` are restored as well. The working directory and the
|
|
115
|
+
`INPUT_DIR` / `OUTPUT_DIR` environment variables are restored after each test.
|
|
116
|
+
|
|
117
|
+
### Output Comparison
|
|
118
|
+
|
|
119
|
+
Each file of the outputs folder is compared with its snapshot according to its type:
|
|
120
|
+
|
|
121
|
+
| File type | Comparison |
|
|
122
|
+
|-----------------------------|---------------------------------------------------------------------|
|
|
123
|
+
| `.csv`, `.parquet`, `.xlsx` | As DataFrames, with numerical tolerances (`rtol`, `atol`) |
|
|
124
|
+
| `.json` | Recursively, with numerical tolerances |
|
|
125
|
+
| Text (`.txt`, `.sql`, ...) | Line by line (line endings ignored), with a unified diff |
|
|
126
|
+
| Anything else | Byte by byte |
|
|
127
|
+
|
|
128
|
+
Missing and unexpected files are reported too. Run logs (`*.log`, timestamps) and HTML files
|
|
129
|
+
(`*.html`, e.g. Plotly figures with random element identifiers and ~5 MB of embedded plotly.js) are
|
|
130
|
+
not compared by default.
|
|
131
|
+
|
|
132
|
+
## Supported Database Clients
|
|
133
|
+
|
|
134
|
+
The default configuration covers the database clients of `detquantlib.data`:
|
|
135
|
+
|
|
136
|
+
| Method | `SnowflakeClient` | `AzureClient` |
|
|
137
|
+
|---------------------------------|---------------------------------|---------------------------------|
|
|
138
|
+
| `query()` | recorded | recorded |
|
|
139
|
+
| `load_table()` | through `query()` | through `query()` |
|
|
140
|
+
| `list_tables()` | recorded | through `query()` |
|
|
141
|
+
| `query_batches()` | recorded (batches) | recorded (batches) |
|
|
142
|
+
| `write_table()` | recorded (row count) | recorded (row count) |
|
|
143
|
+
| `execute()` | recorded (row count) | recorded (row count) |
|
|
144
|
+
| `connect()` | offline stub in replay mode | offline stub in replay mode |
|
|
145
|
+
|
|
146
|
+
Notes:
|
|
147
|
+
|
|
148
|
+
- Methods that only call another recorded method are not recorded themselves, otherwise their
|
|
149
|
+
results would be recorded twice.
|
|
150
|
+
- `query_batches()` returns a generator. In record mode, it is consumed entirely during the call,
|
|
151
|
+
and each batch is stored separately. In replay mode, the batches are returned by a generator.
|
|
152
|
+
- Writes (`write_table()`, `execute()`) are _executed_ against the live databases in record mode,
|
|
153
|
+
as in a normal model run. In replay mode, they return the recorded row count. Since the written
|
|
154
|
+
data is part of the cache key, a change in the written data fails the test with a
|
|
155
|
+
`DbCacheMissError`.
|
|
156
|
+
|
|
157
|
+
## Setting Up a Model Repository
|
|
158
|
+
|
|
159
|
+
The framework expects the standard layout of Quant model repositories: `run.py`, with a function
|
|
160
|
+
`run(input_dir, output_dir)` that calls `load_dotenv()`, at the repository root.
|
|
161
|
+
|
|
162
|
+
### Files
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
<repo>/
|
|
166
|
+
├── run.py, constants.py, src/, pyproject.toml
|
|
167
|
+
├── .github/workflows/python-ci.yml
|
|
168
|
+
└── tests/
|
|
169
|
+
├── unit/
|
|
170
|
+
└── snapshot/
|
|
171
|
+
├── conftest.py
|
|
172
|
+
├── test_snapshots.py
|
|
173
|
+
└── cases/
|
|
174
|
+
└── Case01/
|
|
175
|
+
├── case.toml
|
|
176
|
+
├── Inputs/
|
|
177
|
+
├── db_cache/ created by --snapshot-record
|
|
178
|
+
└── ExpectedOutputs/ created by --snapshot-record / --snapshot-update
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
pytest requires two files: command line options can only be registered from a `conftest.py` file
|
|
182
|
+
(or a plugin loaded at startup), and tests are only collected from `test_*.py` files.
|
|
183
|
+
|
|
184
|
+
`tests/snapshot/conftest.py` activates the command line options and the `snapshot_mode` fixture
|
|
185
|
+
for the snapshot tests only:
|
|
186
|
+
|
|
187
|
+
```python
|
|
188
|
+
"""pytest configuration of the snapshot tests (detquantlib.snapshot_testing)."""
|
|
189
|
+
|
|
190
|
+
from detquantlib.snapshot_testing.pytest_plugin import (
|
|
191
|
+
pytest_addoption,
|
|
192
|
+
pytest_configure,
|
|
193
|
+
snapshot_mode,
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
__all__ = ["pytest_addoption", "pytest_configure", "snapshot_mode"]
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
`tests/snapshot/test_snapshots.py` holds the model-specific settings (`CONFIG`, see
|
|
200
|
+
[Configuration Reference](#configuration-reference)) and defines one test per test case:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
"""Snapshot tests: run the model on every test case and compare the outputs with the snapshot."""
|
|
204
|
+
|
|
205
|
+
from pathlib import Path
|
|
206
|
+
|
|
207
|
+
import pytest
|
|
208
|
+
from detquantlib.snapshot_testing import SnapshotConfig, discover_cases
|
|
209
|
+
from detquantlib.snapshot_testing.pytest_plugin import run_snapshot_case
|
|
210
|
+
|
|
211
|
+
import run as model_entry_point # run.py at the project root
|
|
212
|
+
|
|
213
|
+
CONFIG = SnapshotConfig(snapshot_dir=Path(__file__).resolve().parent)
|
|
214
|
+
CASES = discover_cases(CONFIG.cases_dir)
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
@pytest.mark.snapshot
|
|
218
|
+
@pytest.mark.parametrize("case", CASES, ids=[c.name for c in CASES])
|
|
219
|
+
def test_snapshot(case, snapshot_mode, monkeypatch):
|
|
220
|
+
run_snapshot_case(case, snapshot_mode, monkeypatch, CONFIG, entry_point=model_entry_point)
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
Code outside the tests that needs the test cases (e.g. Invoke tasks listing or creating them)
|
|
224
|
+
should build the folder from the snapshot tests folder, e.g.
|
|
225
|
+
`SnapshotConfig(snapshot_dir=...).cases_dir`, rather than import `test_snapshots.py`. Importing
|
|
226
|
+
it would also import the model, and imports through the `tests` folder (e.g.
|
|
227
|
+
`from tests.snapshot... import`) break when another package named `tests` is on the Python path
|
|
228
|
+
(e.g. the one of an editable install of detquantlib).
|
|
229
|
+
|
|
230
|
+
### Dependencies and pytest Settings
|
|
231
|
+
|
|
232
|
+
Add the test dependencies to the development group of the repository:
|
|
233
|
+
|
|
234
|
+
```cmd
|
|
235
|
+
poetry add --group dev pytest time-machine
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
In `pyproject.toml`, list the test folders explicitly, so that pytest loads
|
|
239
|
+
`tests/snapshot/conftest.py` (and its command line options) at startup, and add the project root
|
|
240
|
+
to the Python path:
|
|
241
|
+
|
|
242
|
+
```toml
|
|
243
|
+
[tool.pytest.ini_options]
|
|
244
|
+
testpaths = ["tests/unit", "tests/snapshot"]
|
|
245
|
+
pythonpath = ["."]
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
In `.gitignore`, ignore the produced outputs and the staging folder of recordings:
|
|
249
|
+
|
|
250
|
+
```gitignore
|
|
251
|
+
tests/snapshot/.actual/
|
|
252
|
+
tests/snapshot/cases/*/db_cache.staging/
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### CI Workflow
|
|
256
|
+
|
|
257
|
+
The CI workflow runs the snapshot tests without database secrets, in the time zone of the
|
|
258
|
+
developer machines, and uploads the produced outputs when the tests fail:
|
|
259
|
+
|
|
260
|
+
```yaml
|
|
261
|
+
jobs:
|
|
262
|
+
python_ci:
|
|
263
|
+
runs-on: ubuntu-latest
|
|
264
|
+
env:
|
|
265
|
+
TZ: Europe/Amsterdam # same local time as the machines that record the test cases
|
|
266
|
+
steps:
|
|
267
|
+
# ... checkout, Python, Poetry, dependencies ...
|
|
268
|
+
- name: Run snapshot tests
|
|
269
|
+
id: snapshot_tests
|
|
270
|
+
run: poetry run pytest tests/snapshot -v
|
|
271
|
+
|
|
272
|
+
- name: Upload outputs of snapshot tests on failure
|
|
273
|
+
if: ${{ !cancelled() && steps.snapshot_tests.outcome == 'failure' }}
|
|
274
|
+
uses: actions/upload-artifact@v4
|
|
275
|
+
with:
|
|
276
|
+
name: snapshot-actual-outputs
|
|
277
|
+
path: tests/snapshot/.actual/
|
|
278
|
+
include-hidden-files: true
|
|
279
|
+
if-no-files-found: ignore
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
`--snapshot-record` is refused when the `CI` environment variable is set.
|
|
283
|
+
|
|
284
|
+
## Test Cases
|
|
285
|
+
|
|
286
|
+
### Folder Layout
|
|
287
|
+
|
|
288
|
+
Test cases are folders of `tests/snapshot/cases`, named `Case01`, `Case02`, ... (two or more
|
|
289
|
+
digits), and sorted by number. Each contains:
|
|
290
|
+
|
|
291
|
+
- `case.toml`: the description and settings of the test case;
|
|
292
|
+
- `Inputs/`: the model inputs (settings JSON file, CSV files, ...);
|
|
293
|
+
- `db_cache/`: the recorded database calls;
|
|
294
|
+
- `ExpectedOutputs/`: the snapshot.
|
|
295
|
+
|
|
296
|
+
Folders starting with `.` or `_` are skipped. Any other invalid folder (wrong name, duplicate
|
|
297
|
+
number, missing `case.toml`, unknown key in `case.toml`) raises an error when the tests are
|
|
298
|
+
collected, so that a typo cannot silently drop a test case.
|
|
299
|
+
|
|
300
|
+
### case.toml
|
|
301
|
+
|
|
302
|
+
```toml
|
|
303
|
+
# Multi-line text uses triple quotes. Indentation and the line breaks right after the opening
|
|
304
|
+
# and before the closing quotes are removed automatically.
|
|
305
|
+
description = """
|
|
306
|
+
Base scenario: default settings, front-month horizon.
|
|
307
|
+
Second line of the description.
|
|
308
|
+
"""
|
|
309
|
+
|
|
310
|
+
# Freezes the time during the run. Give a UTC offset: a time without offset is read as UTC.
|
|
311
|
+
frozen_time = 2026-06-30T08:00:00+02:00
|
|
312
|
+
|
|
313
|
+
# Optional overrides of the defaults of CONFIG in test_snapshots.py
|
|
314
|
+
rtol = 1e-6
|
|
315
|
+
atol = 1e-9
|
|
316
|
+
ignore = ["plots/*.png"]
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
| Key | Default | Meaning |
|
|
320
|
+
|---------------|------------------|--------------------------------------------------------------------|
|
|
321
|
+
| `description` | `""` | Description, listed by `print_cases()` |
|
|
322
|
+
| `frozen_time` | none (real time) | Moment at which the time is frozen during the run |
|
|
323
|
+
| `rtol` | `config.rtol` | Relative tolerance of numerical comparisons |
|
|
324
|
+
| `atol` | `config.atol` | Absolute tolerance of numerical comparisons |
|
|
325
|
+
| `ignore` | `[]` | Additional output files not compared (glob patterns) |
|
|
326
|
+
|
|
327
|
+
### Creating a Test Case
|
|
328
|
+
|
|
329
|
+
`create_case(inputs_dir, cases_root)` creates the folder of a new test case, with the lowest free
|
|
330
|
+
case number. It copies the given inputs folder (typically the project's default `Inputs` folder)
|
|
331
|
+
and writes a `case.toml` file to complete, which freezes the time at the moment of creation. Then
|
|
332
|
+
record the new test case:
|
|
333
|
+
|
|
334
|
+
```cmd
|
|
335
|
+
pytest tests/snapshot --snapshot-record -k Case02
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Test cases can also be created manually: create the folder with a `case.toml` file and an `Inputs`
|
|
339
|
+
folder, then record it. Freeze the time _before_ recording if the model uses the current date.
|
|
340
|
+
|
|
341
|
+
`print_cases(cases_root)` lists the test cases and their descriptions:
|
|
342
|
+
|
|
343
|
+
```
|
|
344
|
+
- Case01:
|
|
345
|
+
Base scenario: default settings, front-month horizon.
|
|
346
|
+
Second line of the description.
|
|
347
|
+
- Case02:
|
|
348
|
+
This is another description
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
## Day-to-Day Usage
|
|
352
|
+
|
|
353
|
+
```cmd
|
|
354
|
+
pytest tests/snapshot # replay: offline, what CI runs
|
|
355
|
+
pytest tests/snapshot -k Case01 # a single test case
|
|
356
|
+
pytest tests/snapshot --snapshot-update # accept the current outputs (offline)
|
|
357
|
+
pytest tests/snapshot --snapshot-record # LOCAL ONLY: re-record from the databases
|
|
358
|
+
pytest tests/snapshot --snapshot-record -k Case01 # re-record a single test case
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
- **The model outputs changed on purpose** (e.g. a new feature): run `--snapshot-update`, and
|
|
362
|
+
review the changes to `ExpectedOutputs` in the pull request.
|
|
363
|
+
- **The database queries changed**, or the recorded data should be refreshed: run
|
|
364
|
+
`--snapshot-record` locally, with database access. Review the changes to `db_cache` (queries,
|
|
365
|
+
data) and `ExpectedOutputs`.
|
|
366
|
+
- **A test fails in CI**: download the `snapshot-actual-outputs` artifact of the workflow run to
|
|
367
|
+
inspect the produced outputs.
|
|
368
|
+
|
|
369
|
+
## The DB Cache
|
|
370
|
+
|
|
371
|
+
```
|
|
372
|
+
db_cache/
|
|
373
|
+
├── manifest.json cache key -> function, call arguments, file
|
|
374
|
+
├── 001_SnowflakeClient_query.csv values of the 1st distinct table returned
|
|
375
|
+
├── 001_SnowflakeClient_query.dtypes.json its column data types
|
|
376
|
+
├── 002_AzureClient_execute.pkl a result that is not a table (e.g. a row count)
|
|
377
|
+
├── 003_AzureClient_query_batches_001.csv the batches returned by query_batches()
|
|
378
|
+
└── ...
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
- Files are numbered in call order, so re-recording a changed query overwrites the same file
|
|
382
|
+
names, and the change shows up as a line diff in the pull request.
|
|
383
|
+
- Identical repeated calls are stored once.
|
|
384
|
+
- `manifest.json` shows each recorded call with its normalized arguments, e.g. the SQL text.
|
|
385
|
+
- Recordings are written to `db_cache.staging` and replace `db_cache` only once the whole run has
|
|
386
|
+
succeeded.
|
|
387
|
+
|
|
388
|
+
### Table Format
|
|
389
|
+
|
|
390
|
+
Tables are stored as `<name>.csv` (the values) and `<name>.dtypes.json` (the column names and
|
|
391
|
+
data types). The round trip is exact, and verified when recording:
|
|
392
|
+
|
|
393
|
+
- integers, floats (written with `repr`, without rounding), booleans and nullable types;
|
|
394
|
+
- naive and time-zone-aware datetimes: values are written with their UTC offset, e.g.
|
|
395
|
+
`2026-03-29T03:00:00+02:00`, and converted back to their original time zone (IANA name and
|
|
396
|
+
`zoneinfo` / `pytz` library), so that daylight saving time switches are handled correctly;
|
|
397
|
+
- timedeltas, string dtypes, and object columns holding one kind of value (`str`, `int`, `float`,
|
|
398
|
+
`bool`, `Decimal`, `date`, `time`, `datetime`);
|
|
399
|
+
- NULL values (`None`, `NaN`, `NaT`, `NA` are restored as they were);
|
|
400
|
+
- named, datetime and multi-level indexes.
|
|
401
|
+
|
|
402
|
+
Tables that cannot be stored exactly (categorical columns, columns mixing value types, columns
|
|
403
|
+
with both empty strings and NULLs) are stored as pickle files instead, with a warning: replay
|
|
404
|
+
works, but the recording is not diffable. Results that are not tables (e.g. row counts) are
|
|
405
|
+
always stored as pickle files.
|
|
406
|
+
|
|
407
|
+
Do not store recordings with Git LFS: GitHub does not show diffs of LFS files. Keep recordings
|
|
408
|
+
small instead, by querying only the data that a test case needs.
|
|
409
|
+
|
|
410
|
+
## Configuration Reference
|
|
411
|
+
|
|
412
|
+
`SnapshotConfig` holds the settings of the snapshot tests of a model repository:
|
|
413
|
+
|
|
414
|
+
| Field | Default | Meaning |
|
|
415
|
+
|-------------------------|---------------------------------|-----------------------------------------------|
|
|
416
|
+
| `snapshot_dir` | (required) | Folder of the snapshot tests |
|
|
417
|
+
| `project_root` | grandparent of `snapshot_dir` | Folder in which the model runs |
|
|
418
|
+
| `db_query_targets` | `DEFAULT_DB_QUERY_TARGETS` | Functions recorded and replayed |
|
|
419
|
+
| `db_connection_targets` | `DEFAULT_DB_CONNECTION_TARGETS` | Connection factories (stubbed in replay mode) |
|
|
420
|
+
| `ignore_patterns` | `DEFAULT_IGNORE_PATTERNS` | Output files not compared (glob patterns) |
|
|
421
|
+
| `rtol` | `1e-8` | Relative tolerance of numerical comparisons |
|
|
422
|
+
| `atol` | `1e-10` | Absolute tolerance of numerical comparisons |
|
|
423
|
+
|
|
424
|
+
`cases_dir` (`snapshot_dir / "cases"`) and `actual_dir` (`snapshot_dir / ".actual"`) are derived
|
|
425
|
+
from `snapshot_dir`.
|
|
426
|
+
|
|
427
|
+
### Adding Database Functions
|
|
428
|
+
|
|
429
|
+
If a model reaches a database without the clients of `detquantlib.data` (e.g. with its own helper
|
|
430
|
+
functions), add those functions to the targets of `CONFIG`, in `tests/snapshot/test_snapshots.py`:
|
|
431
|
+
|
|
432
|
+
```python
|
|
433
|
+
from detquantlib.snapshot_testing import (
|
|
434
|
+
DEFAULT_DB_CONNECTION_TARGETS,
|
|
435
|
+
DEFAULT_DB_QUERY_TARGETS,
|
|
436
|
+
SnapshotConfig,
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
CONFIG = SnapshotConfig(
|
|
440
|
+
snapshot_dir=Path(__file__).resolve().parent,
|
|
441
|
+
db_query_targets=[*DEFAULT_DB_QUERY_TARGETS, "src.data_access:load_prices"],
|
|
442
|
+
db_connection_targets=[*DEFAULT_DB_CONNECTION_TARGETS, "src.data_access:connect"],
|
|
443
|
+
)
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
- Targets use the format `"package.module:function"` or `"package.module:Class.method"`.
|
|
447
|
+
- Prefer the highest-level function that both connects and returns data.
|
|
448
|
+
- Do not list a function that only calls another listed function: its results would be recorded
|
|
449
|
+
twice, and the inner recordings would be reported as unused in replay mode.
|
|
450
|
+
- List functions that only create connections as connection targets.
|
|
451
|
+
|
|
452
|
+
## Troubleshooting
|
|
453
|
+
|
|
454
|
+
| Error or warning | Cause and solution |
|
|
455
|
+
|-----------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------|
|
|
456
|
+
| `DbCacheMissError: No DB cache found` | The test case was never recorded. Record it locally with `--snapshot-record`. |
|
|
457
|
+
| `DbCacheMissError: No recorded result for ...` | A query, its parameters or the written data changed, or a new database call was added. Re-record the test case. If the call depends on the current date, freeze the time in `case.toml`. |
|
|
458
|
+
| `NetworkAccessBlockedError` | The model reaches the network or a database through a function that is not recorded. Add the function to the targets. |
|
|
459
|
+
| `Snapshot mismatch for case ...` | The outputs changed. Fix the model, or accept the change with `--snapshot-update`. |
|
|
460
|
+
| `No snapshot for case ...` | The test case has a DB cache but no snapshot. Create it with `--snapshot-update`. |
|
|
461
|
+
| `... recorded DB result(s) were not requested` | The model no longer makes some recorded calls: the recording may be stale. Re-record the test case. |
|
|
462
|
+
| `... cannot be stored as CSV + JSON ... pickle` | A recorded table has an unsupported data type (see [Table Format](#table-format)). Replay works, but the recording is not diffable. |
|
|
463
|
+
| `--snapshot-record needs database access ...` | Recording is refused in CI (`CI` environment variable set). |
|
|
464
|
+
|
|
465
|
+
## Limitations and Pitfalls
|
|
466
|
+
|
|
467
|
+
- **Time zone.** With a frozen time, `datetime.now()` depends on the local time zone. CI must use
|
|
468
|
+
the time zone of the machines that recorded the test cases (`TZ: Europe/Amsterdam`).
|
|
469
|
+
- **Default database of a client.** The cache key does not include the client's configuration,
|
|
470
|
+
which differs between record mode (credentials) and replay mode (none). Identical SQL sent to
|
|
471
|
+
different default databases in one run shares one recording: qualify table names or pass
|
|
472
|
+
`database=...` in that case.
|
|
473
|
+
- **Writes in record mode.** Recording runs the model against the live databases, including its
|
|
474
|
+
writes. Use credentials of a development environment.
|
|
475
|
+
- **Multiprocessing.** Patches do not reach child processes: database calls made in child
|
|
476
|
+
processes are neither recorded nor blocked.
|
|
477
|
+
- **Module-level caches.** Query results cached at module level (e.g. with `functools.cache`)
|
|
478
|
+
survive between test cases in the same pytest process. Clear them between runs.
|
|
479
|
+
- **Binary outputs.** Binary files (e.g. PNG, PDF) are compared byte by byte and often embed
|
|
480
|
+
timestamps. Add them to the ignore patterns if they cause false failures.
|
|
481
|
+
- **Recorded data is committed.** Make sure that the access rights of the repository suit the
|
|
482
|
+
sensitivity of the recorded data.
|
|
483
|
+
- **Randomness.** The framework does not fix random seeds: models must make their results
|
|
484
|
+
reproducible themselves.
|
|
485
|
+
- **Python 3.10.** `case.toml` files are read with `tomllib` (Python 3.11+) or, on Python 3.10,
|
|
486
|
+
with `tomli` (installed with pytest).
|
|
487
|
+
|
|
488
|
+
## Package Structure
|
|
489
|
+
|
|
490
|
+
| Module | Content |
|
|
491
|
+
|--------------------|------------------------------------------------------------------------------|
|
|
492
|
+
| `config.py` | `SnapshotConfig` and the default targets, ignore patterns and tolerances |
|
|
493
|
+
| `cases.py` | `SnapshotCase`, `discover_cases()`, `print_cases()`, `create_case()` |
|
|
494
|
+
| `recording.py` | Cache keys, `DbRecorder` (record/replay), patching of the targets |
|
|
495
|
+
| `tables.py` | DataFrame <-> CSV + dtypes JSON, with an exact round trip |
|
|
496
|
+
| `isolation.py` | Network guard, frozen time, logging isolation |
|
|
497
|
+
| `comparison.py` | Comparison of the outputs with the snapshot |
|
|
498
|
+
| `pytest_plugin.py` | pytest hooks, `snapshot_mode` fixture, `run_snapshot_case()` (needs pytest) |
|
|
499
|
+
| `errors.py` | `DbCacheMissError`, `NetworkAccessBlockedError` |
|
|
500
|
+
|
|
501
|
+
The package can be imported without pytest: only `pytest_plugin.py` needs it, and is not imported
|
|
502
|
+
by `detquantlib.snapshot_testing`. The tests of the framework are in `tests/snapshot_testing`.
|