Add S3 storage integration tests with a mock S3 server - #6483
Add S3 storage integration tests with a mock S3 server#6483jantonguirao wants to merge 1 commit into
Conversation
DALI's s3:// support had no test coverage. Add a nose2 module that starts a mock S3 server in the background, seeds a bucket, and asserts that the readers produce byte-identical results to the same data on local disk. The server is moto, run in a subprocess. It has to be a subprocess: Pipeline Build() is bound without py::call_guard<py::gil_scoped_release>, and S3 object listing happens inside Build(), so an in-process server thread is GIL-starved and the request eventually times out in libcurl. The endpoint is always an IP literal. DALI does not set useVirtualAddressing, so aws-sdk-cpp only selects path-style addressing when the endpoint host is an IP; with a host name the bucket is prepended to the host instead, which needs a matching DNS entry (and MINIO_DOMAIN, for a real MinIO). DALI_TEST_S3_ENDPOINT points the same tests at MinIO or at real S3 instead. Covered: fn.readers.file with file_root= and files= (including a key with a space and a zero-byte object), ListObjectsV2 continuation across the 1000-key page limit, fn.readers.webdataset with an inferred and with a local index, and the missing-object and missing-bucket error paths. The tests are wired into the existing TL0_python-self-test-readers-decoders suite, which already runs nose2 over dali/test/python/reader, so only the pip package list needs updating. They skip cleanly when moto is absent, which keeps the conda and tegra suites - which also run that directory - unchanged, and are skipped under sanitizers because qa/leak.sup has no suppressions for the AWS SDK, libcurl or OpenSSL globals. Also document AWS_ENDPOINT_URL, which the tests depend on and which was previously undocumented.
|
| text=True, | ||
| ) | ||
| atexit.register(self.stop) | ||
| line = self._proc.stdout.readline() # the child reports the port it bound |
There was a problem hiding this comment.
This blocking readline() runs before the startup deadline is created. If importing moto or starting ThreadedMotoServer stalls before the child prints its port, the test process waits indefinitely and never reaches _wait_until_serving(). Please read the port with a bounded mechanism and terminate the child when startup_timeout_s expires.
| client.create_bucket(Bucket=s3.BUCKET) | ||
| s3.upload_dir(client, s3.BUCKET, g_root, "data") | ||
| client.upload_file(g_tar, s3.BUCKET, "wds/shard0.tar") |
There was a problem hiding this comment.
External MinIO or S3 runs upload data under fixed data/, wds/, and many/ prefixes, but teardown only cleans local resources. If the bucket is shared or was used by an earlier run, extra keys can break the exact 1,100-object assertion, concurrent runs can interfere, and each run leaves test objects behind. Please isolate each run with a unique prefix and remove its remote objects during teardown.
| Network | ||
| ~~~~~~~ | ||
|
|
||
| `AWS_ENDPOINT_URL` |
There was a problem hiding this comment.
The new heading uses single backticks around AWS_ENDPOINT_URL. The repository's RST directive requires double backticks for literal or inline code, so this formatting requirement must be satisfied before merging.
Rule Used: RST formatting: single backticks for cross-reference roles (:ref:, :py:obj:) and named external links, double backticks for literal/inline code. Don't mix the two — using single backticks for what should be a code literal renders as italics, and ... (source)
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical unsupported-build handling and multiple moderate external-S3 lifecycle and configuration issues remain.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds S3 reader integration tests using a subprocess-based Moto server, with optional external S3-compatible endpoints.
Changes:
- Adds S3 lifecycle, seeding, environment, and capability helpers.
- Tests file readers, WebDataset readers, pagination, and error handling.
- Updates dependencies, sanitizer exclusions, and
AWS_ENDPOINT_URLdocumentation.
File summaries
| File | Changes and review comments |
|---|---|
qa/TL0_python-self-test-readers-decoders/test_nofw.sh |
Adds Moto, Flask, Flask-CORS, and boto3 dependencies. |
qa/TL0_python-self-test-readers-decoders/test_body.sh |
Skips the S3 module under sanitizers. |
docs/env_vars.rst |
Documents AWS_ENDPOINT_URL. |
dali/test/python/s3_test_utils.py |
Provides S3 server and setup helpers. Critical (2 votes): the unsupported-build probe does not exercise FileStream::Open, so BUILD_AWSSDK=OFF builds fail instead of skipping. Moderate: external runs do not reliably handle missing boto3 (1), overwrite standard AWS credentials (2), can hang before the startup timeout (1), force us-east-1 (1), and do not support external-region bucket creation (1). |
dali/test/python/reader/test_s3.py |
Adds file-reader and WebDataset tests, pagination, and missing-resource checks. Moderate: external runs leave seeded objects behind (2) and rely on a potentially pre-existing missing-bucket name (1). |
Review details
Suppressed comments (4)
dali/test/python/reader/test_s3.py:191
- The negative test relies on
dali-no-such-bucketnever existing, but that is not guaranteed whenDALI_TEST_S3_ENDPOINTpoints at a persistent MinIO or real S3 service (the name could already be present or owned by the test account). In that case listing succeeds or returns a different error and theNoSuchBucketassertion fails. Use a per-run unique bucket name or make the missing-bucket case configurable/skip it for external services.
def test_file_reader_missing_bucket():
with assert_raises(RuntimeError, glob="*NoSuchBucket*"):
file_pipe(file_root="s3://dali-no-such-bucket/data").build()
dali/test/python/s3_test_utils.py:193
- When
DALI_TEST_S3_ENDPOINTis set, this returns before checkingboto3, butsetUpModulestill callss3_client()and that function unconditionally importsboto3at line 166. Consequently an external MinIO/real-S3 run withoutboto3fails with an uncaught import error instead of being skipped; checkboto3unconditionally and only makemoto.serverconditional on the absence of the external endpoint.
if os.environ.get("DALI_TEST_S3_ENDPOINT"):
return
dali/test/python/s3_test_utils.py:94
- This blocking
readline()is executed before_wait_until_serving()appliesstartup_timeout_s. If moto import orserver.start()hangs before printing a port, the whole reader suite hangs indefinitely rather than failing after the configured 60 seconds; read/poll the pipe with the deadline (or add a watchdog) before waiting for the startup line.
line = self._proc.stdout.readline() # the child reports the port it bound
dali/test/python/s3_test_utils.py:29
REGIONis forced tous-east-1and then exported over both standard region variables, so the advertised external real-S3 mode cannot use a bucket in another region: the unconditionalcreate_bucketcall will require a location constraint, and DALI requests will be signed with the wrong region. Allow the external test region to be configured and create the bucket with the matchingLocationConstraintwhen needed.
REGION = "us-east-1"
- Files reviewed: 5/5 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
|
|
||
| @pipeline_def(batch_size=1, num_threads=1, device_id=None) | ||
| def probe(): | ||
| return tuple(fn.readers.file(files=["s3://dali-no-such-bucket/no-such-key"])) |
| client.create_bucket(Bucket=s3.BUCKET) | ||
| s3.upload_dir(client, s3.BUCKET, g_root, "data") | ||
| client.upload_file(g_tar, s3.BUCKET, "wds/shard0.tar") |
| ACCESS_KEY = os.environ.get("DALI_TEST_S3_ACCESS_KEY", "dalitestaccesskey") | ||
| SECRET_KEY = os.environ.get("DALI_TEST_S3_SECRET_KEY", "dalitestsecretkey") |
|
!build |
|
CI MESSAGE: [67423372]: BUILD STARTED |
|
CI MESSAGE: [67423372]: BUILD FAILED |
Category:
Other (Tests, Documentation)
Description:
DALI's
s3://support had no test coverage. This adds integration tests that start a mock S3 serverin the background, seed a bucket, and assert that the readers return byte-identical results to the
same data read from local disk.
The mock server is
moto, started and torn down by the test module itself, so no external service orCI-side fixture is needed.
DALI_TEST_S3_ENDPOINTpoints the same tests at MinIO or at real S3instead.
Two implementation details are load-bearing and are commented in the source so they are not
"simplified" away later:
Pipeline::Buildis bound withoutpy::call_guard<py::gil_scoped_release>, and S3 object listing happens insideBuild(), so anin-process server thread is GIL-starved and the request eventually times out in libcurl
(
curlCode: 28). Everymotoexample shows the in-process form, hence the comment.useVirtualAddressing, so the AWS SDKonly selects path-style addressing when the endpoint host is an IP. With a host name the bucket is
prepended to the host instead, which requires a matching DNS entry (and
MINIO_DOMAIN, for a realMinIO).
Additional information:
Affected modules and functionalities:
dali/test/python/s3_test_utils.py(new): mock server lifecycle, env setup, bucket helpers andskip predicates.
dali/test/python/reader/test_s3.py(new): the tests.qa/TL0_python-self-test-readers-decoders/test_nofw.sh: addsmoto,flask,flask-corsandboto3topip_packages. Plainmotodoes not pull inflask; that lives in themoto[server]extra, which also drags in
cfn-lint,dockerand more.qa/TL0_python-self-test-readers-decoders/test_body.sh: skips the module under sanitizers.docs/env_vars.rst: documentsAWS_ENDPOINT_URL, which the tests rely on and which waspreviously undocumented.
Key points relevant for the review:
S3ClientManageralready readsAWS_ENDPOINT_URL.nose2overdali/test/python/reader, so the module is picked up bydiscovery and only the pip package list needed changing.
motois absent, so the conda and tegra suites, which run the samedirectory without it, stay green with no edits.
qa/leak.suphas no suppressions for the AWS SDK, libcurlor OpenSSL globals, and the mock server subprocess would inherit
LD_PRELOAD.motodoes not verify SigV4, so the key-with-a-space casepins URL path construction rather than canonical-request encoding. Signing fidelity needs a run
against MinIO or real S3 through
DALI_TEST_S3_ENDPOINT; worth a follow-up job.device_id=None), on a suite that already runs for several minutes.Roughly 9 s of that is the listing-pagination case, which seeds 1100 objects.
Tests:
New module
reader/test_s3.py, covering:test_file_reader_file_rootListObjectsV2with a prefix, label assignment,file_filtersover keystest_file_reader_files_argHeadObject+ rangedGetObject, a key containing a space, a 0-byte objecttest_file_reader_listing_paginationtest_webdataset_index_inferredtest_webdataset_local_indextest_file_reader_missing_objecttest_file_reader_missing_bucketReaders that do not route through
FileStream::Open(FITS, LMDB-backed caffe/caffe2, nemo_asr,sequence, video) are out of scope.
fn.readers.numpyovers3://is also left out of this PR - ithits a separate bug, fixed in DALI-4887; it can be added to this matrix once that lands.
Checklist
Documentation
DALI team only
Requirements
REQ IDs: N/A
JIRA TASK: DALI-4010