Skip to content

Commit 68bb78e

Browse files
committed
Run official SPSS conformance fixtures
1 parent af372f9 commit 68bb78e

6 files changed

Lines changed: 131 additions & 5 deletions

File tree

.github/workflows/ci.yml

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ on:
44
push:
55
pull_request:
66

7+
env:
8+
OPENSTATSPEC_SPECIFICATION_DIR: ${{ github.workspace }}/openstatspec-specification
9+
710
jobs:
811
test:
912
runs-on: ubuntu-latest
@@ -13,6 +16,12 @@ jobs:
1316
python-version: ["3.11", "3.12", "3.13", "3.14"]
1417
steps:
1518
- uses: actions/checkout@v7
19+
- name: Checkout OpenStatSpec specification fixtures
20+
uses: actions/checkout@v7
21+
with:
22+
repository: OpenStatSpec/specification
23+
ref: b2c9283eb3bf7103bff324731922358672974aee
24+
path: openstatspec-specification
1625
- uses: actions/setup-python@v7
1726
with:
1827
python-version: ${{ matrix.python-version }}
@@ -25,6 +34,12 @@ jobs:
2534
runs-on: ubuntu-latest
2635
steps:
2736
- uses: actions/checkout@v7
37+
- name: Checkout OpenStatSpec specification fixtures
38+
uses: actions/checkout@v7
39+
with:
40+
repository: OpenStatSpec/specification
41+
ref: b2c9283eb3bf7103bff324731922358672974aee
42+
path: openstatspec-specification
2843
- uses: actions/setup-python@v7
2944
with:
3045
python-version: "3.12"
@@ -78,6 +93,12 @@ jobs:
7893
OPENSTATSPEC_MARIADB_URL: mysql+pymysql://openstatspec:openstatspec@localhost:3307/openstatspec
7994
steps:
8095
- uses: actions/checkout@v7
96+
- name: Checkout OpenStatSpec specification fixtures
97+
uses: actions/checkout@v7
98+
with:
99+
repository: OpenStatSpec/specification
100+
ref: b2c9283eb3bf7103bff324731922358672974aee
101+
path: openstatspec-specification
81102
- uses: actions/setup-python@v7
82103
with:
83104
python-version: "3.12"

THIRD_PARTY_NOTICES.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ SPSS engine is a separate third-party distribution:
55

66
| Component | Version | Applicable terms |
77
| --- | --- | --- |
8-
| pyspssio | TonisOrmisson/pyspssio pinned commit 6a0f9fa | MIT for the Python wrapper, except for its IBM I/O Module files |
8+
| pyspssio | TonisOrmisson/pyspssio pinned commit 9c07a02 | MIT for the Python wrapper, except for its IBM I/O Module files |
99
| IBM I/O Modules for IBM SPSS Statistics Data Files | bundled by the pinned pyspssio fork | IBM International License Agreement for Non-Warranted Programs and the accompanying License Information / REDIST files |
1010

1111
pyspssio ships the IBM licence, redistribution list, and third-party notices

src/openstatspec/spss/sav.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ def engine_identity() -> dict[str, str]:
5252
return {
5353
"package": "pyspssio",
5454
"distribution": "TonisOrmisson/pyspssio",
55-
"pinned_commit": "6a0f9fa",
55+
"pinned_commit": "9c07a02",
5656
"installed_version": str(pyspssio.__version__),
5757
}
5858

tests/test_cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def test_cli_import_inspect_validate_and_export_emit_json(tmp_path, capsys) -> N
1717
assert openstatspec.cli.main(["inspect", str(source)]) == 0
1818
inspected = json.loads(capsys.readouterr().out)
1919
assert inspected["source_format"] == "SAV"
20-
assert inspected["engine"]["pinned_commit"] == "6a0f9fa"
20+
assert inspected["engine"]["pinned_commit"] == "9c07a02"
2121
assert inspected["source_sha256"]
2222
assert inspected["loss_report"] == []
2323
assert imported["case_count"] == 1
@@ -37,7 +37,7 @@ def test_cli_import_inspect_validate_and_export_emit_json(tmp_path, capsys) -> N
3737

3838
def test_capability_matrix_is_public_and_cli_matches_engine_boundary(capsys) -> None:
3939
matrix = openstatspec.capability_matrix()
40-
assert matrix["engine"]["pinned_commit"] == "6a0f9fa"
40+
assert matrix["engine"]["pinned_commit"] == "9c07a02"
4141

4242
assert matrix["spss"] == {
4343
"values": "supported",

tests/test_loss_reports.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ def test_persisted_import_fidelity_events_require_consent_after_reopen(tmp_path)
2828
connection = sqlite3.connect(database_path)
2929
assert {row[0] for row in connection.execute("select code from fidelity_event_catalog")} == set(_REQUIRED_ENGINE_LOSS)
3030
import_details = json.loads(connection.execute("select details from operation_catalog order by created_at limit 1").fetchone()[0])
31-
assert import_details["engine"]["pinned_commit"] == "6a0f9fa"
31+
assert import_details["engine"]["pinned_commit"] == "9c07a02"
3232

3333
openstatspec.export_sav(database_url=database, dataset_id="persisted", destination=blocked)
3434
assert blocked.exists()

tests/test_official_conformance.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
"""Official OpenStatSpec SPSS SAV/ZSAV 1.0 manifest conformance."""
2+
3+
import json
4+
import os
5+
from pathlib import Path
6+
from uuid import uuid4
7+
8+
import pytest
9+
10+
import openstatspec
11+
from conformance import compare_sav_semantics
12+
13+
14+
def _specification_root() -> Path:
15+
configured = os.environ.get("OPENSTATSPEC_SPECIFICATION_DIR")
16+
candidates = [
17+
Path(configured) if configured else None,
18+
Path(__file__).resolve().parents[2] / "specification",
19+
]
20+
for candidate in candidates:
21+
if candidate and (candidate / "conformance/spss-sav-zsav-1.0.json").is_file():
22+
return candidate
23+
raise RuntimeError(
24+
"The official OpenStatSpec specification checkout is required; "
25+
"set OPENSTATSPEC_SPECIFICATION_DIR."
26+
)
27+
28+
29+
def _official_fixtures() -> list[tuple[str, Path]]:
30+
root = _specification_root()
31+
manifest = json.loads(
32+
(root / "conformance/spss-sav-zsav-1.0.json").read_text(encoding="utf-8")
33+
)
34+
assert manifest["manifest_version"] == "1.0"
35+
fixtures = []
36+
for fixture in manifest["fixtures"]:
37+
if fixture["id"] == "preflight-failure":
38+
continue
39+
source = root / "conformance" / fixture["source"]
40+
assert source.is_file(), f"Missing official fixture: {fixture['id']}"
41+
fixtures.append((fixture["id"], source))
42+
return fixtures
43+
44+
45+
def _assert_round_trip(
46+
*, fixture_id: str, source: Path, database_url: str, tmp_path: Path, profile: str,
47+
) -> None:
48+
token = uuid4().hex[:10]
49+
dataset_id = f"official_{profile}_{fixture_id}_{token}".replace("-", "_")
50+
destination = tmp_path / f"{dataset_id}{source.suffix}"
51+
52+
imported = openstatspec.import_sav(
53+
source, database_url=database_url, dataset_id=dataset_id,
54+
)
55+
assert imported.diagnostics == ()
56+
assert openstatspec.validate(
57+
database_url=database_url, dataset_id=dataset_id,
58+
)["valid"] is True
59+
60+
exported = openstatspec.export_sav(
61+
database_url=database_url, dataset_id=dataset_id, destination=destination,
62+
)
63+
assert exported.diagnostics == ()
64+
assert compare_sav_semantics(source, destination) == {
65+
"equivalent": True,
66+
"differences": [],
67+
}
68+
69+
70+
@pytest.mark.parametrize(("fixture_id", "source"), _official_fixtures())
71+
def test_official_manifest_round_trips_through_sqlite(
72+
fixture_id: str, source: Path, tmp_path: Path,
73+
) -> None:
74+
_assert_round_trip(
75+
fixture_id=fixture_id,
76+
source=source,
77+
database_url=f"sqlite:///{tmp_path / 'official.sqlite'}",
78+
tmp_path=tmp_path,
79+
profile="sqlite",
80+
)
81+
82+
83+
@pytest.mark.services
84+
@pytest.mark.parametrize(
85+
("environment_name", "profile"),
86+
[
87+
("OPENSTATSPEC_POSTGRES_URL", "postgresql"),
88+
("OPENSTATSPEC_MYSQL_URL", "mysql"),
89+
("OPENSTATSPEC_MARIADB_URL", "mariadb"),
90+
],
91+
)
92+
@pytest.mark.parametrize(("fixture_id", "source"), _official_fixtures())
93+
def test_official_manifest_round_trips_through_server_profiles(
94+
environment_name: str, profile: str, fixture_id: str, source: Path, tmp_path: Path,
95+
) -> None:
96+
database_url = os.environ.get(environment_name)
97+
if not database_url:
98+
pytest.skip(f"{environment_name} is not configured")
99+
_assert_round_trip(
100+
fixture_id=fixture_id,
101+
source=source,
102+
database_url=database_url,
103+
tmp_path=tmp_path,
104+
profile=profile,
105+
)

0 commit comments

Comments
 (0)