diff --git a/.github/workflows/version-compat.yaml b/.github/workflows/version-compat.yaml new file mode 100644 index 000000000..38f230b9d --- /dev/null +++ b/.github/workflows/version-compat.yaml @@ -0,0 +1,81 @@ +name: Version Compatibility Tests + +on: + push: + branches: + - main + pull_request: + types: [opened, reopened, synchronize, labeled] + workflow_dispatch: + schedule: + # Run daily at 6am UTC to catch any issues with nightly wheels + - cron: '0 6 * * *' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +env: + PYTHON_VERSION: "3.12" + +defaults: + run: + working-directory: ./version-compat-tests + +jobs: + version-compat: + runs-on: ubuntu-latest + name: Test v1/v2 format compatibility + + steps: + - uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + with: + python-version: ${{ env.PYTHON_VERSION }} + # Don't cache - we want fresh nightly wheels each run + enable-cache: false + + - name: Start wheel-rename proxy server + run: | + uvx --from "wheel-rename[server] @ git+https://github.com/earth-mover/rename-wheel" \ + wheel-rename serve \ + -u https://pypi.anaconda.org/scientific-python-nightly-wheels/simple \ + -r "icechunk=icechunk_v1:<2" \ + --port 8123 & + echo $! > /tmp/proxy.pid + + # Wait for server to start + for _ in {1..30}; do + curl -s http://127.0.0.1:8123/simple/ > /dev/null 2>&1 && break + sleep 1 + done + + - name: Install dependencies + run: uv sync + + - name: Verify both versions are installed + run: | + uv run python -c " + import icechunk + import icechunk_v1 + print(f'icechunk v2: {icechunk.__version__}') + print(f'icechunk v1: {icechunk_v1.__version__}') + assert 'icechunk_v1' in icechunk_v1.__file__ + assert icechunk_v1 is not icechunk + " + + - name: Run version compatibility tests + run: uv run pytest -v --tb=short + + - name: Stop proxy server + if: always() + run: | + if [ -f /tmp/proxy.pid ]; then + kill "$(cat /tmp/proxy.pid)" 2>/dev/null || true + rm /tmp/proxy.pid + fi diff --git a/icechunk-python/tests/test_stateful_repo_ops.py b/icechunk-python/tests/test_stateful_repo_ops.py index 159a464e2..b22210d4d 100644 --- a/icechunk-python/tests/test_stateful_repo_ops.py +++ b/icechunk-python/tests/test_stateful_repo_ops.py @@ -8,11 +8,12 @@ from collections.abc import Iterator from dataclasses import dataclass from functools import partial -from typing import Any, Self, cast +from typing import Any, Literal, Self, cast import numpy as np import pytest +import icechunk from zarr.core.buffer import Buffer, default_buffer_prototype pytest.importorskip("hypothesis") @@ -120,6 +121,7 @@ def __init__(self, **kwargs: Any) -> None: # a tag once created, can never be recreated even after expiration self.created_tags: set[str] = set() + self.spec_version: Literal[1, 2] | None = None def __repr__(self) -> str: return textwrap.dedent(f""" @@ -301,14 +303,12 @@ def __init__(self) -> None: note("----------") self.model = Model() self.commit_times: list[datetime.datetime] = [] + self.storage = None @initialize(data=st.data(), target=branches) def initialize(self, data: st.DataObject) -> str: - # FIXME: currently this test is IC2 only - spec_version = data.draw( - st.one_of(st.integers(min_value=2, max_value=2), st.none()) - ) - self.repo = Repository.create(in_memory_storage(), spec_version=spec_version) + self.storage = in_memory_storage() + self.repo = Repository.create(self.storage, spec_version=1) self.session = self.repo.writable_session(DEFAULT_BRANCH) snap = next(iter(self.repo.ancestry(branch=DEFAULT_BRANCH))) @@ -321,6 +321,7 @@ def initialize(self, data: st.DataObject) -> str: self.model.HEAD = HEAD self.model.create_branch(DEFAULT_BRANCH, HEAD) self.model.checkout_branch(DEFAULT_BRANCH) + self.model.spec_version = 1 # initialize with some data always # TODO: always setting array metadata, since we cannot overwrite an existing group's zarr.json @@ -348,6 +349,31 @@ def set_doc(self, path: str, value: Buffer) -> None: with pytest.raises(IcechunkError, match="read-only store"): self.sync_store.set(path, value) + @rule() + @precondition(lambda self: len(self.model.commits) > 5) + def upgrade_spec_version(self): + icechunk.upgrade_icechunk_repository(self.repo) + self.model.spec_version = 2 + + @rule() + def reopen_repository(self) -> None: + """Reopen the repository from storage to get fresh state. + + This discards any uncommitted changes. + """ + assert self.storage is not None + self.repo = Repository.open(self.storage) + note(f"Reopened repository (spec_version={self.repo.spec_version})") + + # Reopening discards uncommitted changes - reset model to last committed state + branch = ( + self.model.branch + if self.model.branch in self.model.branches + else DEFAULT_BRANCH + ) + self.session = self.repo.writable_session(branch) + self.model.checkout_branch(branch) + @rule(message=st.text(max_size=MAX_TEXT_SIZE), target=commits) @precondition(lambda self: self.model.changes_made) def commit(self, message: str) -> str: @@ -362,6 +388,63 @@ def commit(self, message: str) -> str: self.commit_times.append(snapinfo.written_at) return commit_id + @rule(message=st.text(max_size=MAX_TEXT_SIZE), target=commits) + @precondition(lambda self: self.model.changes_made) + def amend_commit(self, message: str) -> str: + """Amend the last commit. + + Amend requires spec_version >= 2. For spec_version=1, it raises an error. + """ + branch = self.session.branch + assert branch is not None + note( + f"Amending commit on branch {branch!r} (spec_version={self.model.spec_version})" + ) + + # Amend is only supported on spec_version >= 2 + if self.model.spec_version == 1: + with pytest.raises( + IcechunkError, + match="repository version error.*requires.*version 2", + ): + self.session.amend(message) + note("Amend correctly rejected for spec_version=1") + # Return existing HEAD since amend failed + return self.model.branches[branch] + + # spec_version >= 2: amend should succeed + commit_id = self.session.amend(message) + snapinfo = next(iter(self.repo.ancestry(branch=branch))) + assert snapinfo.id == commit_id + self.session = self.repo.writable_session(branch) + note(f"Amended commit: {snapinfo!r}") + + # For model: amend replaces the previous HEAD commit on this branch + old_head = self.model.branches[branch] + old_commit = self.model.commits.get(old_head) + parent_id = old_commit.parent_id if old_commit else None + + # Only remove old commit from model if no other branch references it + other_refs = [ + b for b, c in self.model.branches.items() if c == old_head and b != branch + ] + if not other_refs and old_head in self.model.commits: + del self.model.commits[old_head] + # Update commit times - remove old + if old_commit and old_commit.written_at in self.commit_times: + self.commit_times.remove(old_commit.written_at) + + # Add new commit + self.model.commits[commit_id] = CommitModel.from_snapshot_and_store( + snapinfo, copy.deepcopy(self.model.store) + ) + self.model.commits[commit_id].parent_id = parent_id + self.model.branches[branch] = commit_id + self.model.HEAD = commit_id + self.model.changes_made = False + self.commit_times.append(snapinfo.written_at) + return commit_id + @rule(ref=commits) def checkout_commit(self, ref: str) -> None: if ref not in self.model.commits: @@ -503,7 +586,9 @@ def delete_branch(self, branch: str) -> None: with pytest.raises(IcechunkError): self.repo.delete_branch(branch) - @precondition(lambda self: bool(self.commit_times)) + # TODO: v1 has bugs in expire_snapshots, only test for v2 + # https://github.com/earth-mover/icechunk/issues/1520 + @precondition(lambda self: bool(self.commit_times) and self.model.spec_version >= 2) @rule( data=st.data(), delta=st.timedeltas( @@ -524,12 +609,24 @@ def expire_snapshots( note( f"Expiring snapshots {older_than=!r}, ({commit_time=!r}, {delta=!r}), {delete_expired_branches=!r}, {delete_expired_tags=!r}" ) + + # Track branches and tags before expiration + branches_before = set(self.repo.list_branches()) + tags_before = set(self.repo.list_tags()) + actual = self.repo.expire_snapshots( older_than, delete_expired_branches=delete_expired_branches, delete_expired_tags=delete_expired_tags, ) note(f"repo expired snaps={actual!r}") + + # Track branches and tags after expiration + branches_after = set(self.repo.list_branches()) + tags_after = set(self.repo.list_tags()) + actual_deleted_branches = branches_before - branches_after + actual_deleted_tags = tags_before - tags_after + expected = self.model.expire_snapshots( older_than, delete_expired_branches=delete_expired_branches, @@ -537,10 +634,19 @@ def expire_snapshots( ) note(f"from model: {expected}") note(f"actual: {actual}") + note(f"actual_deleted_branches: {actual_deleted_branches}") + note(f"actual_deleted_tags: {actual_deleted_tags}") + assert self.initial_snapshot.id not in actual assert actual == expected.expired_snapshots, (actual, expected) - - for branch in expected.deleted_branches: + assert ( + actual_deleted_branches == expected.deleted_branches + ), f"deleted branches mismatch: actual={actual_deleted_branches}, expected={expected.deleted_branches}" + assert ( + actual_deleted_tags == expected.deleted_tags + ), f"deleted tags mismatch: actual={actual_deleted_tags}, expected={expected.deleted_tags}" + + for branch in actual_deleted_branches: self.maybe_checkout_branch(branch) @precondition(lambda self: bool(self.commit_times)) diff --git a/version-compat-tests/.gitignore b/version-compat-tests/.gitignore new file mode 100644 index 000000000..e01177842 --- /dev/null +++ b/version-compat-tests/.gitignore @@ -0,0 +1,17 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so + +# Virtual environment +.venv/ + +# pytest +.pytest_cache/ + +# Downloaded wheels +wheels/ + +# uv +uv.lock diff --git a/version-compat-tests/README.md b/version-compat-tests/README.md new file mode 100644 index 000000000..d055a30b4 --- /dev/null +++ b/version-compat-tests/README.md @@ -0,0 +1,102 @@ +# Icechunk Version Compatibility Tests + +This directory contains regression tests to ensure compatibility between icechunk v1 and v2 formats. + +## Purpose + +These tests verify format compatibility between icechunk v1 and v2 libraries, including: + +- Cross-version read/write compatibility +- Format upgrades and migrations +- Graceful error handling when versions are incompatible + +## Setup + +### Prerequisites + +- Python 3.11+ +- [uv](https://github.com/astral-sh/uv) package manager + +### Option 1: Using the Proxy Server (Recommended) + +This approach uses `wheel-rename serve` to run a local proxy that renames packages on the fly. + +**Terminal 1 - Start the proxy server:** +```bash +./scripts/start_proxy.sh +``` + +**Terminal 2 - Install and run tests:** +```bash +uv sync +uv run pytest -v +``` + +### Option 2: Manual Wheel Download + +This approach downloads and renames wheels manually. + +```bash +# Download and rename wheels +./scripts/setup_wheels.sh + +# Create venv and install +uv venv +uv pip install ./wheels/icechunk_v1-*.whl +uv pip install ./wheels/icechunk-2*.whl +uv pip install zarr pytest numpy + +# Run tests +uv run pytest -v +``` + +## How It Works + +The tests use the [wheel-rename](https://github.com/earth-mover/rename-wheel) tool to install both icechunk v1 and v2 in the same Python environment: + +- `icechunk` - The v2 library (latest) +- `icechunk_v1` - The v1 library (renamed from icechunk <2.0) + +This allows the tests to import both versions and verify cross-version compatibility: + +```python +import icechunk # v2 library +import icechunk_v1 # v1 library (renamed) +``` + +## pyproject.toml Configuration + +The `pyproject.toml` is configured to use multiple package indexes: + +```toml +[tool.uv] +extra-index-url = [ + "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple", # v2 + "http://127.0.0.1:8123/simple/", # proxy for renamed v1 +] +prerelease = "allow" +index-strategy = "unsafe-best-match" +``` + +## Troubleshooting + +### Proxy server not running + +If `uv sync` fails with connection errors to `127.0.0.1:8123`, make sure the proxy server is running: +```bash +./scripts/start_proxy.sh +``` + +### Wheel download fails + +If the manual wheel download fails: +1. Check network connectivity to `pypi.anaconda.org` +2. Verify nightly wheels are available for your platform +3. Try: `uvx --from "git+https://github.com/earth-mover/rename-wheel" wheel-rename --help` + +### Import errors + +If you get import errors for `icechunk` or `icechunk_v1`: +1. Verify both packages are installed: `uv pip list | grep icechunk` +2. Re-run the installation steps +3. Test imports: `uv run python -c "import icechunk; import icechunk_v1"` diff --git a/version-compat-tests/pyproject.toml b/version-compat-tests/pyproject.toml new file mode 100644 index 000000000..437ecd6f2 --- /dev/null +++ b/version-compat-tests/pyproject.toml @@ -0,0 +1,36 @@ +[project] +name = "icechunk-version-compat-tests" +version = "0.1.0" +description = "Version compatibility tests for icechunk v1/v2 format" +requires-python = ">=3.11" +dependencies = [ + "icechunk>=2.0.0.dev0", # v2 from nightly + "icechunk_v1", # v1 renamed, from proxy server + "zarr>=3", + "pytest>=8", + "numpy", +] + +[tool.pytest.ini_options] +testpaths = ["tests"] + +[tool.hatch.build.targets.wheel] +# This is a test-only project, include just the tests directory +packages = ["tests"] + +[tool.uv] +# PyPI for most deps, nightly for icechunk v2, proxy for icechunk_v1 +extra-index-url = [ + "https://pypi.anaconda.org/scientific-python-nightly-wheels/simple", + "http://127.0.0.1:8123/simple/", +] +# Allow pre-release versions +prerelease = "allow" +# Consider all versions from all indexes (needed for nightly builds) +index-strategy = "unsafe-best-match" +# Use highest available versions +resolution = "highest" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/version-compat-tests/scripts/setup_wheels.sh b/version-compat-tests/scripts/setup_wheels.sh new file mode 100755 index 000000000..d41a8d35c --- /dev/null +++ b/version-compat-tests/scripts/setup_wheels.sh @@ -0,0 +1,70 @@ +#!/bin/bash +set -e + +# Version Compatibility Test Wheel Setup +# This script downloads icechunk v1 and v2 wheels and renames v1 to icechunk_v1 +# so both versions can be installed in the same environment. +# +# Alternative: Use the proxy-based approach with start_proxy.sh and uv sync + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +WHEELS_DIR="$PROJECT_DIR/wheels" +INDEX_URL="https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" + +# wheel-rename is not on PyPI yet, install from GitHub +WHEEL_RENAME_PKG="git+https://github.com/earth-mover/rename-wheel" + +echo "=== Icechunk Version Compatibility Test Setup ===" +echo "Wheels directory: $WHEELS_DIR" +echo "Index URL: $INDEX_URL" +echo "" + +# Clean up old wheels +rm -rf "$WHEELS_DIR" +mkdir -p "$WHEELS_DIR" + +# Download v1 wheel (version <2) +echo "=== Step 1: Downloading icechunk v1 (version <2) ===" +uvx --from "$WHEEL_RENAME_PKG" wheel-rename download icechunk --version "<2" \ + -i "$INDEX_URL" \ + -o "$WHEELS_DIR" + +# Find the v1 wheel file +V1_WHEEL=$(ls "$WHEELS_DIR"/icechunk-1*.whl 2>/dev/null | head -n 1) +if [ -z "$V1_WHEEL" ]; then + echo "ERROR: Failed to download v1 wheel" + exit 1 +fi +echo "Downloaded: $V1_WHEEL" + +# Rename v1 wheel to icechunk_v1 +echo "" +echo "=== Step 2: Renaming v1 wheel to icechunk_v1 ===" +uvx --from "$WHEEL_RENAME_PKG" wheel-rename "$V1_WHEEL" icechunk_v1 -o "$WHEELS_DIR" + +# Remove the original v1 wheel (we only need the renamed one) +rm "$V1_WHEEL" + +# Download v2 wheel +echo "" +echo "=== Step 3: Downloading icechunk v2 (version >=2.0.0.dev0) ===" +uvx --from "$WHEEL_RENAME_PKG" wheel-rename download icechunk --version ">=2.0.0.dev0" \ + -i "$INDEX_URL" \ + -o "$WHEELS_DIR" + +echo "" +echo "=== Setup Complete ===" +echo "Wheels in $WHEELS_DIR:" +ls -la "$WHEELS_DIR" + +echo "" +echo "To install the wheels, run:" +echo " cd $PROJECT_DIR" +echo " uv venv" +echo " uv pip install $WHEELS_DIR/icechunk_v1-*.whl" +echo " uv pip install $WHEELS_DIR/icechunk-2*.whl" +echo " uv pip install zarr pytest numpy" +echo "" +echo "Then run tests with:" +echo " uv run pytest" diff --git a/version-compat-tests/scripts/start_proxy.sh b/version-compat-tests/scripts/start_proxy.sh new file mode 100755 index 000000000..167612904 --- /dev/null +++ b/version-compat-tests/scripts/start_proxy.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# Start the wheel-rename proxy server +# This proxy serves icechunk v1 packages renamed to icechunk_v1 + +NIGHTLY_INDEX="https://pypi.anaconda.org/scientific-python-nightly-wheels/simple" +PORT=8123 + +echo "Starting wheel-rename proxy server on port $PORT..." +echo "Upstream index: $NIGHTLY_INDEX" +echo "Renaming: icechunk (<2) -> icechunk_v1" +echo "" +echo "Press Ctrl+C to stop the server" +echo "" + +# wheel-rename is not on PyPI yet, install from GitHub with server extras +uvx --from "wheel-rename[server] @ git+https://github.com/earth-mover/rename-wheel" \ + wheel-rename serve \ + -u "$NIGHTLY_INDEX" \ + -r "icechunk=icechunk_v1:<2" \ + --port $PORT diff --git a/version-compat-tests/tests/__init__.py b/version-compat-tests/tests/__init__.py new file mode 100644 index 000000000..309c1ef1b --- /dev/null +++ b/version-compat-tests/tests/__init__.py @@ -0,0 +1 @@ +# Version compatibility tests for icechunk diff --git a/version-compat-tests/tests/test_format_compatibility.py b/version-compat-tests/tests/test_format_compatibility.py new file mode 100644 index 000000000..8771a2141 --- /dev/null +++ b/version-compat-tests/tests/test_format_compatibility.py @@ -0,0 +1,198 @@ +"""Version compatibility tests for icechunk v1/v2 format. + +These tests verify that: +1. The v2 library can write v1 format repositories (using spec_version=1) +2. The actual v1 library can read repositories written in v1 format by v2 +3. After upgrading to v2 format, v1 library fails gracefully +4. The v2 library can read upgraded repositories +""" + +import tempfile + +import pytest +import zarr + +# TODO: use hypothesis here + + +class TestFormatCompatibility: + """Test format compatibility between icechunk v1 and v2 libraries.""" + + def test_v1_can_read_v1_format_written_by_v2(self) -> None: + """Verify v1 library can read a v1-format repo created by v2 library.""" + import icechunk + import icechunk_v1 + + print(f"icechunk (v2) version: {icechunk.__version__}") + print(f"icechunk_v1 version: {icechunk_v1.__version__}") + + with tempfile.TemporaryDirectory() as temp_dir: + # Create repo with v2 library but using spec_version=1 (v1 format) + storage = icechunk.local_filesystem_storage(temp_dir) + repo = icechunk.Repository.create(storage, spec_version=1) + + session = repo.writable_session("main") + store = session.store + group = zarr.group(store) + array = group.create("my_array", shape=10, dtype="int32", chunks=(5,)) + array[:] = 1 + session.commit("first commit") + + assert repo.spec_version == 1, "Repository should be v1 format" + + # Verify v1 library can read it + storage_v1 = icechunk_v1.local_filesystem_storage(temp_dir) + repo_v1 = icechunk_v1.Repository.open(storage_v1) + session_v1 = repo_v1.readonly_session("main") + store_v1 = session_v1.store + group_v1 = zarr.open_group(store_v1, mode="r") + array_v1 = group_v1["my_array"] + + # Verify data integrity + assert list(array_v1[:]) == [1] * 10, "v1 should read data correctly" + + # Now have v1 write to the repo + session_v1_write = repo_v1.writable_session("main") + store_v1_write = session_v1_write.store + array_v1_write = zarr.open_array(store_v1_write, path="my_array", mode="a") + array_v1_write[:] = 2 + session_v1_write.commit("v1 commit") + + # Verify v2 can still read the data written by v1 + storage_v2_read = icechunk.local_filesystem_storage(temp_dir) + repo_v2_read = icechunk.Repository.open(storage_v2_read) + session_v2_read = repo_v2_read.readonly_session("main") + store_v2_read = session_v2_read.store + group_v2_read = zarr.open_group(store_v2_read, mode="r") + array_v2_read = group_v2_read["my_array"] + + # Verify v2 can read what v1 wrote + assert ( + list(array_v2_read[:]) == [2] * 10 + ), "v2 should read data written by v1" + + def test_v1_cannot_read_after_upgrade_to_v2(self) -> None: + """Verify v1 library fails gracefully after repo is upgraded to v2 format.""" + import icechunk + import icechunk_v1 + + with tempfile.TemporaryDirectory() as temp_dir: + # Create repo with v2 library using spec_version=1 + storage = icechunk.local_filesystem_storage(temp_dir) + repo = icechunk.Repository.create(storage, spec_version=1) + + session = repo.writable_session("main") + store = session.store + group = zarr.group(store) + array = group.create("my_array", shape=10, dtype="int32", chunks=(5,)) + array[:] = 42 + session.commit("initial commit") + + # Upgrade to v2 format + storage2 = icechunk.local_filesystem_storage(temp_dir) + repo2 = icechunk.Repository.open(storage2) + icechunk.upgrade_icechunk_repository( + repo2, dry_run=False, delete_unused_v1_files=True + ) + + # Verify repo is now v2 + storage3 = icechunk.local_filesystem_storage(temp_dir) + repo3 = icechunk.Repository.open(storage3) + assert repo3.spec_version == 2, "Repository should be upgraded to v2" + + # Verify v1 library CANNOT read the upgraded repo + storage_v1 = icechunk_v1.local_filesystem_storage(temp_dir) + with pytest.raises(Exception): + # v1 should fail to open a v2 format repo + icechunk_v1.Repository.open(storage_v1) + + def test_v2_can_read_after_upgrade(self) -> None: + """Verify v2 library can still read data after upgrading from v1 to v2 format.""" + import icechunk + + with tempfile.TemporaryDirectory() as temp_dir: + # Create repo with v2 library using spec_version=1 + storage = icechunk.local_filesystem_storage(temp_dir) + repo = icechunk.Repository.create(storage, spec_version=1) + + session = repo.writable_session("main") + store = session.store + group = zarr.group(store) + array = group.create("my_array", shape=10, dtype="int32", chunks=(5,)) + array[:] = 99 + session.commit("initial commit") + + # Upgrade to v2 format + storage2 = icechunk.local_filesystem_storage(temp_dir) + repo2 = icechunk.Repository.open(storage2) + icechunk.upgrade_icechunk_repository( + repo2, dry_run=False, delete_unused_v1_files=True + ) + + # Verify v2 can still read the data + storage3 = icechunk.local_filesystem_storage(temp_dir) + repo3 = icechunk.Repository.open(storage3) + assert repo3.spec_version == 2 + + session3 = repo3.readonly_session("main") + store3 = session3.store + group3 = zarr.open_group(store3, mode="r") + array3 = group3["my_array"] + + # Verify data integrity after upgrade + assert list(array3[:]) == [99] * 10, "v2 should read data after upgrade" + + def test_v2_can_write_and_read_v1_format(self) -> None: + """Verify v2 library can write to and read from a v1 format repo.""" + import icechunk + + with tempfile.TemporaryDirectory() as temp_dir: + # Create v1 format repo + storage = icechunk.local_filesystem_storage(temp_dir) + repo = icechunk.Repository.create(storage, spec_version=1) + + # First commit + session = repo.writable_session("main") + store = session.store + group = zarr.group(store) + array = group.create("data", shape=(5, 5), dtype="float64", chunks=(2, 2)) + array[:] = 1.5 + session.commit("first commit") + + # Second commit - modify data + session2 = repo.writable_session("main") + store2 = session2.store + array2 = zarr.open_array(store2, path="data", mode="a") + array2[0, 0] = 100.0 + session2.commit("second commit") + + # Verify we can read back correctly + session3 = repo.readonly_session("main") + store3 = session3.store + array3 = zarr.open_array(store3, path="data", mode="r") + + assert array3[0, 0] == 100.0 + assert array3[1, 1] == 1.5 + + # Verify it's still v1 format + assert repo.spec_version == 1 + + def test_version_info(self) -> None: + """Print version info for debugging.""" + import icechunk + import icechunk_v1 + + print(f"\nicechunk (v2) version: {icechunk.__version__}") + print(f"icechunk_v1 version: {icechunk_v1.__version__}") + + # Verify versions are as expected + v2_version = icechunk.__version__ + v1_version = icechunk_v1.__version__ + + # v2 should be >= 2.0.0 + assert ( + v2_version.startswith("2.") or "2.0.0" in v2_version or "dev" in v2_version + ) + + # v1 should be < 2.0.0 + assert v1_version.startswith("1.") or v1_version.startswith("0.")