Skip to content

Commit 877cf0b

Browse files
committed
MAINT: Clarify migration tests and documentation
1 parent fc901df commit 877cf0b

3 files changed

Lines changed: 121 additions & 62 deletions

File tree

src/fmu/settings/_migrations/README.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -95,26 +95,27 @@ The following example changes the current `ProjectConfig` from schema version 1
9595
### 1. Update the current model
9696

9797
Edit the existing model in `models/project_config.py`. Do not create a second
98-
`ProjectConfig` class. The `...` lines below represent all unchanged fields in the
99-
current model, such as `version`, `created_at`, `created_by`, `masterdata`, and
100-
`rms`. Only the schema version and the renamed field change in this example:
98+
`ProjectConfig` class. Some existing fields and default values are not shown in the
99+
shortened example. The comments show where they belong. These fields include `version`,
100+
`created_at`, `created_by`, `masterdata`, and `rms`. Only the schema version and the
101+
renamed field change in this example:
101102

102103
```python
103104
class ProjectConfig(ResettableBaseModel):
104105
"""The configuration file in a .fmu directory."""
105106

106107
schema_version: Literal[2] = 2
107-
# ... unchanged fields ...
108+
# Existing fields before this field are not shown in this example.
108109
max_cache_revisions: int = Field(default=10, ge=5)
109-
# ... unchanged fields ...
110+
# Existing fields after this field are not shown in this example.
110111

111112
@classmethod
112113
def reset(cls: type[Self]) -> Self:
113114
"""Reset the configuration to its defaults."""
114115
return cls(
115-
# ... unchanged defaults ...
116+
# Existing default values before this one are not shown.
116117
max_cache_revisions=10,
117-
# ... unchanged defaults ...
118+
# Existing default values after this one are not shown.
118119
)
119120
```
120121

tests/test_migrations/test_migration_manager.py

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,11 @@ def test_migration_manager_applies_all_steps_without_mutating_input() -> None:
8080
value="old",
8181
migrations_applied=[1, 2],
8282
)
83-
assert data["migrations_applied"] == []
84-
assert data["schema_version"] == 1
83+
assert data == {
84+
"schema_version": 1,
85+
"value": "old",
86+
"migrations_applied": [],
87+
}
8588

8689

8790
def test_migration_manager_treats_missing_version_as_version_one() -> None:
@@ -106,6 +109,72 @@ def test_migration_manager_treats_missing_version_as_version_one() -> None:
106109
assert "schema_version" not in data
107110

108111

112+
def test_migration_manager_adds_legacy_version_to_returned_model() -> None:
113+
"""Unversioned data returns a version-one model without changing the input."""
114+
data = {"value": "old"}
115+
manager = MigrationManager(VersionOneModel, {})
116+
117+
result = manager.migrate_resource(data)
118+
119+
assert result == VersionOneModel(schema_version=1, value="old")
120+
assert data == {"value": "old"}
121+
122+
123+
@pytest.mark.parametrize(
124+
("data", "expected"),
125+
[
126+
({"schema_version": 3}, False),
127+
({"schema_version": 2}, True),
128+
({}, True),
129+
],
130+
)
131+
def test_migration_manager_reports_when_migration_is_required(
132+
data: dict[str, Any],
133+
expected: bool,
134+
) -> None:
135+
"""The requirement check compares stored and current schema versions."""
136+
manager = MigrationManager(
137+
VersionThreeModel,
138+
{
139+
1: migrate_one_to_two,
140+
2: migrate_two_to_three,
141+
},
142+
)
143+
144+
assert manager.requires_migration(data) is expected
145+
146+
147+
def test_requires_migration_rejects_missing_step() -> None:
148+
"""The requirement check rejects an incomplete migration path."""
149+
manager = MigrationManager(
150+
VersionThreeModel,
151+
{2: migrate_two_to_three},
152+
)
153+
154+
with pytest.raises(
155+
MigrationError,
156+
match=(
157+
"Missing VersionThreeModel migration function for stored data from "
158+
"schema version 1 to 2"
159+
),
160+
):
161+
manager.requires_migration({"schema_version": 1})
162+
163+
164+
def test_requires_migration_rejects_newer_schema() -> None:
165+
"""The requirement check rejects stored data from a newer schema."""
166+
manager = MigrationManager(VersionThreeModel, {})
167+
168+
with pytest.raises(
169+
MigrationError,
170+
match=(
171+
"Stored VersionThreeModel data has schema version 4, which is newer "
172+
"than supported version 3"
173+
),
174+
):
175+
manager.requires_migration({"schema_version": 4})
176+
177+
109178
def test_migration_manager_rejects_missing_step() -> None:
110179
"""Every intermediate migration must be registered."""
111180
manager = MigrationManager(

tests/test_migrations/test_resource_migration.py

Lines changed: 42 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from fmu.settings.models.project_config import ProjectConfig
2020

2121
if TYPE_CHECKING:
22-
from fmu.settings._fmu_dir import ProjectFMUDirectory, UserFMUDirectory
22+
from fmu.settings._fmu_dir import ProjectFMUDirectory
2323

2424

2525
class VersionTwoResource(BaseModel):
@@ -106,10 +106,10 @@ def test_save_after_migration_backs_up_old_content(
106106
assert {"schema_version": 2, "value": "updated"} in cached_data
107107

108108

109-
def test_cached_old_schema_is_readable_and_restorable(
109+
def test_cached_version_one_content_can_be_read_and_restored(
110110
fmu_dir: ProjectFMUDirectory,
111111
) -> None:
112-
"""Existing cache APIs migrate an old backup before using its content."""
112+
"""Reading or restoring the revision migrates its content to version two."""
113113
manager = MigratableResourceManager(fmu_dir)
114114
old_content = json.dumps({"schema_version": 1, "value": "old"}, indent=2)
115115
fmu_dir.write_text_file(manager.relative_path, old_content)
@@ -141,10 +141,10 @@ def test_cached_old_schema_is_readable_and_restorable(
141141
}
142142

143143

144-
def test_cache_read_rejects_newer_schema_with_resource_context(
144+
def test_cache_manager_cannot_read_content_from_a_newer_schema_version(
145145
fmu_dir: ProjectFMUDirectory,
146146
) -> None:
147-
"""A cache migration error identifies the affected resource."""
147+
"""A version-two manager cannot read a cached version-three revision."""
148148
manager = MigratableResourceManager(fmu_dir)
149149
revision = fmu_dir.cache.store_revision(
150150
manager.relative_path,
@@ -167,27 +167,6 @@ def test_cache_read_rejects_newer_schema_with_resource_context(
167167
)
168168

169169

170-
def test_force_load_without_store_cache_preserves_existing_cached_model(
171-
fmu_dir: ProjectFMUDirectory,
172-
) -> None:
173-
"""A forced migrated read can avoid replacing the in-memory cache."""
174-
manager = MigratableResourceManager(fmu_dir)
175-
fmu_dir.write_text_file(
176-
manager.relative_path,
177-
json.dumps({"schema_version": 1, "value": "first"}),
178-
)
179-
cached = manager.load()
180-
fmu_dir.write_text_file(
181-
manager.relative_path,
182-
json.dumps({"schema_version": 1, "value": "second"}),
183-
)
184-
185-
reloaded = manager.load(force=True, store_cache=False)
186-
187-
assert reloaded == VersionTwoResource(value="second")
188-
assert manager._cache == cached
189-
190-
191170
def test_migration_save_respects_lock_before_backup(
192171
fmu_dir: ProjectFMUDirectory,
193172
) -> None:
@@ -268,10 +247,17 @@ def test_current_schema_save_does_not_add_migration_backup(
268247
assert not fmu_dir.get_file_path("migration-backups").exists()
269248

270249

271-
def test_save_replaces_non_object_json_without_migration_backup(
250+
def test_save_overwrites_non_object_json_without_a_migration_backup(
272251
fmu_dir: ProjectFMUDirectory,
273252
) -> None:
274-
"""A valid model can replace non-object JSON without preserving it."""
253+
"""Allow a valid save when the existing resource contains a JSON list.
254+
255+
For example, the existing file contains only ``[]`` instead of the expected JSON
256+
object. The list has no schema version, so it cannot be migrated and is not
257+
saved as a migration backup. The valid version-two model replaces the list with
258+
``{"schema_version": 2, "value": "current"}``, and only this replacement is
259+
added to the normal cache.
260+
"""
275261
manager = MigratableResourceManager(fmu_dir)
276262
fmu_dir.write_text_file(manager.relative_path, json.dumps([]))
277263

@@ -287,12 +273,13 @@ def test_save_replaces_non_object_json_without_migration_backup(
287273
"schema_version": 2,
288274
"value": "current",
289275
}
276+
assert not fmu_dir.get_file_path("migration-backups").exists()
290277

291278

292-
def test_save_rejects_newer_stored_schema_before_overwrite(
279+
def test_save_does_not_overwrite_a_newer_stored_version(
293280
fmu_dir: ProjectFMUDirectory,
294281
) -> None:
295-
"""A save does not overwrite stored data from a newer schema."""
282+
"""A version-two manager rejects version-three data before writing or caching."""
296283
manager = MigratableResourceManager(fmu_dir)
297284
future_content = json.dumps({"schema_version": 3, "value": "future"})
298285
fmu_dir.write_text_file(manager.relative_path, future_content)
@@ -345,35 +332,37 @@ def test_load_rejects_non_object_resource(
345332
manager.load()
346333

347334

348-
def test_project_resource_managers_have_version_one_migration_managers(
349-
fmu_dir: ProjectFMUDirectory,
335+
@pytest.mark.parametrize(
336+
("directory_fixture", "resource_name"),
337+
[
338+
("fmu_dir", "config"),
339+
("fmu_dir", "mappings"),
340+
("user_fmu_dir", "config"),
341+
],
342+
)
343+
def test_resource_has_version_one_migration_manager(
344+
request: pytest.FixtureRequest,
345+
directory_fixture: str,
346+
resource_name: str,
350347
) -> None:
351-
"""Project resources are wired without a dummy version two."""
352-
managers = [
353-
fmu_dir.config.migration_manager,
354-
fmu_dir.mappings.migration_manager,
355-
]
356-
357-
assert all(manager is not None for manager in managers)
358-
assert all(manager.current_version == 1 for manager in managers if manager)
359-
assert all(manager.migrations == {} for manager in managers if manager)
348+
"""Check the current schema version and migrations for each resource.
360349
361-
362-
def test_user_config_has_version_one_migration_manager(
363-
user_fmu_dir: UserFMUDirectory,
364-
) -> None:
365-
"""User config is wired without a dummy version two."""
366-
manager = user_fmu_dir.config.migration_manager
350+
Project config, user config, and mappings currently use schema version one, so
351+
their migration registries are empty. Update this test when a resource gets a
352+
new schema version and migration function.
353+
"""
354+
fmu_directory = request.getfixturevalue(directory_fixture)
355+
manager = getattr(fmu_directory, resource_name).migration_manager
367356

368357
assert manager is not None
369358
assert manager.current_version == 1
370359
assert manager.migrations == {}
371360

372361

373-
def test_project_config_cache_boundary_handles_unversioned_revision(
362+
def test_unversioned_project_config_can_be_read_and_restored_from_cache(
374363
fmu_dir: ProjectFMUDirectory,
375364
) -> None:
376-
"""Project cache APIs normalize and restore an unversioned config revision."""
365+
"""Legacy config restores version-one data, runtime settings, and changelog."""
377366
legacy_config = fmu_dir.config.load().model_dump(mode="json")
378367
legacy_config.pop("schema_version")
379368
legacy_config["cache_max_revisions"] = 7
@@ -397,10 +386,10 @@ def test_project_config_cache_boundary_handles_unversioned_revision(
397386
assert fmu_dir.changelog.load().root[-1].change_type == ChangeType.restore
398387

399388

400-
def test_mappings_cache_boundary_handles_unversioned_revision(
389+
def test_unversioned_mappings_can_be_read_and_restored_from_cache(
401390
fmu_dir: ProjectFMUDirectory,
402391
) -> None:
403-
"""Project cache APIs use the mappings migration manager across the union."""
392+
"""Legacy mappings restore as version one and add a changelog entry."""
404393
legacy_mappings = InternalMappings().model_dump(mode="json")
405394
legacy_mappings.pop("schema_version")
406395
revision = fmu_dir.cache.store_revision(
@@ -419,10 +408,10 @@ def test_mappings_cache_boundary_handles_unversioned_revision(
419408
assert fmu_dir.changelog.load().root[-1].change_type == ChangeType.restore
420409

421410

422-
def test_resource_manager_rejects_mismatched_migration_model(
411+
def test_resource_manager_rejects_migration_functions_for_another_model(
423412
fmu_dir: ProjectFMUDirectory,
424413
) -> None:
425-
"""A resource manager cannot use migrations for a different model."""
414+
"""The migration and resource managers must use the same Pydantic model."""
426415
migration_manager = MigrationManager(
427416
VersionTwoResource,
428417
{1: migrate_one_to_two},

0 commit comments

Comments
 (0)