mirror of
https://github.com/huggingface/lerobot.git
synced 2026-07-31 05:29:40 +00:00
648 lines
22 KiB
Python
648 lines
22 KiB
Python
#!/usr/bin/env python
|
|
|
|
# Copyright 2025 The HuggingFace Inc. team. All rights reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
from types import SimpleNamespace
|
|
from unittest.mock import Mock, patch
|
|
|
|
import numpy as np
|
|
import pytest
|
|
import torch
|
|
|
|
pytest.importorskip("datasets", reason="datasets is required (install lerobot[dataset])")
|
|
|
|
import lerobot.datasets.streaming_dataset as streaming_dataset_module
|
|
from lerobot.datasets.dataset_metadata import LeRobotDatasetMetadata
|
|
from lerobot.datasets.streaming_dataset import StreamingLeRobotDataset
|
|
from lerobot.datasets.utils import safe_shard
|
|
from lerobot.utils.constants import ACTION
|
|
from tests.fixtures.constants import DUMMY_REPO_ID
|
|
|
|
|
|
def get_frames_expected_order(streaming_ds: StreamingLeRobotDataset) -> list[int]:
|
|
"""Replicates the shuffling logic of StreamingLeRobotDataset to get the expected order of indices."""
|
|
rng = np.random.default_rng(streaming_ds.seed)
|
|
buffer_size = streaming_ds.buffer_size
|
|
num_shards = streaming_ds.num_shards
|
|
|
|
shards_indices = []
|
|
for shard_idx in range(num_shards):
|
|
shard = streaming_ds.hf_dataset.shard(num_shards, index=shard_idx)
|
|
shard_indices = [item["index"] for item in shard]
|
|
shards_indices.append(shard_indices)
|
|
|
|
shard_iterators = {i: iter(s) for i, s in enumerate(shards_indices)}
|
|
|
|
buffer_indices_generator = streaming_ds._iter_random_indices(rng, buffer_size)
|
|
|
|
frames_buffer = []
|
|
expected_indices = []
|
|
|
|
while shard_iterators: # While there are still available shards
|
|
available_shard_keys = list(shard_iterators.keys())
|
|
if not available_shard_keys:
|
|
break
|
|
|
|
# Call _infinite_generator_over_elements with current available shards (key difference!)
|
|
shard_key = next(streaming_ds._infinite_generator_over_elements(rng, available_shard_keys))
|
|
|
|
try:
|
|
frame_index = next(shard_iterators[shard_key])
|
|
|
|
if len(frames_buffer) == buffer_size:
|
|
i = next(buffer_indices_generator)
|
|
expected_indices.append(frames_buffer[i])
|
|
frames_buffer[i] = frame_index
|
|
else:
|
|
frames_buffer.append(frame_index)
|
|
|
|
except StopIteration:
|
|
del shard_iterators[shard_key] # Remove exhausted shard
|
|
|
|
rng.shuffle(frames_buffer)
|
|
expected_indices.extend(frames_buffer)
|
|
|
|
return expected_indices
|
|
|
|
|
|
@pytest.mark.parametrize("token", ["hf_test_token", True, False])
|
|
@pytest.mark.parametrize("from_local", [False, True])
|
|
def test_streaming_dataset_forwards_hub_token_only_for_remote_data(tmp_path, monkeypatch, token, from_local):
|
|
requested_root = tmp_path / "local" if from_local else None
|
|
metadata = SimpleNamespace(
|
|
root=requested_root or tmp_path / "snapshot",
|
|
revision=streaming_dataset_module.CODEBASE_VERSION,
|
|
_version=streaming_dataset_module.CODEBASE_VERSION,
|
|
features={},
|
|
depth_keys=[],
|
|
image_keys=[],
|
|
rescale_depth_stats=Mock(),
|
|
)
|
|
metadata_cls = Mock(return_value=metadata)
|
|
load_dataset = Mock(return_value=SimpleNamespace(num_shards=1))
|
|
monkeypatch.setattr(streaming_dataset_module, "LeRobotDatasetMetadata", metadata_cls)
|
|
monkeypatch.setattr(streaming_dataset_module, "load_dataset", load_dataset)
|
|
|
|
dataset = StreamingLeRobotDataset(DUMMY_REPO_ID, root=requested_root, token=token)
|
|
|
|
metadata_cls.assert_called_once_with(
|
|
DUMMY_REPO_ID,
|
|
requested_root,
|
|
streaming_dataset_module.CODEBASE_VERSION,
|
|
force_cache_sync=False,
|
|
repo_type="dataset",
|
|
token=token,
|
|
)
|
|
if from_local:
|
|
assert "token" not in load_dataset.call_args.kwargs
|
|
else:
|
|
assert load_dataset.call_args.kwargs["token"] is token
|
|
assert not hasattr(dataset, "_token")
|
|
|
|
|
|
def test_single_frame_consistency(tmp_path, lerobot_dataset_factory):
|
|
"""Test if are correctly accessed"""
|
|
ds_num_frames = 400
|
|
ds_num_episodes = 10
|
|
buffer_size = 100
|
|
|
|
local_path = tmp_path / "test"
|
|
repo_id = f"{DUMMY_REPO_ID}"
|
|
|
|
ds = lerobot_dataset_factory(
|
|
root=local_path,
|
|
repo_id=repo_id,
|
|
total_episodes=ds_num_episodes,
|
|
total_frames=ds_num_frames,
|
|
)
|
|
|
|
streaming_ds = iter(StreamingLeRobotDataset(repo_id=repo_id, root=local_path, buffer_size=buffer_size))
|
|
|
|
key_checks = []
|
|
for _ in range(ds_num_frames):
|
|
streaming_frame = next(streaming_ds)
|
|
frame_idx = streaming_frame["index"]
|
|
target_frame = ds[frame_idx]
|
|
|
|
for key in streaming_frame:
|
|
left = streaming_frame[key]
|
|
right = target_frame[key]
|
|
|
|
if isinstance(left, str):
|
|
check = left == right
|
|
|
|
elif isinstance(left, torch.Tensor):
|
|
check = torch.allclose(left, right) and left.shape == right.shape
|
|
|
|
elif isinstance(left, float):
|
|
check = left == right.item() # right is a torch.Tensor
|
|
|
|
key_checks.append((key, check))
|
|
|
|
assert all(t[1] for t in key_checks), (
|
|
f"Checking {list(filter(lambda t: not t[1], key_checks))[0][0]} left and right were found different (frame_idx: {frame_idx})"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"shuffle",
|
|
[False, True],
|
|
)
|
|
def test_frames_order_over_epochs(tmp_path, lerobot_dataset_factory, shuffle):
|
|
"""Test if streamed frames correspond to shuffling operations over in-memory dataset."""
|
|
ds_num_frames = 400
|
|
ds_num_episodes = 10
|
|
buffer_size = 100
|
|
seed = 42
|
|
n_epochs = 3
|
|
|
|
local_path = tmp_path / "test"
|
|
repo_id = f"{DUMMY_REPO_ID}"
|
|
|
|
lerobot_dataset_factory(
|
|
root=local_path,
|
|
repo_id=repo_id,
|
|
total_episodes=ds_num_episodes,
|
|
total_frames=ds_num_frames,
|
|
)
|
|
|
|
streaming_ds = StreamingLeRobotDataset(
|
|
repo_id=repo_id, root=local_path, buffer_size=buffer_size, seed=seed, shuffle=shuffle
|
|
)
|
|
|
|
first_epoch_indices = [frame["index"] for frame in streaming_ds]
|
|
expected_indices = get_frames_expected_order(streaming_ds)
|
|
|
|
assert first_epoch_indices == expected_indices, "First epoch indices do not match expected indices"
|
|
|
|
expected_indices = get_frames_expected_order(streaming_ds)
|
|
for _ in range(n_epochs):
|
|
streaming_indices = [frame["index"] for frame in streaming_ds]
|
|
frames_match = all(
|
|
s_index == e_index for s_index, e_index in zip(streaming_indices, expected_indices, strict=True)
|
|
)
|
|
|
|
if shuffle:
|
|
assert not frames_match
|
|
else:
|
|
assert frames_match
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"shuffle",
|
|
[False, True],
|
|
)
|
|
def test_frames_order_with_shards(tmp_path, lerobot_dataset_factory, shuffle):
|
|
"""Test if streamed frames correspond to shuffling operations over in-memory dataset with multiple shards."""
|
|
ds_num_frames = 100
|
|
ds_num_episodes = 10
|
|
buffer_size = 10
|
|
|
|
seed = 42
|
|
n_epochs = 3
|
|
data_file_size_mb = 0.001
|
|
|
|
chunks_size = 1
|
|
|
|
local_path = tmp_path / "test"
|
|
repo_id = f"{DUMMY_REPO_ID}-ciao"
|
|
|
|
lerobot_dataset_factory(
|
|
root=local_path,
|
|
repo_id=repo_id,
|
|
total_episodes=ds_num_episodes,
|
|
total_frames=ds_num_frames,
|
|
data_files_size_in_mb=data_file_size_mb,
|
|
chunks_size=chunks_size,
|
|
)
|
|
|
|
streaming_ds = StreamingLeRobotDataset(
|
|
repo_id=repo_id,
|
|
root=local_path,
|
|
buffer_size=buffer_size,
|
|
seed=seed,
|
|
shuffle=shuffle,
|
|
max_num_shards=4,
|
|
)
|
|
|
|
first_epoch_indices = [frame["index"] for frame in streaming_ds]
|
|
expected_indices = get_frames_expected_order(streaming_ds)
|
|
|
|
assert first_epoch_indices == expected_indices, "First epoch indices do not match expected indices"
|
|
|
|
for _ in range(n_epochs):
|
|
streaming_indices = [
|
|
frame["index"] for frame in streaming_ds
|
|
] # NOTE: this is the same as first_epoch_indices
|
|
frames_match = all(
|
|
s_index == e_index for s_index, e_index in zip(streaming_indices, expected_indices, strict=True)
|
|
)
|
|
if shuffle:
|
|
assert not frames_match
|
|
else:
|
|
assert frames_match
|
|
|
|
|
|
def test_iter_raises_on_frame_error(tmp_path, lerobot_dataset_factory, monkeypatch):
|
|
"""Video decode failures must propagate instead of being silently ignored."""
|
|
ds_num_frames = 20
|
|
ds_num_episodes = 2
|
|
buffer_size = 10
|
|
|
|
local_path = tmp_path / "test"
|
|
repo_id = f"{DUMMY_REPO_ID}"
|
|
|
|
lerobot_dataset_factory(
|
|
root=local_path,
|
|
repo_id=repo_id,
|
|
total_episodes=ds_num_episodes,
|
|
total_frames=ds_num_frames,
|
|
)
|
|
|
|
streaming_ds = StreamingLeRobotDataset(repo_id=repo_id, root=local_path, buffer_size=buffer_size)
|
|
|
|
def broken_video_decode(*args, **kwargs):
|
|
raise RuntimeError("Could not load libtorchcodec")
|
|
|
|
monkeypatch.setattr(streaming_dataset_module, "decode_video_frames_torchcodec", broken_video_decode)
|
|
|
|
with pytest.raises(RuntimeError, match="libtorchcodec"):
|
|
next(iter(streaming_ds))
|
|
|
|
|
|
def test_iter_raises_on_nested_generator_error(tmp_path, lerobot_dataset_factory, monkeypatch):
|
|
"""PEP 479 errors below frame construction must not be mistaken for shard exhaustion."""
|
|
local_path = tmp_path / "test"
|
|
repo_id = DUMMY_REPO_ID
|
|
|
|
lerobot_dataset_factory(root=local_path, repo_id=repo_id, total_episodes=2, total_frames=20)
|
|
streaming_ds = StreamingLeRobotDataset(repo_id=repo_id, root=local_path, buffer_size=10)
|
|
|
|
def broken_frame_generator():
|
|
raise StopIteration("decoder internal failure")
|
|
yield
|
|
|
|
def broken_video_decode(*args, **kwargs):
|
|
return next(broken_frame_generator())
|
|
|
|
monkeypatch.setattr(streaming_dataset_module, "decode_video_frames_torchcodec", broken_video_decode)
|
|
|
|
with pytest.raises(RuntimeError, match="generator raised StopIteration"):
|
|
next(iter(streaming_ds))
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"state_deltas, action_deltas",
|
|
[
|
|
([-1, -0.5, -0.20, 0], [0, 1, 2, 3]),
|
|
([-1, -0.5, -0.20, 0], [-1.5, -1, -0.5, -0.20, -0.10, 0]),
|
|
([-2, -1, -0.5, 0], [0, 1, 2, 3]),
|
|
([-2, -1, -0.5, 0], [-1.5, -1, -0.5, -0.20, -0.10, 0]),
|
|
],
|
|
)
|
|
def test_frames_with_delta_consistency(tmp_path, lerobot_dataset_factory, state_deltas, action_deltas):
|
|
ds_num_frames = 500
|
|
ds_num_episodes = 10
|
|
buffer_size = 100
|
|
|
|
seed = 42
|
|
|
|
local_path = tmp_path / "test"
|
|
repo_id = f"{DUMMY_REPO_ID}-ciao"
|
|
camera_key = "phone"
|
|
|
|
delta_timestamps = {
|
|
camera_key: state_deltas,
|
|
"state": state_deltas,
|
|
ACTION: action_deltas,
|
|
}
|
|
|
|
ds = lerobot_dataset_factory(
|
|
root=local_path,
|
|
repo_id=repo_id,
|
|
total_episodes=ds_num_episodes,
|
|
total_frames=ds_num_frames,
|
|
delta_timestamps=delta_timestamps,
|
|
)
|
|
|
|
streaming_ds = iter(
|
|
StreamingLeRobotDataset(
|
|
repo_id=repo_id,
|
|
root=local_path,
|
|
buffer_size=buffer_size,
|
|
seed=seed,
|
|
shuffle=False,
|
|
delta_timestamps=delta_timestamps,
|
|
)
|
|
)
|
|
|
|
for i in range(ds_num_frames):
|
|
streaming_frame = next(streaming_ds)
|
|
frame_idx = streaming_frame["index"]
|
|
target_frame = ds[frame_idx]
|
|
|
|
assert set(streaming_frame.keys()) == set(target_frame.keys()), (
|
|
f"Keys differ between streaming frame and target one. Differ at: {set(streaming_frame.keys()) - set(target_frame.keys())}"
|
|
)
|
|
|
|
key_checks = []
|
|
for key in streaming_frame:
|
|
left = streaming_frame[key]
|
|
right = target_frame[key]
|
|
|
|
if isinstance(left, str):
|
|
check = left == right
|
|
|
|
elif isinstance(left, torch.Tensor):
|
|
if (
|
|
key not in ds.meta.camera_keys
|
|
and "is_pad" not in key
|
|
and f"{key}_is_pad" in streaming_frame
|
|
):
|
|
# comparing frames only on non-padded regions. Padding is applied to last-valid broadcasting
|
|
left = left[~streaming_frame[f"{key}_is_pad"]]
|
|
right = right[~target_frame[f"{key}_is_pad"]]
|
|
|
|
check = torch.allclose(left, right) and left.shape == right.shape
|
|
|
|
key_checks.append((key, check))
|
|
|
|
assert all(t[1] for t in key_checks), (
|
|
f"Checking {list(filter(lambda t: not t[1], key_checks))[0][0]} left and right were found different (i: {i}, frame_idx: {frame_idx})"
|
|
)
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"state_deltas, action_deltas",
|
|
[
|
|
([-1, -0.5, -0.20, 0], [0, 1, 2, 3, 10, 20]),
|
|
([-1, -0.5, -0.20, 0], [-20, -1.5, -1, -0.5, -0.20, -0.10, 0]),
|
|
([-2, -1, -0.5, 0], [0, 1, 2, 3, 10, 20]),
|
|
([-2, -1, -0.5, 0], [-20, -1.5, -1, -0.5, -0.20, -0.10, 0]),
|
|
],
|
|
)
|
|
def test_frames_with_delta_consistency_with_shards(
|
|
tmp_path, lerobot_dataset_factory, state_deltas, action_deltas
|
|
):
|
|
ds_num_frames = 100
|
|
ds_num_episodes = 10
|
|
buffer_size = 10
|
|
data_file_size_mb = 0.001
|
|
chunks_size = 1
|
|
|
|
seed = 42
|
|
|
|
local_path = tmp_path / "test"
|
|
repo_id = f"{DUMMY_REPO_ID}-ciao"
|
|
camera_key = "phone"
|
|
|
|
delta_timestamps = {
|
|
camera_key: state_deltas,
|
|
"state": state_deltas,
|
|
ACTION: action_deltas,
|
|
}
|
|
|
|
ds = lerobot_dataset_factory(
|
|
root=local_path,
|
|
repo_id=repo_id,
|
|
total_episodes=ds_num_episodes,
|
|
total_frames=ds_num_frames,
|
|
delta_timestamps=delta_timestamps,
|
|
data_files_size_in_mb=data_file_size_mb,
|
|
chunks_size=chunks_size,
|
|
)
|
|
streaming_ds = StreamingLeRobotDataset(
|
|
repo_id=repo_id,
|
|
root=local_path,
|
|
buffer_size=buffer_size,
|
|
seed=seed,
|
|
shuffle=False,
|
|
delta_timestamps=delta_timestamps,
|
|
max_num_shards=4,
|
|
)
|
|
|
|
iter(streaming_ds)
|
|
|
|
num_shards = 4
|
|
shards_indices = []
|
|
for shard_idx in range(num_shards):
|
|
shard = safe_shard(streaming_ds.hf_dataset, shard_idx, num_shards)
|
|
shard_indices = [item["index"] for item in shard]
|
|
shards_indices.append(shard_indices)
|
|
|
|
streaming_ds = iter(streaming_ds)
|
|
|
|
for i in range(ds_num_frames):
|
|
streaming_frame = next(streaming_ds)
|
|
frame_idx = streaming_frame["index"]
|
|
target_frame = ds[frame_idx]
|
|
|
|
assert set(streaming_frame.keys()) == set(target_frame.keys()), (
|
|
f"Keys differ between streaming frame and target one. Differ at: {set(streaming_frame.keys()) - set(target_frame.keys())}"
|
|
)
|
|
|
|
key_checks = []
|
|
for key in streaming_frame:
|
|
left = streaming_frame[key]
|
|
right = target_frame[key]
|
|
|
|
if isinstance(left, str):
|
|
check = left == right
|
|
|
|
elif isinstance(left, torch.Tensor):
|
|
if (
|
|
key not in ds.meta.camera_keys
|
|
and "is_pad" not in key
|
|
and f"{key}_is_pad" in streaming_frame
|
|
):
|
|
# comparing frames only on non-padded regions. Padding is applied to last-valid broadcasting
|
|
left = left[~streaming_frame[f"{key}_is_pad"]]
|
|
right = right[~target_frame[f"{key}_is_pad"]]
|
|
|
|
check = torch.allclose(left, right) and left.shape == right.shape
|
|
|
|
elif isinstance(left, float):
|
|
check = left == right.item() # right is a torch.Tensor
|
|
|
|
key_checks.append((key, check))
|
|
|
|
assert all(t[1] for t in key_checks), (
|
|
f"Checking {list(filter(lambda t: not t[1], key_checks))[0][0]} left and right were found different (i: {i}, frame_idx: {frame_idx})"
|
|
)
|
|
|
|
|
|
class _StopConstructionError(Exception):
|
|
"""Sentinel raised from a patched load_dataset to halt __init__ after the branch under test."""
|
|
|
|
|
|
def _fake_meta(*args, **kwargs):
|
|
"""Minimal LeRobotDatasetMetadata stand-in exposing only what __init__ reads."""
|
|
meta = type("_Meta", (), {})()
|
|
root = kwargs.get("root", args[1] if len(args) > 1 else None)
|
|
revision = kwargs.get("revision", args[2] if len(args) > 2 else None)
|
|
meta.root = root or "/tmp/_streaming_meta"
|
|
meta.revision = revision or "v0"
|
|
meta._version = "v3.0"
|
|
meta.depth_keys = []
|
|
meta.image_keys = []
|
|
meta.rescale_depth_stats = lambda *_a, **_k: None
|
|
meta.repo_type = kwargs.get("repo_type", "dataset")
|
|
return meta
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"repo_type, expected_source, expected_data_files",
|
|
[
|
|
("bucket", "parquet", "hf://buckets/{repo_id}/data/*/*.parquet"),
|
|
("dataset", "{repo_id}", "data/*/*.parquet"),
|
|
],
|
|
)
|
|
def test_streaming_repo_type_routes_load_dataset(repo_type, expected_source, expected_data_files):
|
|
"""repo_type='bucket' loads parquet from hf://buckets/...; 'dataset' keeps the Hub-repo path."""
|
|
captured = {}
|
|
token = "hf_test_token"
|
|
|
|
def fake_load_dataset(source, **kwargs):
|
|
captured["source"] = source
|
|
captured["data_files"] = kwargs.get("data_files")
|
|
captured["token"] = kwargs.get("token")
|
|
raise _StopConstructionError
|
|
|
|
with (
|
|
patch("lerobot.datasets.streaming_dataset.LeRobotDatasetMetadata", _fake_meta),
|
|
patch("lerobot.datasets.streaming_dataset.check_version_compatibility", lambda *a, **k: None),
|
|
patch("lerobot.datasets.streaming_dataset.load_dataset", fake_load_dataset),
|
|
pytest.raises(_StopConstructionError),
|
|
):
|
|
StreamingLeRobotDataset(DUMMY_REPO_ID, repo_type=repo_type, token=token)
|
|
|
|
assert captured["source"] == expected_source.format(repo_id=DUMMY_REPO_ID)
|
|
assert captured["data_files"] == expected_data_files.format(repo_id=DUMMY_REPO_ID)
|
|
assert captured["token"] == token
|
|
|
|
|
|
def test_bucket_metadata_url_root(tmp_path):
|
|
"""repo_type='bucket' produces url_root pointing at hf://buckets/..."""
|
|
with patch.object(LeRobotDatasetMetadata, "_load_metadata"):
|
|
meta = LeRobotDatasetMetadata(
|
|
DUMMY_REPO_ID,
|
|
root=tmp_path,
|
|
repo_type="bucket",
|
|
)
|
|
assert meta.url_root == f"hf://buckets/{DUMMY_REPO_ID}"
|
|
|
|
|
|
def test_bucket_skips_get_safe_version(tmp_path):
|
|
"""repo_type='bucket' must NOT call get_safe_version (buckets have no git refs).
|
|
|
|
The first ``_load_metadata()`` raises ``FileNotFoundError`` so ``__init__``
|
|
enters the except branch where the ``repo_type != "bucket"`` guard and
|
|
``get_safe_version`` live; the second call (after the bucket meta pull)
|
|
succeeds. ``_pull_from_repo`` is stubbed so the except path runs without a
|
|
network call. Without the raise, the try block would succeed and the guard
|
|
branch would never execute, so the assertion would pass vacuously.
|
|
"""
|
|
with (
|
|
patch("lerobot.datasets.dataset_metadata.get_safe_version") as mock_gsv,
|
|
patch.object(
|
|
LeRobotDatasetMetadata,
|
|
"_load_metadata",
|
|
side_effect=[FileNotFoundError, None],
|
|
),
|
|
patch.object(LeRobotDatasetMetadata, "_pull_from_repo") as mock_pull,
|
|
):
|
|
LeRobotDatasetMetadata(
|
|
DUMMY_REPO_ID,
|
|
root=tmp_path,
|
|
repo_type="bucket",
|
|
)
|
|
# The except branch ran (proven by the pull), but the bucket guard skipped
|
|
# version resolution.
|
|
mock_pull.assert_called_once()
|
|
mock_gsv.assert_not_called()
|
|
|
|
|
|
def test_bucket_metadata_sync_uses_stable_cache_and_token(tmp_path):
|
|
hub_cache = tmp_path / "hub"
|
|
token = "hf_test_token"
|
|
expected_root = hub_cache / f"buckets--{DUMMY_REPO_ID.replace('/', '--')}"
|
|
|
|
with (
|
|
patch("lerobot.datasets.dataset_metadata.HF_LEROBOT_HUB_CACHE", hub_cache),
|
|
patch("lerobot.datasets.dataset_metadata.sync_bucket") as mock_sync,
|
|
patch.object(
|
|
LeRobotDatasetMetadata,
|
|
"_load_metadata",
|
|
side_effect=[FileNotFoundError, None],
|
|
),
|
|
):
|
|
meta = LeRobotDatasetMetadata(DUMMY_REPO_ID, repo_type="bucket", token=token)
|
|
|
|
assert meta.root == expected_root
|
|
mock_sync.assert_called_once_with(
|
|
f"hf://buckets/{DUMMY_REPO_ID}/meta",
|
|
str(expected_root / "meta"),
|
|
delete=True,
|
|
quiet=True,
|
|
token=token,
|
|
)
|
|
|
|
with (
|
|
patch("lerobot.datasets.dataset_metadata.HF_LEROBOT_HUB_CACHE", hub_cache),
|
|
patch("lerobot.datasets.dataset_metadata.sync_bucket") as mock_sync,
|
|
patch.object(LeRobotDatasetMetadata, "_load_metadata"),
|
|
):
|
|
cached_meta = LeRobotDatasetMetadata(DUMMY_REPO_ID, repo_type="bucket", token=token)
|
|
|
|
assert cached_meta.root == expected_root
|
|
mock_sync.assert_not_called()
|
|
|
|
|
|
def test_repo_type_is_keyword_only_and_preserves_positional_episodes():
|
|
episodes = [1, 2]
|
|
with (
|
|
patch("lerobot.datasets.streaming_dataset.LeRobotDatasetMetadata", _fake_meta),
|
|
patch("lerobot.datasets.streaming_dataset.check_version_compatibility"),
|
|
patch(
|
|
"lerobot.datasets.streaming_dataset.load_dataset",
|
|
return_value=SimpleNamespace(num_shards=1),
|
|
),
|
|
):
|
|
dataset = StreamingLeRobotDataset(DUMMY_REPO_ID, None, episodes)
|
|
|
|
assert dataset.episodes == episodes
|
|
assert dataset.repo_type == "dataset"
|
|
|
|
|
|
def test_bucket_root_caches_metadata_without_switching_to_local_streaming(tmp_path):
|
|
with (
|
|
patch("lerobot.datasets.streaming_dataset.LeRobotDatasetMetadata", _fake_meta),
|
|
patch("lerobot.datasets.streaming_dataset.check_version_compatibility"),
|
|
patch(
|
|
"lerobot.datasets.streaming_dataset.load_dataset",
|
|
return_value=SimpleNamespace(num_shards=1),
|
|
) as mock_load_dataset,
|
|
):
|
|
dataset = StreamingLeRobotDataset(DUMMY_REPO_ID, root=tmp_path, repo_type="bucket")
|
|
|
|
assert dataset.root == tmp_path
|
|
assert not dataset.streaming_from_local
|
|
assert mock_load_dataset.call_args.args == ("parquet",)
|
|
assert mock_load_dataset.call_args.kwargs["data_files"].startswith("hf://buckets/")
|
|
|
|
|
|
def test_invalid_repo_type_fails_before_io():
|
|
with pytest.raises(ValueError, match="repo_type must be 'dataset' or 'bucket'"):
|
|
StreamingLeRobotDataset(DUMMY_REPO_ID, repo_type="space")
|