Skip to content

Commit d974125

Browse files
committed
Ensure test order: create_empty after teapod_roundtrip
1 parent baa3da3 commit d974125

File tree

5 files changed

+129
-125
lines changed

5 files changed

+129
-125
lines changed

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,6 @@ dev = [
5555
"pre-commit-hooks>=6.0.0",
5656
"pytest>=8.4.2",
5757
"pytest-dependency>=0.6.0",
58-
"pytest-order>=1.3.0",
5958
"typeguard>=4.4.4",
6059
"xdoctest[colors]>=1.3.0",
6160
"Pygments>=2.19.2"

tests/integration/test_segy_roundtrip_teapot.py

Lines changed: 4 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,17 @@
99
import numpy as np
1010
import numpy.testing as npt
1111
import pytest
12-
from segy.schema import HeaderField
13-
from segy.schema import ScalarType
14-
from segy.standards import get_segy_standard
12+
from tests.integration.testing_helpers import UNITS_METER
13+
from tests.integration.testing_helpers import UNITS_NONE
14+
from tests.integration.testing_helpers import UNITS_SECOND
1515
from tests.integration.testing_helpers import get_inline_header_values
16+
from tests.integration.testing_helpers import get_teapot_segy_spec
1617
from tests.integration.testing_helpers import get_values
1718
from tests.integration.testing_helpers import validate_xr_variable
1819

1920
from mdio import __version__
2021
from mdio import mdio_to_segy
2122
from mdio.api.io import open_mdio
22-
from mdio.builder.schemas.v1.units import LengthUnitEnum
23-
from mdio.builder.schemas.v1.units import LengthUnitModel
24-
from mdio.builder.schemas.v1.units import TimeUnitEnum
25-
from mdio.builder.schemas.v1.units import TimeUnitModel
2623
from mdio.builder.template_registry import TemplateRegistry
2724
from mdio.converters.segy import segy_to_mdio
2825
from mdio.segy.file import SegyFileWrapper
@@ -50,17 +47,6 @@ def teapot_segy_spec() -> SegySpec:
5047
return get_teapot_segy_spec()
5148

5249

53-
def get_teapot_segy_spec() -> SegySpec:
54-
"""Return the customized SEG-Y specification for the teapot dome dataset."""
55-
teapot_fields = [
56-
HeaderField(name="inline", byte=17, format=ScalarType.INT32),
57-
HeaderField(name="crossline", byte=13, format=ScalarType.INT32),
58-
HeaderField(name="cdp_x", byte=81, format=ScalarType.INT32),
59-
HeaderField(name="cdp_y", byte=85, format=ScalarType.INT32),
60-
]
61-
return get_segy_standard(1.0).customize(trace_header_fields=teapot_fields)
62-
63-
6450
def text_header_teapot_dome() -> str:
6551
"""Return the teapot dome expected text header."""
6652
header_rows = [
@@ -157,12 +143,6 @@ def raw_binary_header_teapot_dome() -> str:
157143
)
158144

159145

160-
UNITS_NONE = None
161-
UNITS_METER = LengthUnitModel(length=LengthUnitEnum.METER)
162-
UNITS_SECOND = TimeUnitModel(time=TimeUnitEnum.SECOND)
163-
164-
165-
@pytest.mark.order(1)
166146
class TestTeapotRoundtrip:
167147
"""Tests for Teapot Dome data ingestion and export."""
168148

Lines changed: 97 additions & 86 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
"""Test for create_empty_mdio function."""
1+
"""Test for create_empty_mdio function.
2+
3+
This set of tests has to run after the segy_roundtrip_teapot tests have run because
4+
the teapot dataset is used as the input for the create_empty_like test.
5+
6+
NOTE: The only reliable way to ensure the test order (including the case when the
7+
test are run in parallel) is to use the alphabetical order of the test names.
8+
"""
29

310
from __future__ import annotations
411

@@ -22,9 +29,11 @@
2229
from xarray import Dataset as xr_Dataset
2330

2431

25-
from tests.integration.test_segy_roundtrip_teapot import get_teapot_segy_spec
26-
from tests.integration.testing_helpers import get_values
32+
from tests.integration.testing_helpers import UNITS_NONE, UNITS_SECOND, UNITS_METER, UNITS_FOOT, UNITS_METER_PER_SECOND, UNITS_FEET_PER_SECOND
33+
from tests.integration.testing_helpers import get_teapot_segy_spec
2734
from tests.integration.testing_helpers import validate_xr_variable
35+
from tests.integration.testing_helpers import get_values
36+
2837

2938
from mdio import __version__
3039
from mdio.api.create import create_empty
@@ -37,14 +46,6 @@
3746
from mdio.converters.mdio import mdio_to_segy
3847
from mdio.core import Dimension
3948

40-
UNITS_NONE = None
41-
UNITS_METER = LengthUnitModel(length=LengthUnitEnum.METER)
42-
UNITS_SECOND = TimeUnitModel(time=TimeUnitEnum.SECOND)
43-
UNITS_METER_PER_SECOND = SpeedUnitModel(speed=SpeedUnitEnum.METER_PER_SECOND)
44-
UNITS_FOOT = LengthUnitModel(length=LengthUnitEnum.FOOT)
45-
UNITS_FEET_PER_SECOND = SpeedUnitModel(speed=SpeedUnitEnum.FEET_PER_SECOND)
46-
47-
4849
class PostStack3DVelocityTemplate(Seismic3DPostStackTemplate):
4950
"""Custom template that uses 'velocity' as the default variable name instead of 'amplitude'."""
5051

@@ -80,13 +81,72 @@ def _name(self) -> str:
8081
domain_suffix = self._data_domain.capitalize()
8182
return f"PostStack3DVelocity{domain_suffix}"
8283

83-
84-
@pytest.mark.order(1000)
8584
class TestCreateEmptyMdio:
8685
"""Tests for create_empty_mdio function."""
8786

8887
@classmethod
89-
def _validate_empty_mdio_dataset(cls, ds: xr_Dataset, has_headers: bool, is_velocity: bool) -> None:
88+
def _create_empty_mdio(cls, create_headers: bool, output_path: Path, overwrite: bool = True) -> None:
89+
"""Create a temporary empty MDIO file for testing."""
90+
# Create the grid with the specified dimensions
91+
dims = [
92+
Dimension(name="inline", coords=range(1, 346, 1)), # 100-300 with step 1
93+
Dimension(name="crossline", coords=range(1, 189, 1)), # 1000-1600 with step 2
94+
Dimension(name="time", coords=range(0, 3002, 2)), # 0-3 seconds 4ms sample rate
95+
]
96+
97+
# If later on, we want to export to SEG-Y, we need to provide the trace header spec.
98+
# The HeaderSpec can be either standard or customized.
99+
headers = get_teapot_segy_spec().trace.header if create_headers else None
100+
# Create an empty MDIO v1 metric post-stack 3D time velocity dataset
101+
create_empty(
102+
mdio_template=PostStack3DVelocityTemplate(data_domain="time", is_metric=True),
103+
dimensions=dims,
104+
output_path=output_path,
105+
headers=headers,
106+
overwrite=overwrite,
107+
)
108+
109+
@classmethod
110+
def validate_teapod_dataset_metadata(cls, ds: xr_Dataset, is_velocity: bool) -> None:
111+
"""Validate the dataset metadata."""
112+
if is_velocity:
113+
assert ds.name == "PostStack3DVelocityTime"
114+
else:
115+
assert ds.name == "PostStack3DTime"
116+
117+
# Check basic metadata attributes
118+
expected_attrs = {
119+
"apiVersion": __version__,
120+
"name": ds.name,
121+
}
122+
actual_attrs_json = ds.attrs
123+
124+
# Compare one by one due to ever changing createdOn
125+
for key, value in expected_attrs.items():
126+
assert key in actual_attrs_json
127+
if key == "createdOn":
128+
assert actual_attrs_json[key] is not None
129+
else:
130+
assert actual_attrs_json[key] == value
131+
132+
# Check that createdOn exists
133+
assert "createdOn" in actual_attrs_json
134+
assert actual_attrs_json["createdOn"] is not None
135+
136+
# Validate template attributes
137+
attributes = ds.attrs["attributes"]
138+
assert attributes is not None
139+
assert len(attributes) == 3
140+
# Validate all attributes provided by the abstract template
141+
if is_velocity:
142+
assert attributes["defaultVariableName"] == "velocity"
143+
else:
144+
assert attributes["defaultVariableName"] == "amplitude"
145+
assert attributes["surveyType"] == "3D"
146+
assert attributes["gatherType"] == "stacked"
147+
148+
@classmethod
149+
def validate_teapod_dataset_variables(cls, ds: xr_Dataset, header_dtype: np.dtype | None, is_velocity: bool) -> None:
90150
"""Validate an empty MDIO dataset structure and content."""
91151
# Check that the dataset has the expected shape
92152
assert ds.sizes == {"inline": 345, "crossline": 188, "time": 1501}
@@ -102,10 +162,10 @@ def _validate_empty_mdio_dataset(cls, ds: xr_Dataset, has_headers: bool, is_velo
102162
validate_xr_variable(ds, "cdp_x", {"inline": 345, "crossline": 188}, UNITS_METER, np.float64)
103163
validate_xr_variable(ds, "cdp_y", {"inline": 345, "crossline": 188}, UNITS_METER, np.float64)
104164

105-
if has_headers:
165+
if header_dtype is not None:
106166
# Validate the headers (should be empty for empty dataset)
107167
# Infer the dtype from segy_spec and ignore endianness
108-
header_dtype = get_teapot_segy_spec().trace.header.dtype.newbyteorder("native")
168+
header_dtype = header_dtype.newbyteorder("native")
109169
validate_xr_variable(ds, "headers", {"inline": 345, "crossline": 188}, UNITS_NONE, header_dtype)
110170
validate_xr_variable(ds, "segy_file_header", dims={}, units=UNITS_NONE, data_type=np.dtype("U1"))
111171
else:
@@ -127,28 +187,6 @@ def _validate_empty_mdio_dataset(cls, ds: xr_Dataset, has_headers: bool, is_velo
127187
ds, "amplitude", {"inline": 345, "crossline": 188, "time": 1501}, UNITS_NONE, np.float32
128188
)
129189

130-
@classmethod
131-
def _create_empty_mdio(cls, create_headers: bool, output_path: Path, overwrite: bool = True) -> None:
132-
"""Create a temporary empty MDIO file for testing."""
133-
# Create the grid with the specified dimensions
134-
dims = [
135-
Dimension(name="inline", coords=range(1, 346, 1)), # 100-300 with step 1
136-
Dimension(name="crossline", coords=range(1, 189, 1)), # 1000-1600 with step 2
137-
Dimension(name="time", coords=range(0, 3002, 2)), # 0-3 seconds 4ms sample rate
138-
]
139-
140-
# If later on, we want to export to SEG-Y, we need to provide the trace header spec.
141-
# The HeaderSpec can be either standard or customized.
142-
headers = get_teapot_segy_spec().trace.header if create_headers else None
143-
# Create an empty MDIO v1 metric post-stack 3D time velocity dataset
144-
create_empty(
145-
mdio_template=PostStack3DVelocityTemplate(data_domain="time", is_metric=True),
146-
dimensions=dims,
147-
output_path=output_path,
148-
headers=headers,
149-
overwrite=overwrite,
150-
)
151-
152190
@pytest.fixture(scope="class")
153191
def mdio_with_headers(self, empty_mdio_dir: Path) -> Path:
154192
"""Create a temporary empty MDIO file for testing.
@@ -171,56 +209,22 @@ def mdio_no_headers(self, empty_mdio_dir: Path) -> Path:
171209
self._create_empty_mdio(create_headers=False, output_path=empty_mdio)
172210
return empty_mdio
173211

174-
def validate_dataset_metadata(self, ds: xr_Dataset, is_velocity: bool) -> None:
175-
"""Validate the dataset metadata."""
176-
if is_velocity:
177-
assert ds.name == "PostStack3DVelocityTime"
178-
else:
179-
assert ds.name == "PostStack3DTime"
180-
181-
# Check basic metadata attributes
182-
expected_attrs = {
183-
"apiVersion": __version__,
184-
"name": ds.name,
185-
}
186-
actual_attrs_json = ds.attrs
187-
188-
# Compare one by one due to ever changing createdOn
189-
for key, value in expected_attrs.items():
190-
assert key in actual_attrs_json
191-
if key == "createdOn":
192-
assert actual_attrs_json[key] is not None
193-
else:
194-
assert actual_attrs_json[key] == value
195-
196-
# Check that createdOn exists
197-
assert "createdOn" in actual_attrs_json
198-
assert actual_attrs_json["createdOn"] is not None
199-
200-
# Validate template attributes
201-
attributes = ds.attrs["attributes"]
202-
assert attributes is not None
203-
assert len(attributes) == 3
204-
# Validate all attributes provided by the abstract template
205-
if is_velocity:
206-
assert attributes["defaultVariableName"] == "velocity"
207-
else:
208-
assert attributes["defaultVariableName"] == "amplitude"
209-
assert attributes["surveyType"] == "3D"
210-
assert attributes["gatherType"] == "stacked"
211212

212213
def test_dataset_metadata(self, mdio_with_headers: Path) -> None:
213214
"""Test dataset metadata for empty MDIO file."""
214215
ds = open_mdio(mdio_with_headers)
215-
self.validate_dataset_metadata(ds, is_velocity=True)
216+
self.validate_teapod_dataset_metadata(ds, is_velocity=True)
217+
216218

217219
def test_variables(self, mdio_with_headers: Path, mdio_no_headers: Path) -> None:
218220
"""Test grid validation for empty MDIO file."""
221+
219222
ds = open_mdio(mdio_with_headers)
220-
self._validate_empty_mdio_dataset(ds, has_headers=True, is_velocity=True)
223+
header_dtype = get_teapot_segy_spec().trace.header.dtype
224+
self.validate_teapod_dataset_variables(ds, header_dtype=header_dtype, is_velocity=True)
221225

222226
ds = open_mdio(mdio_no_headers)
223-
self._validate_empty_mdio_dataset(ds, has_headers=False, is_velocity=True)
227+
self.validate_teapod_dataset_variables(ds, header_dtype=None, is_velocity=True)
224228

225229
def test_overwrite_behavior(self, empty_mdio_dir: Path) -> None:
226230
"""Test overwrite parameter behavior in create_empty_mdio."""
@@ -246,7 +250,9 @@ def test_overwrite_behavior(self, empty_mdio_dir: Path) -> None:
246250

247251
# Validate that the MDIO file can be loaded correctly using the helper function
248252
ds = open_mdio(empty_mdio)
249-
self._validate_empty_mdio_dataset(ds, has_headers=True, is_velocity=True)
253+
self.validate_teapod_dataset_metadata(ds, is_velocity=True)
254+
header_dtype = get_teapot_segy_spec().trace.header.dtype
255+
self.validate_teapod_dataset_variables(ds, header_dtype=header_dtype, is_velocity=True)
250256

251257
# Verify the garbage data was overwritten (should not exist)
252258
assert not garbage_file.exists(), "Garbage file should have been overwritten"
@@ -375,16 +381,21 @@ def test_populate_empty_dataset(self, mdio_with_headers: Path) -> None:
375381
output_path=mdio_with_headers.parent / "populated_empty.sgy",
376382
)
377383

378-
@pytest.mark.order(1001)
379-
@pytest.mark.dependency
380-
def test_create_empty_like(self, teapot_mdio_tmp: Path, mdio_with_headers: Path) -> None:
381-
"""Create an empty MDIO file like the input file."""
382-
_ = mdio_with_headers
384+
385+
def test_create_empty_like(self, teapot_mdio_tmp: Path) -> None:
386+
"""Create an empty MDIO file like the input MDIO file.
387+
388+
This test has to run after the segy_roundtrip_teapot tests have run because
389+
its uses 'teapot_mdio_tmp' created by the segy_roundtrip_teapot tests as the input.
390+
"""
391+
383392
ds = create_empty_like(
384393
input_path=teapot_mdio_tmp,
394+
# TODO: write to a file
385395
output_path=None, # We don't want to write to disk for now
386396
keep_coordinates=True,
387397
overwrite=True,
388398
)
389-
self.validate_dataset_metadata(ds, is_velocity=False)
390-
self._validate_empty_mdio_dataset(ds, has_headers=True, is_velocity=False)
399+
self.validate_teapod_dataset_metadata(ds, is_velocity=False)
400+
header_dtype = get_teapot_segy_spec().trace.header.dtype
401+
self.validate_teapod_dataset_variables(ds, header_dtype=header_dtype, is_velocity=False)

tests/integration/testing_helpers.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,36 @@
44

55
import numpy as np
66
import xarray as xr
7+
from segy.schema import HeaderField
8+
from segy.schema import ScalarType
9+
from segy.schema.segy import SegySpec
10+
from segy.standards import get_segy_standard
711

812
from mdio.builder.schemas.v1.units import AllUnitModel
13+
from mdio.builder.schemas.v1.units import LengthUnitEnum
14+
from mdio.builder.schemas.v1.units import LengthUnitModel
15+
from mdio.builder.schemas.v1.units import SpeedUnitEnum
16+
from mdio.builder.schemas.v1.units import SpeedUnitModel
17+
from mdio.builder.schemas.v1.units import TimeUnitEnum
18+
from mdio.builder.schemas.v1.units import TimeUnitModel
19+
20+
UNITS_NONE = None
21+
UNITS_METER = LengthUnitModel(length=LengthUnitEnum.METER)
22+
UNITS_SECOND = TimeUnitModel(time=TimeUnitEnum.SECOND)
23+
UNITS_METER_PER_SECOND = SpeedUnitModel(speed=SpeedUnitEnum.METER_PER_SECOND)
24+
UNITS_FOOT = LengthUnitModel(length=LengthUnitEnum.FOOT)
25+
UNITS_FEET_PER_SECOND = SpeedUnitModel(speed=SpeedUnitEnum.FEET_PER_SECOND)
26+
27+
28+
def get_teapot_segy_spec() -> SegySpec:
29+
"""Return the customized SEG-Y specification for the teapot dome dataset."""
30+
teapot_fields = [
31+
HeaderField(name="inline", byte=17, format=ScalarType.INT32),
32+
HeaderField(name="crossline", byte=13, format=ScalarType.INT32),
33+
HeaderField(name="cdp_x", byte=81, format=ScalarType.INT32),
34+
HeaderField(name="cdp_y", byte=85, format=ScalarType.INT32),
35+
]
36+
return get_segy_standard(1.0).customize(trace_header_fields=teapot_fields)
937

1038

1139
def get_values(arr: xr.DataArray) -> np.ndarray:

uv.lock

Lines changed: 0 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)