forked from MakazhanAlpamys/Soup
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_dataset_hub.py
More file actions
357 lines (287 loc) · 13 KB
/
Copy pathtest_dataset_hub.py
File metadata and controls
357 lines (287 loc) · 13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
"""Tests for HuggingFace Dataset Hub browser: search, preview, download."""
from __future__ import annotations
from unittest.mock import MagicMock, patch
from typer.testing import CliRunner
from soup_cli.commands.data import app
runner = CliRunner()
# ---------------------------------------------------------------------------
# soup data search
# ---------------------------------------------------------------------------
class TestDataSearch:
"""Tests for `soup data search` subcommand."""
@patch("soup_cli.commands.data.list_datasets")
def test_search_basic(self, mock_list):
"""Basic search returns results table."""
mock_ds = MagicMock()
mock_ds.id = "teknium/OpenHermes-2.5"
mock_ds.downloads = 50000
mock_ds.likes = 200
mock_ds.tags = ["en", "sft"]
mock_list.return_value = [mock_ds]
result = runner.invoke(app, ["search", "openhermes"])
assert result.exit_code == 0
assert "teknium/OpenHermes-2.5" in result.output
@patch("soup_cli.commands.data.list_datasets")
def test_search_no_results(self, mock_list):
"""Search with no results shows message."""
mock_list.return_value = []
result = runner.invoke(app, ["search", "nonexistent_dataset_xyz_123"])
assert result.exit_code == 0
assert "No datasets found" in result.output
@patch("soup_cli.commands.data.list_datasets")
def test_search_limit(self, mock_list):
"""--limit controls number of results."""
datasets = []
for idx in range(5):
mock_ds = MagicMock()
mock_ds.id = f"user/dataset-{idx}"
mock_ds.downloads = 100
mock_ds.likes = 10
mock_ds.tags = []
datasets.append(mock_ds)
mock_list.return_value = datasets
result = runner.invoke(app, ["search", "dataset", "--limit", "3"])
assert result.exit_code == 0
@patch("soup_cli.commands.data.list_datasets")
def test_search_sort_downloads(self, mock_list):
"""--sort downloads sorts by download count."""
mock_list.return_value = []
result = runner.invoke(
app, ["search", "code", "--sort", "downloads"]
)
assert result.exit_code == 0
mock_list.assert_called_once()
call_kwargs = mock_list.call_args
assert call_kwargs[1].get("sort") == "downloads"
@patch("soup_cli.commands.data.list_datasets")
def test_search_sort_likes(self, mock_list):
"""--sort likes sorts by likes."""
mock_list.return_value = []
result = runner.invoke(app, ["search", "code", "--sort", "likes"])
assert result.exit_code == 0
call_kwargs = mock_list.call_args
assert call_kwargs[1].get("sort") == "likes"
@patch("soup_cli.commands.data.list_datasets")
def test_search_sort_invalid(self, mock_list):
"""Invalid --sort value is rejected."""
result = runner.invoke(app, ["search", "code", "--sort", "invalid"])
assert result.exit_code != 0
@patch("soup_cli.commands.data.list_datasets")
def test_search_huggingface_hub_not_installed(self, mock_list):
"""Graceful error when huggingface_hub is missing."""
mock_list.side_effect = ImportError("No module named 'huggingface_hub'")
result = runner.invoke(app, ["search", "code"])
assert result.exit_code == 1
assert "huggingface" in result.output.lower()
# ---------------------------------------------------------------------------
# soup data preview
# ---------------------------------------------------------------------------
class TestDataPreview:
"""Tests for `soup data preview` subcommand."""
@patch("soup_cli.commands.data._hf_dataset_info")
def test_preview_basic(self, mock_info):
"""Preview shows dataset info table."""
mock_info.return_value = {
"id": "teknium/OpenHermes-2.5",
"description": "A large collection of instruction pairs.",
"downloads": 50000,
"likes": 200,
"size_bytes": 2_000_000_000,
"splits": {"train": 1_000_000, "test": 10_000},
"features": ["conversations"],
"tags": ["en", "sft"],
}
result = runner.invoke(app, ["preview", "teknium/OpenHermes-2.5"])
assert result.exit_code == 0
assert "teknium/OpenHermes-2.5" in result.output
@patch("soup_cli.commands.data._hf_dataset_info")
def test_preview_not_found(self, mock_info):
"""Preview of nonexistent dataset shows error."""
mock_info.side_effect = ValueError("Dataset not found")
result = runner.invoke(app, ["preview", "nonexistent/dataset"])
assert result.exit_code == 1
assert "not found" in result.output.lower() or "error" in result.output.lower()
@patch("soup_cli.commands.data._hf_dataset_info")
def test_preview_shows_splits(self, mock_info):
"""Preview shows split information."""
mock_info.return_value = {
"id": "test/ds",
"description": "Test",
"downloads": 100,
"likes": 5,
"size_bytes": 1000,
"splits": {"train": 500, "validation": 100},
"features": ["text"],
"tags": [],
}
result = runner.invoke(app, ["preview", "test/ds"])
assert result.exit_code == 0
assert "train" in result.output
@patch("soup_cli.commands.data._hf_dataset_info")
def test_preview_shows_features(self, mock_info):
"""Preview shows feature columns."""
mock_info.return_value = {
"id": "test/ds",
"description": "Test",
"downloads": 100,
"likes": 5,
"size_bytes": 1000,
"splits": {"train": 500},
"features": ["instruction", "output", "input"],
"tags": [],
}
result = runner.invoke(app, ["preview", "test/ds"])
assert result.exit_code == 0
# ---------------------------------------------------------------------------
# soup data download
# ---------------------------------------------------------------------------
class TestDataDownload:
"""Tests for `soup data download` subcommand."""
@patch("soup_cli.commands.data._hf_download_dataset")
def test_download_basic(self, mock_download, tmp_path, monkeypatch):
"""Basic download writes JSONL output."""
monkeypatch.chdir(tmp_path)
output_file = tmp_path / "data.jsonl"
mock_download.return_value = [
{"instruction": "What is 2+2?", "output": "4"},
{"instruction": "Hello", "output": "Hi there"},
]
result = runner.invoke(
app, ["download", "test/dataset", "-o", str(output_file)]
)
assert result.exit_code == 0
assert output_file.exists()
lines = output_file.read_text(encoding="utf-8").strip().split("\n")
assert len(lines) == 2
@patch("soup_cli.commands.data._hf_download_dataset")
def test_download_with_split(self, mock_download, tmp_path, monkeypatch):
"""--split flag is passed to download function."""
monkeypatch.chdir(tmp_path)
output_file = tmp_path / "data.jsonl"
mock_download.return_value = [{"text": "hello"}]
result = runner.invoke(
app,
["download", "test/dataset", "--split", "train[:100]", "-o", str(output_file)],
)
assert result.exit_code == 0
mock_download.assert_called_once()
call_kwargs = mock_download.call_args
assert call_kwargs[1].get("split") == "train[:100]"
@patch("soup_cli.commands.data._hf_download_dataset")
def test_download_with_format_conversion(self, mock_download, tmp_path, monkeypatch):
"""--format converts downloaded data."""
monkeypatch.chdir(tmp_path)
output_file = tmp_path / "data.jsonl"
# Alpaca format input
mock_download.return_value = [
{"instruction": "Q1", "input": "", "output": "A1"},
]
result = runner.invoke(
app,
[
"download", "test/dataset",
"--format", "sharegpt",
"-o", str(output_file),
],
)
assert result.exit_code == 0
assert output_file.exists()
@patch("soup_cli.commands.data._hf_download_dataset")
def test_download_empty_dataset(self, mock_download, tmp_path, monkeypatch):
"""Empty download result shows error."""
monkeypatch.chdir(tmp_path)
mock_download.return_value = []
output_file = tmp_path / "data.jsonl"
result = runner.invoke(
app, ["download", "test/empty", "-o", str(output_file)]
)
assert result.exit_code == 1
assert "empty" in result.output.lower() or "no data" in result.output.lower()
@patch("soup_cli.commands.data._hf_download_dataset")
def test_download_error_handling(self, mock_download, tmp_path):
"""Download failure shows friendly error."""
mock_download.side_effect = ValueError("Dataset not found on HuggingFace Hub")
output_file = tmp_path / "data.jsonl"
result = runner.invoke(
app, ["download", "nonexistent/dataset", "-o", str(output_file)]
)
assert result.exit_code == 1
@patch("soup_cli.commands.data._hf_download_dataset")
def test_download_output_path_traversal(self, mock_download, tmp_path):
"""Output path must stay under cwd."""
mock_download.return_value = [{"text": "hello"}]
result = runner.invoke(
app,
["download", "test/dataset", "-o", "/etc/passwd"],
)
assert result.exit_code == 1
assert "current working directory" in result.output.lower()
@patch("soup_cli.commands.data._hf_download_dataset")
def test_download_default_output(self, mock_download, tmp_path, monkeypatch):
"""Default output file uses dataset name."""
monkeypatch.chdir(tmp_path)
mock_download.return_value = [{"text": "hello"}]
result = runner.invoke(
app, ["download", "user/my-dataset"]
)
assert result.exit_code == 0
# Default should be my-dataset.jsonl in cwd
expected = tmp_path / "my-dataset.jsonl"
assert expected.exists()
@patch("soup_cli.commands.data._hf_download_dataset")
def test_download_samples_limit(self, mock_download, tmp_path, monkeypatch):
"""--samples limits number of downloaded rows."""
monkeypatch.chdir(tmp_path)
output_file = tmp_path / "data.jsonl"
mock_download.return_value = [
{"text": f"row {idx}"} for idx in range(10)
]
result = runner.invoke(
app, ["download", "test/dataset", "--samples", "5", "-o", str(output_file)]
)
assert result.exit_code == 0
lines = output_file.read_text(encoding="utf-8").strip().split("\n")
assert len(lines) == 5
# ---------------------------------------------------------------------------
# Helper function tests
# ---------------------------------------------------------------------------
class TestHelperFunctions:
"""Tests for internal helper functions."""
def test_format_size_bytes(self):
"""_format_size_bytes formats various sizes correctly."""
from soup_cli.commands.data import _format_size_bytes
assert _format_size_bytes(0) == "0 B"
assert _format_size_bytes(500) == "500 B"
assert "KB" in _format_size_bytes(1024)
assert "MB" in _format_size_bytes(1024 * 1024)
assert "GB" in _format_size_bytes(1024 * 1024 * 1024)
def test_format_size_bytes_none(self):
"""_format_size_bytes handles None."""
from soup_cli.commands.data import _format_size_bytes
assert _format_size_bytes(None) == "unknown"
def test_format_count(self):
"""_format_count formats large numbers."""
from soup_cli.commands.data import _format_count
assert _format_count(500) == "500"
assert "K" in _format_count(1500)
assert "M" in _format_count(1_500_000)
# ---------------------------------------------------------------------------
# Security edge cases
# ---------------------------------------------------------------------------
class TestSecurityEdgeCases:
"""Security-relevant edge case tests."""
def test_download_samples_over_limit(self):
"""--samples above 1M is rejected."""
result = runner.invoke(
app,
["download", "test/dataset", "--samples", "2000000", "-o", "out.jsonl"],
)
assert result.exit_code == 1
assert "1,000,000" in result.output
@patch("soup_cli.commands.data._hf_dataset_info")
def test_preview_huggingface_hub_not_installed(self, mock_info):
"""Preview gracefully handles missing huggingface_hub."""
mock_info.side_effect = ImportError("No module named 'huggingface_hub'")
result = runner.invoke(app, ["preview", "test/ds"])
assert result.exit_code == 1
assert "huggingface" in result.output.lower()