Skip to content

Commit cdf85ae

Browse files
test: replace MinIO with SeaweedFS for object-storage testing (#1647)
* test: run object-storage tests against SeaweedFS instead of MinIO MinIO withdrew its public images and archived its source. SeaweedFS 4.48 matches MinIO on s3fs directory emulation, which GC relies on, and passes the object-storage suites from a cold pull. The image is overridable through DJ_TEST_S3_IMAGE. Refs #1560 * test: finish moving the dev and test setup from MinIO to SeaweedFS - docker-compose: replace the minio service with s3 (SeaweedFS 4.48, the image the fixture uses); the old minio/minio image no longer pulls - rename the requires_minio marker to requires_s3 and the minio_client fixture to s3_client - default S3_ENDPOINT for external containers is localhost:8333 - drop the unused minio extra from testcontainers; relock pixi - update CONTRIBUTING.md Refs #1560
1 parent 15610e5 commit cdf85ae

8 files changed

Lines changed: 90 additions & 257 deletions

File tree

‎CONTRIBUTING.md‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ pytest tests/
3636

3737
## Running Tests
3838

39-
Tests use [testcontainers](https://testcontainers.com/) to automatically manage MySQL, PostgreSQL, and MinIO containers. No manual `docker-compose up` required.
39+
Tests use [testcontainers](https://testcontainers.com/) to automatically manage MySQL, PostgreSQL, and S3 ([SeaweedFS](https://github.com/seaweedfs/seaweedfs)) containers. No manual `docker-compose up` required.
4040

4141
Integration tests are **backend-parameterized** — tests using the `backend` fixture run automatically against both MySQL and PostgreSQL.
4242

@@ -67,13 +67,13 @@ Tests automatically spin up both MySQL and PostgreSQL containers via testcontain
6767
### External Containers (for debugging)
6868

6969
```bash
70-
# MySQL + MinIO
71-
docker compose up -d db minio
70+
# MySQL + S3
71+
docker compose up -d db s3
7272
DJ_USE_EXTERNAL_CONTAINERS=1 pixi run -e test test
7373
docker compose down
7474

75-
# MySQL + PostgreSQL + MinIO
76-
docker compose up -d db postgres minio
75+
# MySQL + PostgreSQL + S3
76+
docker compose up -d db postgres s3
7777
DJ_USE_EXTERNAL_CONTAINERS=1 pixi run -e test test
7878
docker compose down
7979
```
@@ -133,7 +133,7 @@ For `DJ_USE_EXTERNAL_CONTAINERS=1`:
133133

134134
| Variable | Default | Description |
135135
|----------|---------|-------------|
136-
| `S3_ENDPOINT` | `localhost:9000` | MinIO endpoint |
136+
| `S3_ENDPOINT` | `localhost:8333` | S3 endpoint |
137137

138138
---
139139

‎docker-compose.yaml‎

Lines changed: 12 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
# Development environment with MySQL and MinIO services
1+
# Development environment with MySQL, PostgreSQL, and S3 (SeaweedFS) services
22
#
33
# NOTE: docker-compose is OPTIONAL for running tests.
44
# Tests use testcontainers to automatically manage containers.
55
# Just run: pytest tests/
66
#
77
# Use docker-compose for development/debugging when you want
88
# persistent containers that survive test runs:
9-
# docker compose up -d db minio # Start services manually
9+
# docker compose up -d db s3 # Start services manually
1010
# pytest tests/ # Tests will use these containers
1111
#
1212
# Full Docker testing (CI):
@@ -37,20 +37,17 @@ services:
3737
timeout: 30s
3838
retries: 5
3939
interval: 15s
40-
minio:
41-
image: minio/minio:${MINIO_VER:-RELEASE.2025-02-28T09-55-16Z}
40+
s3:
41+
# Same image as the test fixture in tests/conftest.py
42+
image: chrislusf/seaweedfs:${SEAWEEDFS_VER:-4.48}
4243
environment:
43-
- MINIO_ACCESS_KEY=datajoint
44-
- MINIO_SECRET_KEY=datajoint
44+
- AWS_ACCESS_KEY_ID=datajoint
45+
- AWS_SECRET_ACCESS_KEY=datajoint
4546
ports:
46-
- "9000:9000"
47-
command: server --address ":9000" /data
47+
- "8333:8333"
48+
command: server -s3 -ip.bind=0.0.0.0
4849
healthcheck:
49-
test:
50-
- "CMD"
51-
- "curl"
52-
- "--fail"
53-
- "http://localhost:9000/minio/health/live"
50+
test: [ "CMD", "nc", "-z", "localhost", "8333" ]
5451
timeout: 30s
5552
retries: 5
5653
interval: 15s
@@ -67,7 +64,7 @@ services:
6764
condition: service_healthy
6865
postgres:
6966
condition: service_healthy
70-
minio:
67+
s3:
7168
condition: service_healthy
7269
environment:
7370
- DJ_HOST=db
@@ -80,7 +77,7 @@ services:
8077
- DJ_PG_USER=postgres
8178
- DJ_PG_PASS=password
8279
- DJ_PG_PORT=5432
83-
- S3_ENDPOINT=minio:9000
80+
- S3_ENDPOINT=s3:8333
8481
- S3_ACCESS_KEY=datajoint
8582
- S3_SECRET_KEY=datajoint
8683
- S3_BUCKET=datajoint.test

‎pixi.lock‎

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

‎pyproject.toml‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ test = [
9696
"matplotlib",
9797
"ipython",
9898
"graphviz",
99-
"testcontainers[mysql,minio,postgres]>=4.0",
99+
"testcontainers[mysql,postgres]>=4.0",
100100
"polars>=0.20.0",
101101
"pyarrow>=14.0.0",
102102
]
@@ -117,7 +117,7 @@ test = [
117117
"matplotlib",
118118
"ipython",
119119
"s3fs>=2023.1.0",
120-
"testcontainers[mysql,minio,postgres]>=4.0",
120+
"testcontainers[mysql,postgres]>=4.0",
121121
"psycopg2-binary>=2.9.0",
122122
"polars>=0.20.0",
123123
"pyarrow>=14.0.0",
@@ -245,7 +245,7 @@ ignore-words-list = "rever,numer,astroid"
245245
[tool.pytest.ini_options]
246246
markers = [
247247
"requires_mysql: marks tests as requiring MySQL database (deselect with '-m \"not requires_mysql\"')",
248-
"requires_minio: marks tests as requiring MinIO object storage (deselect with '-m \"not requires_minio\"')",
248+
"requires_s3: marks tests as requiring S3-compatible object storage (deselect with '-m \"not requires_s3\"')",
249249
"mysql: marks tests that run on MySQL backend (select with '-m mysql')",
250250
"postgresql: marks tests that run on PostgreSQL backend (select with '-m postgresql')",
251251
"backend_agnostic: marks tests that should pass on all backends (auto-marked for parameterized tests)",
@@ -293,7 +293,7 @@ test-py315 = { features = ["test", "py315"] }
293293
test = "pytest tests/"
294294
test-cov = "pytest --cov-report term-missing --cov=datajoint tests/"
295295
# Optional: use external containers (docker-compose) instead of testcontainers
296-
services-up = "docker compose up -d db minio"
296+
services-up = "docker compose up -d db s3"
297297
services-down = "docker compose down"
298298
test-external = { cmd = "DJ_USE_EXTERNAL_CONTAINERS=1 pytest tests/", depends-on = ["services-up"] }
299299

‎tests/conftest.py‎

Lines changed: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
44
Tests are organized by their dependencies:
55
- Unit tests: No external dependencies, run with `pytest -m "not requires_mysql"`
6-
- Integration tests: Require MySQL/MinIO, marked with @pytest.mark.requires_mysql
6+
- Integration tests: Require MySQL/S3, marked with @pytest.mark.requires_mysql
77
88
Containers are automatically started via testcontainers when needed.
99
Just run: pytest tests/
@@ -58,9 +58,9 @@ def pytest_collection_modifyitems(config, items):
5858
"db_creds_root",
5959
"db_creds_test",
6060
}
61-
# Tests that use these fixtures require MinIO
62-
minio_fixtures = {
63-
"minio_client",
61+
# Tests that use these fixtures require S3 object storage
62+
s3_fixtures = {
63+
"s3_client",
6464
"s3fs_client",
6565
"s3_creds",
6666
"stores_config",
@@ -83,8 +83,8 @@ def pytest_collection_modifyitems(config, items):
8383
# Auto-add marks based on fixture usage
8484
if fixturenames & mysql_fixtures:
8585
item.add_marker(pytest.mark.requires_mysql)
86-
if fixturenames & minio_fixtures:
87-
item.add_marker(pytest.mark.requires_minio)
86+
if fixturenames & s3_fixtures:
87+
item.add_marker(pytest.mark.requires_s3)
8888

8989
# Auto-mark backend-parameterized tests
9090
if fixturenames & backend_fixtures:
@@ -95,7 +95,7 @@ def pytest_collection_modifyitems(config, items):
9595

9696

9797
# =============================================================================
98-
# Container Fixtures - Auto-start MySQL and MinIO via testcontainers
98+
# Container Fixtures - Auto-start MySQL, PostgreSQL, and S3 via testcontainers
9999
# =============================================================================
100100

101101
# Check if we should use external containers (for CI or manual docker-compose)
@@ -164,36 +164,50 @@ def postgres_container():
164164

165165

166166
@pytest.fixture(scope="session")
167-
def minio_container():
168-
"""Start MinIO container for the test session (or use external)."""
167+
def s3_container():
168+
"""Start an S3-compatible object store for the test session (or use external)."""
169169
if USE_EXTERNAL_CONTAINERS:
170170
# Use external container - return None, credentials come from env
171-
logger.info("Using external MinIO container")
171+
logger.info("Using external S3 container")
172172
yield None
173173
return
174174

175-
from testcontainers.minio import MinioContainer
176-
177-
# MinIO withdrew its public images: `minio/minio` on Docker Hub is gone
178-
# entirely (any tag, not just `latest`) and `quay.io/minio/minio` now
179-
# requires authentication. Chainguard publishes a maintained build that
180-
# needs no credentials and is drop-in for this fixture. Overridable so a
181-
# deployment can point at its own mirror without editing tests.
182-
container = MinioContainer(
183-
image=os.environ.get("DJ_TEST_MINIO_IMAGE", "cgr.dev/chainguard/minio:latest"),
184-
access_key="datajoint",
185-
secret_key="datajoint",
175+
import time
176+
177+
from testcontainers.core.container import DockerContainer
178+
179+
# SeaweedFS replaces MinIO, which withdrew its public images and archived
180+
# its source (#1560). It matched MinIO on s3fs directory emulation
181+
# (isdir/walk/find over prefixes and zero-byte markers), which GC depends
182+
# on. Overridable so a deployment can point at its own mirror.
183+
container = (
184+
DockerContainer(os.environ.get("DJ_TEST_S3_IMAGE", "chrislusf/seaweedfs:4.48"))
185+
.with_command("server -s3 -ip.bind=0.0.0.0")
186+
.with_env("AWS_ACCESS_KEY_ID", "datajoint")
187+
.with_env("AWS_SECRET_ACCESS_KEY", "datajoint")
188+
.with_exposed_ports(8333)
186189
)
187190
container.start()
188191

189192
host = container.get_container_host_ip()
190-
port = container.get_exposed_port(9000)
191-
logger.info(f"MinIO container started at {host}:{port}")
193+
port = container.get_exposed_port(8333)
194+
# The S3 gateway starts after the master, volume, and filer servers
195+
deadline = time.monotonic() + 60
196+
while True:
197+
try:
198+
urllib3.PoolManager().request("GET", f"http://{host}:{port}/", retries=False, timeout=1)
199+
break
200+
except urllib3.exceptions.HTTPError:
201+
if time.monotonic() > deadline:
202+
container.stop()
203+
raise
204+
time.sleep(0.5)
205+
logger.info(f"S3 container started at {host}:{port}")
192206

193207
yield container
194208

195209
container.stop()
196-
logger.info("MinIO container stopped")
210+
logger.info("S3 container stopped")
197211

198212

199213
# =============================================================================
@@ -253,12 +267,12 @@ def db_creds_test(mysql_container) -> Dict:
253267

254268

255269
@pytest.fixture(scope="session")
256-
def s3_creds(minio_container) -> Dict:
257-
"""S3/MinIO credentials from container or environment."""
258-
if minio_container is not None:
270+
def s3_creds(s3_container) -> Dict:
271+
"""S3 credentials from container or environment."""
272+
if s3_container is not None:
259273
# From testcontainer
260-
host = minio_container.get_container_host_ip()
261-
port = minio_container.get_exposed_port(9000)
274+
host = s3_container.get_container_host_ip()
275+
port = s3_container.get_exposed_port(8333)
262276
return dict(
263277
endpoint=f"{host}:{port}",
264278
access_key="datajoint",
@@ -268,7 +282,7 @@ def s3_creds(minio_container) -> Dict:
268282
else:
269283
# From environment (external container)
270284
return dict(
271-
endpoint=os.environ.get("S3_ENDPOINT", "localhost:9000"),
285+
endpoint=os.environ.get("S3_ENDPOINT", "localhost:8333"),
272286
access_key=os.environ.get("S3_ACCESS_KEY", "datajoint"),
273287
secret_key=os.environ.get("S3_SECRET_KEY", "datajoint"),
274288
bucket=os.environ.get("S3_BUCKET", "datajoint.test"),
@@ -487,7 +501,7 @@ def connection_test(connection_root, prefix, db_creds_test):
487501

488502

489503
# =============================================================================
490-
# S3/MinIO Fixtures
504+
# S3 Fixtures
491505
# =============================================================================
492506

493507

@@ -509,7 +523,7 @@ def stores_config(s3_creds, tmpdir_factory):
509523
bucket=s3_creds.get("bucket", "datajoint-test"),
510524
location="dj/repo",
511525
stage=str(tmpdir_factory.mktemp("repo-s3")),
512-
secure=False, # MinIO runs without SSL in tests
526+
secure=False, # The test S3 server runs without SSL
513527
),
514528
"local": dict(protocol="file", location=str(tmpdir_factory.mktemp("local"))),
515529
"share": dict(
@@ -519,7 +533,7 @@ def stores_config(s3_creds, tmpdir_factory):
519533
secret_key=s3_creds["secret_key"],
520534
bucket=s3_creds.get("bucket", "datajoint-test"),
521535
location="dj/store/repo",
522-
secure=False, # MinIO runs without SSL in tests
536+
secure=False, # The test S3 server runs without SSL
523537
),
524538
}
525539

@@ -566,7 +580,7 @@ def http_client():
566580

567581
@pytest.fixture(scope="session")
568582
def s3fs_client(s3_creds):
569-
"""Initialize s3fs filesystem for MinIO."""
583+
"""Initialize s3fs filesystem for the test S3 server."""
570584
import s3fs
571585

572586
return s3fs.S3FileSystem(
@@ -577,8 +591,8 @@ def s3fs_client(s3_creds):
577591

578592

579593
@pytest.fixture(scope="session")
580-
def minio_client(s3_creds, s3fs_client, teardown=False):
581-
"""S3 filesystem with test bucket created (legacy name for compatibility)."""
594+
def s3_client(s3_creds, s3fs_client, teardown=False):
595+
"""S3 filesystem with the test bucket created."""
582596
bucket = s3_creds["bucket"]
583597

584598
# Create bucket if it doesn't exist

‎tests/integration/test_attach.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from tests.schema_external import Attach
66

77

8-
def test_attach_attributes(schema_ext, minio_client, tmpdir_factory):
8+
def test_attach_attributes(schema_ext, s3_client, tmpdir_factory):
99
"""Test saving files in attachments"""
1010
import datajoint as dj
1111

@@ -46,7 +46,7 @@ def test_attach_attributes(schema_ext, minio_client, tmpdir_factory):
4646
assert p2 == path2[0]
4747

4848

49-
def test_return_string(schema_ext, minio_client, tmpdir_factory):
49+
def test_return_string(schema_ext, s3_client, tmpdir_factory):
5050
"""Test returning string on fetch"""
5151
import datajoint as dj
5252

‎tests/integration/test_codecs.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ def test_codec_graph(schema_codec):
7272
c.delete()
7373

7474

75-
def test_codec_chained(schema_codec, minio_client):
75+
def test_codec_chained(schema_codec, s3_client):
7676
"""Test codec chaining (layout -> blob)."""
7777
c = Connectivity()
7878
c.delete()

0 commit comments

Comments
 (0)