Skip to content

Commit e46ec09

Browse files
authored
Mirror source layout in tests (#4426)
* Mirror source layout in API and plugin tests * Mirror source layout in server and CLI tests * Name pipeline package tests test_pipeline.py
1 parent d736e86 commit e46ec09

45 files changed

Lines changed: 1438 additions & 1449 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎src/tests/_internal/cli/commands/test_dstack.py‎

Lines changed: 0 additions & 19 deletions
This file was deleted.

src/tests/_internal/cli/services/configurators/test_profile.py renamed to src/tests/_internal/cli/services/test_profile.py

File renamed without changes.

‎src/tests/_internal/cli/test_main.py‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,31 @@
11
import signal
22
import subprocess
33
import sys
4+
from pathlib import Path
45
from textwrap import dedent
56
from unittest.mock import Mock
67

78
import pytest
9+
from pytest import CaptureFixture
810

911
import dstack._internal.cli.main as main_module
1012
from dstack._internal.compat import IS_WINDOWS
1113
from tests._internal.cli.common import run_dstack_cli
1214

1315

1416
class TestMain:
17+
def test_prints_help_and_exists_with_0_exit_code(self, capsys: CaptureFixture):
18+
exit_code = run_dstack_cli([])
19+
assert exit_code == 0
20+
assert capsys.readouterr().out.startswith("Usage: dstack [-h] [-v] COMMAND ...")
21+
22+
def test_prints_errors_to_stderr(self, tmp_path: Path, capsys: CaptureFixture):
23+
exit_code = run_dstack_cli(["ps", "--nonsense"], home_dir=tmp_path)
24+
assert exit_code == 1
25+
captured = capsys.readouterr()
26+
assert captured.out == ""
27+
assert "Unrecognized arguments: --nonsense" in captured.err
28+
1529
def test_reraises_keyboard_interrupt(self, monkeypatch: pytest.MonkeyPatch, capsys):
1630
monkeypatch.setattr(sys, "excepthook", sys.excepthook)
1731
monkeypatch.setattr(main_module, "check_for_updates", Mock(side_effect=KeyboardInterrupt))

‎src/tests/_internal/core/backends/lium/test_compute.py‎

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import subprocess
55
from datetime import datetime, timedelta, timezone
66
from types import SimpleNamespace
7+
from typing import Union, cast
78
from unittest.mock import Mock, patch
89

910
import gpuhunt
@@ -19,6 +20,7 @@
1920
_get_bootstrap_command,
2021
_get_image_env_and_cwd_command,
2122
_get_provider_ssh,
23+
_get_volume_commands,
2224
)
2325
from dstack._internal.core.backends.lium.models import LiumConfig, LiumCreds
2426
from dstack._internal.core.errors import (
@@ -31,8 +33,10 @@
3133
from dstack._internal.core.models.common import RegistryAuth
3234
from dstack._internal.core.models.instances import InstanceAvailability, InstanceRuntime
3335
from dstack._internal.core.models.resources import ResourcesSpec
34-
from dstack._internal.core.models.runs import JobProvisioningData, Requirements
36+
from dstack._internal.core.models.runs import Job, JobProvisioningData, Requirements
3537
from dstack._internal.core.models.unix import UnixUser
38+
from dstack._internal.core.models.volumes import LiumVolumeConfiguration, VolumeMountPoint
39+
from dstack._internal.server.testing.common import get_volume
3640
from dstack._internal.utils.crypto import generate_rsa_key_pair_bytes
3741

3842
COMPUTE_MODULE = "dstack._internal.core.backends.lium.compute"
@@ -676,3 +680,94 @@ def test_oversized_environment_fails_without_starting_partial_runner(self, tmp_p
676680

677681
assert result.returncode != 0
678682
assert result.stdout == b""
683+
684+
685+
class TestVolumeLifecycle:
686+
def test_create_unbounded_volume(self, compute):
687+
compute.api_client.create_volume.return_value = {"id": "volume-id"}
688+
with patch(f"{COMPUTE_MODULE}.generate_unique_volume_name", return_value="unique-name"):
689+
data = compute.create_volume(Mock())
690+
691+
compute.api_client.create_volume.assert_called_once_with("unique-name")
692+
assert data.volume_id == "volume-id"
693+
assert data.size_gb is None
694+
assert data.price is None
695+
assert not data.attachable
696+
assert not data.detachable
697+
698+
def test_recovers_lost_create_response_by_unique_name(self, compute):
699+
compute.api_client.create_volume.side_effect = LiumAPIError("timed out")
700+
compute.api_client.get_volumes.return_value = [
701+
{"id": "other-id", "name": "other"},
702+
{"id": "volume-id", "name": "unique-name"},
703+
]
704+
with patch(f"{COMPUTE_MODULE}.generate_unique_volume_name", return_value="unique-name"):
705+
data = compute.create_volume(Mock())
706+
707+
assert data.volume_id == "volume-id"
708+
assert compute.api_client.create_volume.call_count == 1
709+
710+
def test_register_existing_volume(self, compute):
711+
volume = SimpleNamespace(configuration=LiumVolumeConfiguration(volume_id="existing"))
712+
compute.api_client.get_volume.return_value = {"id": "existing"}
713+
714+
data = compute.register_volume(volume)
715+
716+
assert data.volume_id == "existing"
717+
compute.api_client.create_volume.assert_not_called()
718+
719+
def test_register_missing_volume(self, compute):
720+
compute.api_client.get_volume.return_value = None
721+
with pytest.raises(ComputeError, match="not found"):
722+
compute.register_volume(
723+
SimpleNamespace(configuration=LiumVolumeConfiguration(volume_id="missing"))
724+
)
725+
726+
def test_delete_volume(self, compute):
727+
compute.delete_volume(SimpleNamespace(volume_id="volume-id"))
728+
compute.api_client.delete_volume.assert_called_once_with("volume-id")
729+
730+
731+
class TestVolumeMount:
732+
@pytest.mark.parametrize(
733+
"user",
734+
[None, UnixUser(uid=10001), UnixUser(username="app")],
735+
)
736+
def test_custom_path(self, user):
737+
commands = _volume_commands(user=user, path="/checkpoints")
738+
739+
assert commands[0] == "grep -q ' /mnt .* - fuse.s3fs ' /proc/self/mountinfo"
740+
assert commands[1:] == ["mkdir -p -- /checkpoints", "mount --bind /mnt /checkpoints"]
741+
742+
def test_native_mount_path_needs_no_bind(self):
743+
assert len(_volume_commands(path="/mnt")) == 1
744+
745+
def test_quotes_mount_path(self):
746+
commands = _volume_commands(path="/checkpoint dir; echo unsafe")
747+
assert commands[-1] == "mount --bind /mnt '/checkpoint dir; echo unsafe'"
748+
749+
@pytest.mark.parametrize("path", ["/", "/root", "/root/.dstack-lium", "/proc/a", "/mnt/a"])
750+
def test_rejects_paths_that_hide_bootstrap_or_nest_source(self, path):
751+
with pytest.raises(ComputeError, match="cannot be mounted"):
752+
_volume_commands(path=path)
753+
754+
def test_two_volumes_rejected(self):
755+
with pytest.raises(ComputeError, match="only one"):
756+
_get_volume_commands(Mock(), Mock(), [Mock(), Mock()])
757+
758+
def test_selects_volume_from_alternatives(self):
759+
assert _volume_commands(name=["elsewhere", "cache"], path="/data")[-1] == (
760+
"mount --bind /mnt /data"
761+
)
762+
763+
764+
def _volume_commands(user=None, path="/checkpoints", name: Union[str, list[str]] = "cache"):
765+
# A real `Job` needs a full `JobSpec`, while `_get_volume_commands()` only reads the user and
766+
# the mount points
767+
job = cast(
768+
Job,
769+
SimpleNamespace(
770+
job_spec=SimpleNamespace(user=user, volumes=[VolumeMountPoint(name=name, path=path)])
771+
),
772+
)
773+
return _get_volume_commands(Mock(), job, [get_volume(name="cache")])

‎src/tests/_internal/core/backends/lium/test_volumes.py‎

Lines changed: 0 additions & 114 deletions
This file was deleted.
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from typing import Optional
2+
3+
import pytest
4+
5+
from dstack._internal.core.compatibility.gpus import get_list_gpus_excludes
6+
from dstack._internal.server.schemas.gpus import ListGpusRequest
7+
from dstack._internal.server.testing.common import get_run_spec
8+
9+
10+
class TestGetListGpusExcludes:
11+
@pytest.mark.parametrize("reverse", [None, False, True])
12+
def test_omits_disabled_reverse(self, reverse: Optional[bool]):
13+
kwargs = {} if reverse is None else {"reverse": reverse}
14+
request = ListGpusRequest.model_validate(
15+
{"run_spec": get_run_spec(repo_id="test-repo"), **kwargs}
16+
)
17+
18+
body = request.model_dump(exclude=get_list_gpus_excludes(request))
19+
20+
if reverse:
21+
assert body["reverse"] is True
22+
else:
23+
assert "reverse" not in body
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from typing import Optional
2+
3+
import pytest
4+
5+
from dstack._internal.core.compatibility.runs import get_get_plan_excludes
6+
from dstack._internal.server.schemas.runs import GetRunPlanRequest
7+
from dstack._internal.server.testing.common import get_run_spec
8+
9+
10+
class TestGetGetPlanExcludes:
11+
@pytest.mark.parametrize("reverse_offers", [None, False, True])
12+
def test_omits_disabled_reverse_offers(self, reverse_offers: Optional[bool]):
13+
kwargs = {} if reverse_offers is None else {"reverse_offers": reverse_offers}
14+
request = GetRunPlanRequest.model_validate(
15+
{"run_spec": get_run_spec(repo_id="test-repo"), **kwargs}
16+
)
17+
18+
body = request.model_dump(exclude=get_get_plan_excludes(request))
19+
20+
if reverse_offers:
21+
assert body["reverse_offers"] is True
22+
else:
23+
assert "reverse_offers" not in body

src/tests/_internal/server/background/pipeline_tasks/test_instances/__init__.py renamed to src/tests/_internal/server/background/pipeline_tasks/instances/__init__.py

File renamed without changes.

src/tests/_internal/server/background/pipeline_tasks/test_instances/conftest.py renamed to src/tests/_internal/server/background/pipeline_tasks/instances/conftest.py

File renamed without changes.

src/tests/_internal/server/background/pipeline_tasks/test_instances/helpers.py renamed to src/tests/_internal/server/background/pipeline_tasks/instances/helpers.py

File renamed without changes.

0 commit comments

Comments
 (0)