Framework: pytest (requirements/test.txt). Config in pyproject.toml [tool.pytest.ini_options].
# Single test (the fast dev loop)
pytest tests/streaming/test_dataset.py::test_name -v --capture=no
# A whole file / dir
pytest tests/streaming/ -v
# How CI runs it (authoritative). Two phases because processing tests must be serial:
pytest tests --ignore=tests/processing --ignore=tests/raw \
-n 2 --dist=loadgroup --cov=litdata --durations=0 --timeout=120 --capture=no --verbose
pytest tests/processing tests/raw --cov=litdata --cov-append --durations=0 --timeout=120 --capture=no --verbose--dist=loadgroup is required, not optional, when running with -n>1. tests/conftest.py:165 (pytest_collection_modifyitems) pins every test using the clean_pq_index_cache fixture onto a single xdist worker via xdist_group, because that fixture shutil.rmtrees ~/.lightning/chunks and would otherwise race across workers.
The Makefile test target is stale (--flake8 flag, points at src) — prefer the CI commands above.
addoptsincludes--doctest-modules→ docstrings insrc/are collected as tests; a malformed docstring example fails CI.--strict-markers→ only registered markers allowed. Sole custom marker:cloud(real integration tests, not in the standard matrix).filterwarnings = ["error::FutureWarning"]→ FutureWarnings are hard errors.xfail_strict = true→ anxfailthat unexpectedly passes fails the suite.
Tests mirror src/litdata/: src/litdata/streaming/dataset.py → tests/streaming/test_dataset.py. Dirs: tests/streaming/, tests/processing/, tests/utilities/, tests/raw/, plus top-level tests/test_cli.py, test_debugger.py, test_helper.py, test_imports.py, test_requirements.py. Every dir has __init__.py; cross-test helpers import as a package (from tests.streaming.utils import filter_lock_files).
Template (from CONTRIBUTING.md):
def test_explain_what_is_being_tested(tmpdir):
"""One-line description of what/why."""
cache_dir = os.path.join(tmpdir, "cache_dir")
assert ...CONTRIBUTING.md shows a @RunIf(min_cuda_gpus=1) template copied from PyTorch Lightning, but no RunIf class exists in this repo. Don't use it. The real pattern is pytest.mark.skipif + the _*_AVAILABLE RequirementCache constants in src/litdata/constants.py:
from litdata.constants import _ZSTD_AVAILABLE, _PIL_AVAILABLE
# optional dependency, at param level
pytest.param("zstd", marks=pytest.mark.skipif(not _ZSTD_AVAILABLE, reason="Requires: ['zstd']"))
# at function level
@pytest.mark.skipif(not _PIL_AVAILABLE or sys.platform == "win32", reason="Requires: ['pil']")
# OS gating (very common)
@pytest.mark.skipif(sys.platform == "win32", reason="Not tested on windows")
# python-version gating
@pytest.mark.skipif(sys.version_info >= (3, 12), reason="Multiprocessing issues on 3.12+")Available flags: _ZSTD_AVAILABLE, _BOTO3_AVAILABLE, _FSSPEC_AVAILABLE, _CRYPTOGRAPHY_AVAILABLE, _GOOGLE_STORAGE_AVAILABLE, _AZURE_STORAGE_AVAILABLE, _POLARS_AVAILABLE, _PYARROW_AVAILABLE, _PIL_AVAILABLE, _TORCH_VISION_AVAILABLE, _AV_AVAILABLE, _OBSTORE_AVAILABLE, _HF_HUB_AVAILABLE, _LIGHTNING_SDK_AVAILABLE, _VIZ_TRACKER_AVAILABLE.
Two techniques:
- Fake modules injected into
sys.modulesviamonkeypatch.setitem, provided asconftest.pyfixtures:google_mock,fsspec_mock,obstore_mock,azure_mock,lightning_cloud_mock(setsrest_client.LightningClient = Mock()),lightning_sdk_mock,huggingface_hub_mock,huggingface_hub_fs_mock(full fakeHfFileSystembacked by local parquet),fsspec_pq_mock. These let import-guarded cloud code run without the real SDK. monkeypatch.setattron the module'sboto3/botocorefor S3 — e.g.tests/streaming/test_client.pypatchesclient.boto3/client.botocorewithMagicMock()and asserts on.client.assert_called_with(...).boto3is a hard dep, so it's stubbed, not installed-away.
Use stdlib unittest.mock (Mock, MagicMock, patch) + the pytest monkeypatch fixture.
- Autouse/session:
teardown_process_group(destroys torch.distributed group),_thread_police(fails a test leaking a non-daemon "zombie" thread — special-casesPrepareChunksThread,QueueFeederThread,pytest_timeout). - Autouse/per-test:
disable_signals(no-opssignal.signalso worker signal handlers don't break tests). - Data:
mosaic_mds_index_data,prepare_combined_dataset(two on-disk 50-itemCachedatasets),combined_dataset(aCombinedStreamingDataset),pq_data,write_pq_data(5 polars parquet files),clean_pq_index_cache(wipes the default cache dir — the one that triggers xdist grouping).
Local idiom: most files define their own seed_everything(seed). CLI tests use a run_cli(args_list) helper (patches sys.argv, captures stdout). tests/streaming/utils.py gives filter_lock_files / get_lock_files to assert on chunk dirs ignoring .lock/.cnt artifacts.
tests/raw/test_fork_safety.py is the correctness suite for cloud-download hardening. When changing StreamingRawDataset / CacheManager / indexer publish paths, keep or extend coverage for:
- Fork/spawn LoopRunner reinit + pid-guarded downloader/permit caches
- Allowlisted pickle (
__getstate__must not ship accidental instance attrs) - Atomic cache +
index.json.zstdpublishes (tmp +os.replace) - Batch-level
download_timeouthang recovery (cancel poisoned_inflight; retry succeeds) - Fast path: per-item GETs stay bare when
hedge_delay=0even ifdownload_timeout=120 - Adaptive vs exact
max_concurrent_downloads(None vs int)
tests/raw/conftest.py tears down the module LoopRunner between tests — follow that pattern if you add fixtures that touch _RUNNER.
Error-path behavior is production code: do not “fix” hang recovery only in comments — assert it.
- Processing/worker tests spawn real subprocesses — run serially (why CI separates
tests/processing). Use--timeout=120. - A leaked thread →
_thread_policeAssertionError: Test left zombie thread. Ensure background threads (PrepareChunksThread) are stopped. Ignore leftoverasyncio_Ndaemons from async prefetch teardown (seetests/conftest.py). - Tiny
max_cache_size+ delete-when-processed timeouts (_wait_until_chunk_ready/thread.join): usuallymax_pre_downloadcapped to 1 or blockingshutdown_default_executor. See cache-and-chunk-lifecycle.md § Prefetch & eviction. - Windows
PermissionErroron.binopen after zstd decompress: expect retry via_open_chunk_file; close handles before delete. - To force the boto3 download path in a unit test when obstore is installed:
monkeypatch.setattr(downloader_mod, "_OBSTORE_AVAILABLE", False). - Streaming S3 fork / Studio session regressions (
tests/streaming/test_downloader.py,test_client.py): keeptest_s3_index_download_does_not_start_obstore,test_obstore_usable_false_after_parent_init,test_build_obstore_s3_store_does_not_pass_data_connection_id_to_session, pickle tests that drop_store/_client. - Keyed lookup (
tests/streaming/test_dataset_update.py): requires polars (collection errors without it). No multi-nodekey_fnmerge tests intests/processing/yet — add those when touching_merge_and_upload_keys. - Windows gap: most MP/resolver tests
skipif(win32). Retry paths (_open_chunk_file, keysos.replace) exist in code and are largely untested on Windows runners. Close mmaps before rewrite tests. @pytest.mark.cloudis registered but unused.test_dataloader_profilingis permanently skipped.- Tracer (
tests/test_debugger.py,tests/streaming/test_reader.py): one-line formatter (no traceback bodies), microsecond timestamps, level/category filtering, crash instantph: I/name: crash. spawnrequires picklable args; a test failing only under spawn usually means an unpicklable closure.- To debug inside a worker:
from litdata.utilities.breakpoint import breakpoint; breakpoint()(see debugging.md).