fraiseql-confiture 0.3.4__cp311-cp311-win_amd64.whl
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.
- confiture/__init__.py +48 -0
- confiture/_core.cp311-win_amd64.pyd +0 -0
- confiture/cli/__init__.py +0 -0
- confiture/cli/dry_run.py +116 -0
- confiture/cli/lint_formatter.py +193 -0
- confiture/cli/main.py +1656 -0
- confiture/config/__init__.py +0 -0
- confiture/config/environment.py +263 -0
- confiture/core/__init__.py +51 -0
- confiture/core/anonymization/__init__.py +0 -0
- confiture/core/anonymization/audit.py +485 -0
- confiture/core/anonymization/benchmarking.py +372 -0
- confiture/core/anonymization/breach_notification.py +652 -0
- confiture/core/anonymization/compliance.py +617 -0
- confiture/core/anonymization/composer.py +298 -0
- confiture/core/anonymization/data_subject_rights.py +669 -0
- confiture/core/anonymization/factory.py +319 -0
- confiture/core/anonymization/governance.py +737 -0
- confiture/core/anonymization/performance.py +1092 -0
- confiture/core/anonymization/profile.py +284 -0
- confiture/core/anonymization/registry.py +195 -0
- confiture/core/anonymization/security/kms_manager.py +547 -0
- confiture/core/anonymization/security/lineage.py +888 -0
- confiture/core/anonymization/security/token_store.py +686 -0
- confiture/core/anonymization/strategies/__init__.py +41 -0
- confiture/core/anonymization/strategies/address.py +359 -0
- confiture/core/anonymization/strategies/credit_card.py +374 -0
- confiture/core/anonymization/strategies/custom.py +161 -0
- confiture/core/anonymization/strategies/date.py +218 -0
- confiture/core/anonymization/strategies/differential_privacy.py +398 -0
- confiture/core/anonymization/strategies/email.py +141 -0
- confiture/core/anonymization/strategies/format_preserving_encryption.py +310 -0
- confiture/core/anonymization/strategies/hash.py +150 -0
- confiture/core/anonymization/strategies/ip_address.py +235 -0
- confiture/core/anonymization/strategies/masking_retention.py +252 -0
- confiture/core/anonymization/strategies/name.py +298 -0
- confiture/core/anonymization/strategies/phone.py +119 -0
- confiture/core/anonymization/strategies/preserve.py +85 -0
- confiture/core/anonymization/strategies/redact.py +101 -0
- confiture/core/anonymization/strategies/salted_hashing.py +322 -0
- confiture/core/anonymization/strategies/text_redaction.py +183 -0
- confiture/core/anonymization/strategies/tokenization.py +334 -0
- confiture/core/anonymization/strategy.py +241 -0
- confiture/core/anonymization/syncer_audit.py +357 -0
- confiture/core/blue_green.py +683 -0
- confiture/core/builder.py +500 -0
- confiture/core/checksum.py +358 -0
- confiture/core/connection.py +132 -0
- confiture/core/differ.py +522 -0
- confiture/core/drift.py +564 -0
- confiture/core/dry_run.py +182 -0
- confiture/core/health.py +313 -0
- confiture/core/hooks/__init__.py +87 -0
- confiture/core/hooks/base.py +232 -0
- confiture/core/hooks/context.py +146 -0
- confiture/core/hooks/execution_strategies.py +57 -0
- confiture/core/hooks/observability.py +220 -0
- confiture/core/hooks/phases.py +53 -0
- confiture/core/hooks/registry.py +295 -0
- confiture/core/large_tables.py +775 -0
- confiture/core/linting/__init__.py +70 -0
- confiture/core/linting/composer.py +192 -0
- confiture/core/linting/libraries/__init__.py +17 -0
- confiture/core/linting/libraries/gdpr.py +168 -0
- confiture/core/linting/libraries/general.py +184 -0
- confiture/core/linting/libraries/hipaa.py +144 -0
- confiture/core/linting/libraries/pci_dss.py +104 -0
- confiture/core/linting/libraries/sox.py +120 -0
- confiture/core/linting/schema_linter.py +491 -0
- confiture/core/linting/versioning.py +151 -0
- confiture/core/locking.py +389 -0
- confiture/core/migration_generator.py +298 -0
- confiture/core/migrator.py +793 -0
- confiture/core/observability/__init__.py +44 -0
- confiture/core/observability/audit.py +323 -0
- confiture/core/observability/logging.py +187 -0
- confiture/core/observability/metrics.py +174 -0
- confiture/core/observability/tracing.py +192 -0
- confiture/core/pg_version.py +418 -0
- confiture/core/pool.py +406 -0
- confiture/core/risk/__init__.py +39 -0
- confiture/core/risk/predictor.py +188 -0
- confiture/core/risk/scoring.py +248 -0
- confiture/core/rollback_generator.py +388 -0
- confiture/core/schema_analyzer.py +769 -0
- confiture/core/schema_to_schema.py +590 -0
- confiture/core/security/__init__.py +32 -0
- confiture/core/security/logging.py +201 -0
- confiture/core/security/validation.py +416 -0
- confiture/core/signals.py +371 -0
- confiture/core/syncer.py +540 -0
- confiture/exceptions.py +192 -0
- confiture/integrations/__init__.py +0 -0
- confiture/models/__init__.py +0 -0
- confiture/models/lint.py +193 -0
- confiture/models/migration.py +180 -0
- confiture/models/schema.py +203 -0
- confiture/scenarios/__init__.py +36 -0
- confiture/scenarios/compliance.py +586 -0
- confiture/scenarios/ecommerce.py +199 -0
- confiture/scenarios/financial.py +253 -0
- confiture/scenarios/healthcare.py +315 -0
- confiture/scenarios/multi_tenant.py +340 -0
- confiture/scenarios/saas.py +295 -0
- confiture/testing/FRAMEWORK_API.md +722 -0
- confiture/testing/__init__.py +38 -0
- confiture/testing/fixtures/__init__.py +11 -0
- confiture/testing/fixtures/data_validator.py +229 -0
- confiture/testing/fixtures/migration_runner.py +167 -0
- confiture/testing/fixtures/schema_snapshotter.py +352 -0
- confiture/testing/frameworks/__init__.py +10 -0
- confiture/testing/frameworks/mutation.py +587 -0
- confiture/testing/frameworks/performance.py +479 -0
- confiture/testing/utils/__init__.py +0 -0
- fraiseql_confiture-0.3.4.dist-info/METADATA +438 -0
- fraiseql_confiture-0.3.4.dist-info/RECORD +119 -0
- fraiseql_confiture-0.3.4.dist-info/WHEEL +4 -0
- fraiseql_confiture-0.3.4.dist-info/entry_points.txt +2 -0
- fraiseql_confiture-0.3.4.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,722 @@
|
|
|
1
|
+
# Confiture Testing Framework - API Reference
|
|
2
|
+
|
|
3
|
+
Complete API documentation for the Confiture migration testing framework.
|
|
4
|
+
|
|
5
|
+
## Package Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
confiture.testing/
|
|
9
|
+
├── __init__.py
|
|
10
|
+
├── FRAMEWORK_API.md (This file)
|
|
11
|
+
├── frameworks/
|
|
12
|
+
│ ├── __init__.py
|
|
13
|
+
│ ├── mutation.py (Mutation testing framework)
|
|
14
|
+
│ └── performance.py (Performance profiling framework)
|
|
15
|
+
└── fixtures/
|
|
16
|
+
├── __init__.py
|
|
17
|
+
├── migration_runner.py (Migration execution)
|
|
18
|
+
├── schema_snapshotter.py (Schema capture/comparison)
|
|
19
|
+
└── data_validator.py (Data integrity validation)
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Quick Reference
|
|
23
|
+
|
|
24
|
+
### Main Classes
|
|
25
|
+
|
|
26
|
+
| Class | Module | Purpose |
|
|
27
|
+
|-------|--------|---------|
|
|
28
|
+
| `MutationRegistry` | `frameworks.mutation` | Catalog of test-killing mutations |
|
|
29
|
+
| `MutationRunner` | `frameworks.mutation` | Execute mutations and track results |
|
|
30
|
+
| `MutationReport` | `frameworks.mutation` | Analyze mutation testing results |
|
|
31
|
+
| `MigrationPerformanceProfiler` | `frameworks.performance` | Profile operation timing |
|
|
32
|
+
| `PerformanceProfile` | `frameworks.performance` | Single operation metrics |
|
|
33
|
+
| `PerformanceBaseline` | `frameworks.performance` | Track performance over time |
|
|
34
|
+
| `MigrationRunner` | `fixtures.migration_runner` | Execute migrations |
|
|
35
|
+
| `SchemaSnapshotter` | `fixtures.schema_snapshotter` | Capture schema state |
|
|
36
|
+
| `DataValidator` | `fixtures.data_validator` | Validate data integrity |
|
|
37
|
+
|
|
38
|
+
## Mutation Testing Framework
|
|
39
|
+
|
|
40
|
+
### MutationRegistry
|
|
41
|
+
|
|
42
|
+
Provides catalog of mutations for test quality validation.
|
|
43
|
+
|
|
44
|
+
**Import:**
|
|
45
|
+
```python
|
|
46
|
+
from confiture.testing.frameworks.mutation import MutationRegistry
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Constructor:**
|
|
50
|
+
```python
|
|
51
|
+
registry = MutationRegistry()
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Properties:**
|
|
55
|
+
|
|
56
|
+
| Property | Type | Description |
|
|
57
|
+
|----------|------|-------------|
|
|
58
|
+
| `mutations` | `dict[str, list[Mutation]]` | All mutations organized by category |
|
|
59
|
+
| `schema_mutations` | `list[Mutation]` | SCHEMA category mutations |
|
|
60
|
+
| `data_mutations` | `list[Mutation]` | DATA category mutations |
|
|
61
|
+
| `rollback_mutations` | `list[Mutation]` | ROLLBACK category mutations |
|
|
62
|
+
| `performance_mutations` | `list[Mutation]` | PERFORMANCE category mutations |
|
|
63
|
+
|
|
64
|
+
**Methods:**
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
# Get all mutations for a category
|
|
68
|
+
schema_mutations = registry.get_category_mutations("SCHEMA")
|
|
69
|
+
# Returns: list[Mutation]
|
|
70
|
+
|
|
71
|
+
# Get a specific mutation by ID
|
|
72
|
+
mutation = registry.get_mutation_by_id("SCH_001")
|
|
73
|
+
# Returns: Mutation or None
|
|
74
|
+
|
|
75
|
+
# Get all mutations
|
|
76
|
+
all_mutations = registry.get_all_mutations()
|
|
77
|
+
# Returns: list[Mutation]
|
|
78
|
+
|
|
79
|
+
# Check mutation availability
|
|
80
|
+
has_mutation = registry.has_mutation("SCH_001")
|
|
81
|
+
# Returns: bool
|
|
82
|
+
|
|
83
|
+
# Get mutations by severity
|
|
84
|
+
critical = registry.get_mutations_by_severity("CRITICAL")
|
|
85
|
+
# Returns: list[Mutation]
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Mutation Categories:**
|
|
89
|
+
|
|
90
|
+
| Category | Count | Purpose |
|
|
91
|
+
|----------|-------|---------|
|
|
92
|
+
| SCHEMA | 6 | DDL operation changes |
|
|
93
|
+
| DATA | 7 | Data manipulation changes |
|
|
94
|
+
| ROLLBACK | 5 | Rollback operation changes |
|
|
95
|
+
| PERFORMANCE | 6 | Performance threshold changes |
|
|
96
|
+
|
|
97
|
+
**Example Usage:**
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from confiture.testing.frameworks.mutation import MutationRegistry
|
|
101
|
+
|
|
102
|
+
registry = MutationRegistry()
|
|
103
|
+
|
|
104
|
+
# Get schema mutations
|
|
105
|
+
schema_mutations = registry.schema_mutations
|
|
106
|
+
for mutation in schema_mutations:
|
|
107
|
+
print(f"{mutation.id}: {mutation.description}")
|
|
108
|
+
|
|
109
|
+
# Check specific mutation
|
|
110
|
+
if registry.has_mutation("SCH_002"):
|
|
111
|
+
mutation = registry.get_mutation_by_id("SCH_002")
|
|
112
|
+
print(f"Severity: {mutation.severity}")
|
|
113
|
+
print(f"Category: {mutation.category}")
|
|
114
|
+
print(f"Apply function: {mutation.apply_function}")
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
### Mutation
|
|
118
|
+
|
|
119
|
+
Represents a single mutation for test validation.
|
|
120
|
+
|
|
121
|
+
**Properties:**
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
mutation.id # str: Mutation identifier (e.g., "SCH_001")
|
|
125
|
+
mutation.description # str: Human-readable description
|
|
126
|
+
mutation.category # str: Category (SCHEMA, DATA, ROLLBACK, PERFORMANCE)
|
|
127
|
+
mutation.severity # str: CRITICAL, HIGH, MEDIUM, LOW
|
|
128
|
+
mutation.apply_function # callable: Function to apply mutation
|
|
129
|
+
mutation.keywords # list[str]: Keywords for filtering
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
**Example:**
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
mutation = registry.get_mutation_by_id("SCH_001")
|
|
136
|
+
|
|
137
|
+
print(f"ID: {mutation.id}")
|
|
138
|
+
print(f"Description: {mutation.description}")
|
|
139
|
+
print(f"Category: {mutation.category}")
|
|
140
|
+
print(f"Severity: {mutation.severity}")
|
|
141
|
+
|
|
142
|
+
# Apply mutation
|
|
143
|
+
modified_sql = mutation.apply_function(original_sql)
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
### MutationRunner
|
|
147
|
+
|
|
148
|
+
Executes mutations and tracks test results.
|
|
149
|
+
|
|
150
|
+
**Import:**
|
|
151
|
+
```python
|
|
152
|
+
from confiture.testing.frameworks.mutation import MutationRunner
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Constructor:**
|
|
156
|
+
```python
|
|
157
|
+
runner = MutationRunner(registry=None, verbose=True)
|
|
158
|
+
# registry: Optional MutationRegistry (creates new if None)
|
|
159
|
+
# verbose: bool - Enable detailed output
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**Methods:**
|
|
163
|
+
|
|
164
|
+
```python
|
|
165
|
+
# Run mutation suite
|
|
166
|
+
report = runner.run_mutation_suite(registry)
|
|
167
|
+
# Returns: MutationReport
|
|
168
|
+
|
|
169
|
+
# Run single mutation
|
|
170
|
+
result = runner.run_mutation(mutation, test_function)
|
|
171
|
+
# mutation: Mutation
|
|
172
|
+
# test_function: callable(mutation) -> bool
|
|
173
|
+
# Returns: MutationTestResult
|
|
174
|
+
|
|
175
|
+
# Run with custom settings
|
|
176
|
+
report = runner.run_mutation_suite(
|
|
177
|
+
registry,
|
|
178
|
+
timeout_seconds=300,
|
|
179
|
+
max_mutations=None # None = all mutations
|
|
180
|
+
)
|
|
181
|
+
# Returns: MutationReport
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Example:**
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from confiture.testing.frameworks.mutation import MutationRunner
|
|
188
|
+
|
|
189
|
+
runner = MutationRunner()
|
|
190
|
+
|
|
191
|
+
# Define test function
|
|
192
|
+
def test_with_mutation(mutation):
|
|
193
|
+
"""Test that should be killed by mutation."""
|
|
194
|
+
try:
|
|
195
|
+
# Run migration with mutation applied
|
|
196
|
+
result = execute_migration_with_mutation(mutation)
|
|
197
|
+
|
|
198
|
+
# Test should detect the mutation
|
|
199
|
+
assert result.schema_valid, "Mutation not detected!"
|
|
200
|
+
return True # Mutation killed
|
|
201
|
+
except AssertionError:
|
|
202
|
+
return False # Mutation survived
|
|
203
|
+
|
|
204
|
+
# Run test suite
|
|
205
|
+
report = runner.run_mutation_suite(registry)
|
|
206
|
+
print(f"Kill rate: {report.kill_rate}%")
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
### MutationReport
|
|
210
|
+
|
|
211
|
+
Analysis of mutation testing results.
|
|
212
|
+
|
|
213
|
+
**Properties:**
|
|
214
|
+
|
|
215
|
+
```python
|
|
216
|
+
report.total_mutations # int: Total mutations in suite
|
|
217
|
+
report.killed_mutations # int: Mutations that failed tests
|
|
218
|
+
report.survived_mutations # int: Mutations that passed tests
|
|
219
|
+
report.error_mutations # int: Mutations with errors
|
|
220
|
+
report.results # list[MutationResult]: Individual results
|
|
221
|
+
report.timestamp # datetime: Report generation time
|
|
222
|
+
report.duration_seconds # float: Total execution time
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
**Methods:**
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
# Calculate kill rate (test quality metric)
|
|
229
|
+
kill_rate = report.calculate_kill_rate()
|
|
230
|
+
# Returns: float (0-100)
|
|
231
|
+
|
|
232
|
+
# Get statistics
|
|
233
|
+
stats = report.get_statistics()
|
|
234
|
+
# Returns: dict with metrics
|
|
235
|
+
|
|
236
|
+
# Find weak areas (survived mutations)
|
|
237
|
+
weak = report.get_survived_mutations()
|
|
238
|
+
# Returns: list[Mutation]
|
|
239
|
+
|
|
240
|
+
# Get summary
|
|
241
|
+
summary = report.get_summary()
|
|
242
|
+
# Returns: str - Human-readable summary
|
|
243
|
+
|
|
244
|
+
# Export results
|
|
245
|
+
json_data = report.to_json()
|
|
246
|
+
# Returns: str - JSON representation
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
**Example:**
|
|
250
|
+
|
|
251
|
+
```python
|
|
252
|
+
report = runner.run_mutation_suite(registry)
|
|
253
|
+
|
|
254
|
+
# Check kill rate
|
|
255
|
+
kill_rate = report.calculate_kill_rate()
|
|
256
|
+
print(f"Test quality: {kill_rate}%")
|
|
257
|
+
|
|
258
|
+
if kill_rate < 80:
|
|
259
|
+
print("Tests need improvement - low mutation kill rate")
|
|
260
|
+
|
|
261
|
+
# Find weak areas
|
|
262
|
+
survived = report.get_survived_mutations()
|
|
263
|
+
for mutation in survived:
|
|
264
|
+
print(f"Weak test coverage: {mutation.description}")
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
## Performance Framework
|
|
268
|
+
|
|
269
|
+
### MigrationPerformanceProfiler
|
|
270
|
+
|
|
271
|
+
Profiles migration operation execution times.
|
|
272
|
+
|
|
273
|
+
**Import:**
|
|
274
|
+
```python
|
|
275
|
+
from confiture.testing.frameworks.performance import MigrationPerformanceProfiler
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
**Constructor:**
|
|
279
|
+
```python
|
|
280
|
+
profiler = MigrationPerformanceProfiler()
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
**Methods:**
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
# Profile a migration operation
|
|
287
|
+
profile = profiler.profile_operation(
|
|
288
|
+
operation="CREATE TABLE",
|
|
289
|
+
sql="CREATE TABLE users (id UUID PRIMARY KEY)",
|
|
290
|
+
db_connection=conn # Optional
|
|
291
|
+
)
|
|
292
|
+
# Returns: PerformanceProfile
|
|
293
|
+
|
|
294
|
+
# Profile with custom timeout
|
|
295
|
+
profile = profiler.profile_operation(
|
|
296
|
+
operation="Complex Migration",
|
|
297
|
+
sql=complex_sql,
|
|
298
|
+
timeout_seconds=60
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
# Get baseline
|
|
302
|
+
baseline = profiler.get_baseline()
|
|
303
|
+
# Returns: PerformanceBaseline or None
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**Example:**
|
|
307
|
+
|
|
308
|
+
```python
|
|
309
|
+
from confiture.testing.frameworks.performance import MigrationPerformanceProfiler
|
|
310
|
+
import psycopg
|
|
311
|
+
|
|
312
|
+
profiler = MigrationPerformanceProfiler()
|
|
313
|
+
|
|
314
|
+
# Connect to database
|
|
315
|
+
conn = psycopg.connect("postgresql://localhost/confiture_test")
|
|
316
|
+
|
|
317
|
+
# Profile CREATE TABLE operation
|
|
318
|
+
profile = profiler.profile_operation(
|
|
319
|
+
operation="CREATE TABLE users",
|
|
320
|
+
sql="CREATE TABLE users (id UUID PRIMARY KEY, name VARCHAR(255))",
|
|
321
|
+
db_connection=conn
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
print(f"Duration: {profile.duration_seconds}s")
|
|
325
|
+
print(f"Success: {profile.success}")
|
|
326
|
+
|
|
327
|
+
conn.close()
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
### PerformanceProfile
|
|
331
|
+
|
|
332
|
+
Single operation performance metrics.
|
|
333
|
+
|
|
334
|
+
**Properties:**
|
|
335
|
+
|
|
336
|
+
```python
|
|
337
|
+
profile.operation # str: Operation name
|
|
338
|
+
profile.duration_seconds # float: Execution duration
|
|
339
|
+
profile.timestamp # str: ISO timestamp
|
|
340
|
+
profile.success # bool: Operation succeeded
|
|
341
|
+
profile.memory_mb # Optional[float]: Memory used
|
|
342
|
+
profile.query_plan # Optional[str]: EXPLAIN output
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
**Methods:**
|
|
346
|
+
|
|
347
|
+
```python
|
|
348
|
+
# Check against threshold
|
|
349
|
+
is_slow = profile.is_regression(baseline_seconds=1.0, threshold_pct=20)
|
|
350
|
+
# Returns: bool
|
|
351
|
+
|
|
352
|
+
# Calculate regression percentage
|
|
353
|
+
regression = profile.calculate_regression(baseline_seconds=1.0)
|
|
354
|
+
# Returns: float (-50 to 100+)
|
|
355
|
+
|
|
356
|
+
# Get summary
|
|
357
|
+
summary = profile.get_summary()
|
|
358
|
+
# Returns: str - Human-readable summary
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
**Example:**
|
|
362
|
+
|
|
363
|
+
```python
|
|
364
|
+
# Check if operation is slow
|
|
365
|
+
if profile.duration_seconds > 5.0:
|
|
366
|
+
print(f"⚠️ Slow operation: {profile.duration_seconds}s")
|
|
367
|
+
|
|
368
|
+
# Check regression against baseline
|
|
369
|
+
if profile.is_regression(baseline_seconds=1.0, threshold_pct=20):
|
|
370
|
+
print("Performance regression detected!")
|
|
371
|
+
regression_pct = profile.calculate_regression(1.0)
|
|
372
|
+
print(f" Increase: {regression_pct}%")
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### PerformanceBaseline
|
|
376
|
+
|
|
377
|
+
Tracks performance metrics over time for regression detection.
|
|
378
|
+
|
|
379
|
+
**Import:**
|
|
380
|
+
```python
|
|
381
|
+
from confiture.testing.frameworks.performance import PerformanceBaseline
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
**Constructor:**
|
|
385
|
+
```python
|
|
386
|
+
baseline = PerformanceBaseline()
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
**Methods:**
|
|
390
|
+
|
|
391
|
+
```python
|
|
392
|
+
# Add a performance profile
|
|
393
|
+
baseline.add_profile(
|
|
394
|
+
operation_name="CREATE TABLE",
|
|
395
|
+
duration_seconds=0.5
|
|
396
|
+
)
|
|
397
|
+
# Returns: None
|
|
398
|
+
|
|
399
|
+
# Get baseline for operation
|
|
400
|
+
operation_baseline = baseline.get_baseline("CREATE TABLE")
|
|
401
|
+
# Returns: PerformanceProfile or None
|
|
402
|
+
|
|
403
|
+
# Check for regression
|
|
404
|
+
is_regression = baseline.detect_regression(
|
|
405
|
+
operation="CREATE TABLE",
|
|
406
|
+
current_seconds=0.6,
|
|
407
|
+
threshold_pct=20 # 20% increase
|
|
408
|
+
)
|
|
409
|
+
# Returns: bool
|
|
410
|
+
|
|
411
|
+
# Get all baselines
|
|
412
|
+
baselines = baseline.get_all_baselines()
|
|
413
|
+
# Returns: dict[str, PerformanceProfile]
|
|
414
|
+
|
|
415
|
+
# Statistics
|
|
416
|
+
stats = baseline.get_statistics()
|
|
417
|
+
# Returns: dict with min, max, average times
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
**Example:**
|
|
421
|
+
|
|
422
|
+
```python
|
|
423
|
+
from confiture.testing.frameworks.performance import PerformanceBaseline
|
|
424
|
+
|
|
425
|
+
baseline = PerformanceBaseline()
|
|
426
|
+
|
|
427
|
+
# Load historical baselines
|
|
428
|
+
baseline.add_profile("CREATE TABLE", 0.5)
|
|
429
|
+
baseline.add_profile("CREATE INDEX", 1.2)
|
|
430
|
+
baseline.add_profile("Bulk Insert 10k", 8.5)
|
|
431
|
+
|
|
432
|
+
# Check if new operation is regression
|
|
433
|
+
if baseline.detect_regression("CREATE TABLE", 0.65, threshold_pct=20):
|
|
434
|
+
print("Performance regression: CREATE TABLE now takes 30% longer")
|
|
435
|
+
|
|
436
|
+
# Get statistics
|
|
437
|
+
stats = baseline.get_statistics()
|
|
438
|
+
print(f"Average CREATE TABLE time: {stats['average']}s")
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
## Fixtures
|
|
442
|
+
|
|
443
|
+
### test_db_connection
|
|
444
|
+
|
|
445
|
+
Provides PostgreSQL database connection for tests.
|
|
446
|
+
|
|
447
|
+
**Usage:**
|
|
448
|
+
```python
|
|
449
|
+
def test_example(test_db_connection):
|
|
450
|
+
"""Test using database connection fixture."""
|
|
451
|
+
with test_db_connection.cursor() as cur:
|
|
452
|
+
cur.execute("CREATE TABLE test (id UUID PRIMARY KEY)")
|
|
453
|
+
test_db_connection.commit()
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
**Database Details:**
|
|
457
|
+
```python
|
|
458
|
+
# Connection string
|
|
459
|
+
DATABASE_URL: postgresql://confiture:confiture@localhost:5432/confiture_test
|
|
460
|
+
|
|
461
|
+
# Auto-cleanup
|
|
462
|
+
# Connection automatically closes after test
|
|
463
|
+
```
|
|
464
|
+
|
|
465
|
+
### temp_project_dir
|
|
466
|
+
|
|
467
|
+
Provides temporary project directory for test files.
|
|
468
|
+
|
|
469
|
+
**Usage:**
|
|
470
|
+
```python
|
|
471
|
+
def test_with_files(temp_project_dir):
|
|
472
|
+
"""Test using temporary directory."""
|
|
473
|
+
migration_file = temp_project_dir / "001_create_table.sql"
|
|
474
|
+
migration_file.write_text("CREATE TABLE test (...)")
|
|
475
|
+
|
|
476
|
+
assert migration_file.exists()
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### sample_confiture_schema
|
|
480
|
+
|
|
481
|
+
Provides sample PostgreSQL schema files.
|
|
482
|
+
|
|
483
|
+
**Usage:**
|
|
484
|
+
```python
|
|
485
|
+
def test_with_schema(sample_confiture_schema, test_db_connection):
|
|
486
|
+
"""Test using sample schema."""
|
|
487
|
+
with test_db_connection.cursor() as cur:
|
|
488
|
+
# sample_confiture_schema is dict[str, Path]
|
|
489
|
+
users_sql = sample_confiture_schema["users"].read_text()
|
|
490
|
+
cur.execute(users_sql)
|
|
491
|
+
test_db_connection.commit()
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
**Available Schemas:**
|
|
495
|
+
```python
|
|
496
|
+
sample_confiture_schema = {
|
|
497
|
+
"extensions.sql": Path(...), # PostgreSQL extensions
|
|
498
|
+
"users.sql": Path(...), # Users table
|
|
499
|
+
"posts.sql": Path(...), # Posts table
|
|
500
|
+
"comments.sql": Path(...), # Comments table
|
|
501
|
+
"user_stats.sql": Path(...), # User aggregates
|
|
502
|
+
}
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
### mutation_registry
|
|
506
|
+
|
|
507
|
+
Provides configured MutationRegistry for tests.
|
|
508
|
+
|
|
509
|
+
**Usage:**
|
|
510
|
+
```python
|
|
511
|
+
def test_with_mutations(mutation_registry):
|
|
512
|
+
"""Test using mutation framework."""
|
|
513
|
+
# mutation_registry is pre-configured MutationRegistry
|
|
514
|
+
|
|
515
|
+
schema_mutations = mutation_registry.schema_mutations
|
|
516
|
+
for mutation in schema_mutations:
|
|
517
|
+
# Test mutation killing
|
|
518
|
+
pass
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
### performance_profiler
|
|
522
|
+
|
|
523
|
+
Provides configured PerformanceProfiler for tests.
|
|
524
|
+
|
|
525
|
+
**Usage:**
|
|
526
|
+
```python
|
|
527
|
+
def test_with_performance(performance_profiler, test_db_connection):
|
|
528
|
+
"""Test using performance profiling."""
|
|
529
|
+
profile = performance_profiler.profile_operation(
|
|
530
|
+
operation="CREATE TABLE",
|
|
531
|
+
sql="CREATE TABLE test (...)",
|
|
532
|
+
db_connection=test_db_connection
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
assert profile.duration_seconds < 5.0
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
## Common Patterns
|
|
539
|
+
|
|
540
|
+
### Testing Forward Migration
|
|
541
|
+
|
|
542
|
+
```python
|
|
543
|
+
def test_forward_migration(test_db_connection):
|
|
544
|
+
"""Test forward migration succeeds and validates schema."""
|
|
545
|
+
with test_db_connection.cursor() as cur:
|
|
546
|
+
# Create table
|
|
547
|
+
cur.execute("""
|
|
548
|
+
CREATE TABLE users (
|
|
549
|
+
id UUID PRIMARY KEY,
|
|
550
|
+
name VARCHAR(255) NOT NULL
|
|
551
|
+
)
|
|
552
|
+
""")
|
|
553
|
+
test_db_connection.commit()
|
|
554
|
+
|
|
555
|
+
# Validate schema
|
|
556
|
+
cur.execute("""
|
|
557
|
+
SELECT column_name, data_type
|
|
558
|
+
FROM information_schema.columns
|
|
559
|
+
WHERE table_name = 'users'
|
|
560
|
+
""")
|
|
561
|
+
|
|
562
|
+
columns = {row[0]: row[1] for row in cur.fetchall()}
|
|
563
|
+
assert 'id' in columns
|
|
564
|
+
assert 'name' in columns
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
### Testing Rollback Safety
|
|
568
|
+
|
|
569
|
+
```python
|
|
570
|
+
def test_rollback_safe(test_db_connection):
|
|
571
|
+
"""Test rollback preserves data integrity."""
|
|
572
|
+
with test_db_connection.cursor() as cur:
|
|
573
|
+
# Create and populate table
|
|
574
|
+
cur.execute("CREATE TABLE data (id UUID PRIMARY KEY, value INT)")
|
|
575
|
+
cur.execute("INSERT INTO data VALUES (gen_random_uuid(), 42)")
|
|
576
|
+
test_db_connection.commit()
|
|
577
|
+
|
|
578
|
+
# Simulate migration
|
|
579
|
+
cur.execute("ALTER TABLE data ADD COLUMN new_col VARCHAR(255)")
|
|
580
|
+
test_db_connection.commit()
|
|
581
|
+
|
|
582
|
+
# Simulate rollback
|
|
583
|
+
cur.execute("ALTER TABLE data DROP COLUMN new_col")
|
|
584
|
+
test_db_connection.commit()
|
|
585
|
+
|
|
586
|
+
# Verify original data intact
|
|
587
|
+
cur.execute("SELECT COUNT(*) FROM data WHERE value = 42")
|
|
588
|
+
assert cur.fetchone()[0] == 1
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
### Testing Performance
|
|
592
|
+
|
|
593
|
+
```python
|
|
594
|
+
def test_performance(test_db_connection, performance_profiler):
|
|
595
|
+
"""Test operation performance against threshold."""
|
|
596
|
+
import time
|
|
597
|
+
|
|
598
|
+
with test_db_connection.cursor() as cur:
|
|
599
|
+
# Setup
|
|
600
|
+
cur.execute("""
|
|
601
|
+
CREATE TABLE perf_test (
|
|
602
|
+
id UUID PRIMARY KEY,
|
|
603
|
+
data BIGINT
|
|
604
|
+
)
|
|
605
|
+
""")
|
|
606
|
+
test_db_connection.commit()
|
|
607
|
+
|
|
608
|
+
# Measure bulk insert
|
|
609
|
+
start = time.time()
|
|
610
|
+
cur.execute("""
|
|
611
|
+
INSERT INTO perf_test (id, data)
|
|
612
|
+
SELECT gen_random_uuid(), i FROM generate_series(1, 10000) i
|
|
613
|
+
""")
|
|
614
|
+
test_db_connection.commit()
|
|
615
|
+
duration = time.time() - start
|
|
616
|
+
|
|
617
|
+
# Assert within threshold
|
|
618
|
+
assert duration < 30.0, f"Bulk insert took {duration}s (> 30s)"
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
## Error Handling
|
|
622
|
+
|
|
623
|
+
### Database Errors
|
|
624
|
+
|
|
625
|
+
```python
|
|
626
|
+
def test_with_error_handling(test_db_connection):
|
|
627
|
+
"""Handle database errors gracefully."""
|
|
628
|
+
try:
|
|
629
|
+
with test_db_connection.cursor() as cur:
|
|
630
|
+
# This will fail - table doesn't exist
|
|
631
|
+
cur.execute("INSERT INTO nonexistent VALUES (1)")
|
|
632
|
+
test_db_connection.commit()
|
|
633
|
+
except Exception as e:
|
|
634
|
+
test_db_connection.rollback()
|
|
635
|
+
print(f"Expected error: {e}")
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
### Timeout Handling
|
|
639
|
+
|
|
640
|
+
```python
|
|
641
|
+
def test_with_timeout(test_db_connection):
|
|
642
|
+
"""Handle operation timeouts."""
|
|
643
|
+
try:
|
|
644
|
+
with test_db_connection.cursor() as cur:
|
|
645
|
+
# Set timeout for long operation
|
|
646
|
+
cur.execute("SET statement_timeout TO '5s'")
|
|
647
|
+
|
|
648
|
+
# This might timeout
|
|
649
|
+
cur.execute("SELECT * FROM generate_series(1, 1000000)")
|
|
650
|
+
except TimeoutError:
|
|
651
|
+
print("Operation timed out")
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
## Environment Variables
|
|
655
|
+
|
|
656
|
+
```bash
|
|
657
|
+
# PostgreSQL Connection
|
|
658
|
+
DATABASE_URL=postgresql://confiture:confiture@localhost:5432/confiture_test
|
|
659
|
+
POSTGRES_HOST=localhost
|
|
660
|
+
POSTGRES_PORT=5432
|
|
661
|
+
POSTGRES_USER=confiture
|
|
662
|
+
POSTGRES_PASSWORD=confiture
|
|
663
|
+
POSTGRES_DB=confiture_test
|
|
664
|
+
|
|
665
|
+
# Performance Thresholds
|
|
666
|
+
PERF_CREATE_TABLE_THRESHOLD=5.0 # seconds
|
|
667
|
+
PERF_CREATE_INDEX_THRESHOLD=10.0 # seconds
|
|
668
|
+
PERF_BULK_INSERT_10K_THRESHOLD=30.0 # seconds
|
|
669
|
+
|
|
670
|
+
# Mutation Testing
|
|
671
|
+
MUTATION_TIMEOUT=300 # seconds
|
|
672
|
+
MUTATION_VERBOSE=true # Enable detailed output
|
|
673
|
+
```
|
|
674
|
+
|
|
675
|
+
## Best Practices
|
|
676
|
+
|
|
677
|
+
1. **Use Fixtures**: Always use provided fixtures for database access
|
|
678
|
+
2. **Clean Up**: Always drop tables created with `DROP TABLE IF EXISTS`
|
|
679
|
+
3. **Isolate Tests**: Each test should be independent
|
|
680
|
+
4. **Use Assertions**: Clear assertion messages for failures
|
|
681
|
+
5. **Document Tests**: Include docstrings explaining test purpose
|
|
682
|
+
6. **Handle Transactions**: Explicitly commit/rollback
|
|
683
|
+
7. **Check Performance**: Always assert performance thresholds
|
|
684
|
+
8. **Verify Data**: Validate data integrity after operations
|
|
685
|
+
|
|
686
|
+
## Troubleshooting
|
|
687
|
+
|
|
688
|
+
### "Database connection failed"
|
|
689
|
+
|
|
690
|
+
```bash
|
|
691
|
+
# Check PostgreSQL is running
|
|
692
|
+
pg_isready -h localhost -p 5432
|
|
693
|
+
|
|
694
|
+
# Verify credentials
|
|
695
|
+
psql -h localhost -U confiture -d confiture_test
|
|
696
|
+
```
|
|
697
|
+
|
|
698
|
+
### "Mutation not detected"
|
|
699
|
+
|
|
700
|
+
```python
|
|
701
|
+
# Make sure test actually validates the mutation
|
|
702
|
+
# Good:
|
|
703
|
+
assert migrated_schema.columns['id'].type == 'UUID'
|
|
704
|
+
|
|
705
|
+
# Bad:
|
|
706
|
+
assert True # Always passes, mutation not detected
|
|
707
|
+
```
|
|
708
|
+
|
|
709
|
+
### "Performance regression detected"
|
|
710
|
+
|
|
711
|
+
```python
|
|
712
|
+
# Review recent commits for performance impacts
|
|
713
|
+
# Run test in isolation
|
|
714
|
+
uv run pytest tests/migration_testing/test_performance.py -v --durations=10
|
|
715
|
+
```
|
|
716
|
+
|
|
717
|
+
## Additional Resources
|
|
718
|
+
|
|
719
|
+
- [Confiture Documentation](https://github.com/your-org/confiture)
|
|
720
|
+
- [PostgreSQL Documentation](https://www.postgresql.org/docs/)
|
|
721
|
+
- [psycopg3 Documentation](https://www.psycopg.org/)
|
|
722
|
+
- [pytest Documentation](https://docs.pytest.org/)
|