dstack 0.18.40rc1__py3-none-any.whl → 0.18.41__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (98) hide show
  1. dstack/_internal/cli/commands/apply.py +8 -5
  2. dstack/_internal/cli/services/configurators/base.py +4 -2
  3. dstack/_internal/cli/services/configurators/fleet.py +21 -9
  4. dstack/_internal/cli/services/configurators/gateway.py +15 -0
  5. dstack/_internal/cli/services/configurators/run.py +6 -5
  6. dstack/_internal/cli/services/configurators/volume.py +15 -0
  7. dstack/_internal/cli/services/repos.py +3 -3
  8. dstack/_internal/cli/utils/fleet.py +44 -33
  9. dstack/_internal/cli/utils/run.py +27 -7
  10. dstack/_internal/cli/utils/volume.py +21 -9
  11. dstack/_internal/core/backends/aws/compute.py +92 -52
  12. dstack/_internal/core/backends/aws/resources.py +22 -12
  13. dstack/_internal/core/backends/azure/compute.py +2 -0
  14. dstack/_internal/core/backends/base/compute.py +20 -2
  15. dstack/_internal/core/backends/gcp/compute.py +30 -23
  16. dstack/_internal/core/backends/gcp/resources.py +0 -15
  17. dstack/_internal/core/backends/oci/compute.py +10 -5
  18. dstack/_internal/core/backends/oci/resources.py +23 -26
  19. dstack/_internal/core/backends/remote/provisioning.py +65 -27
  20. dstack/_internal/core/backends/runpod/compute.py +1 -0
  21. dstack/_internal/core/models/backends/azure.py +3 -1
  22. dstack/_internal/core/models/configurations.py +24 -1
  23. dstack/_internal/core/models/fleets.py +46 -0
  24. dstack/_internal/core/models/instances.py +5 -1
  25. dstack/_internal/core/models/pools.py +4 -1
  26. dstack/_internal/core/models/profiles.py +10 -4
  27. dstack/_internal/core/models/runs.py +20 -0
  28. dstack/_internal/core/models/volumes.py +3 -0
  29. dstack/_internal/core/services/ssh/attach.py +92 -53
  30. dstack/_internal/core/services/ssh/tunnel.py +58 -31
  31. dstack/_internal/proxy/gateway/routers/registry.py +2 -0
  32. dstack/_internal/proxy/gateway/schemas/registry.py +2 -0
  33. dstack/_internal/proxy/gateway/services/registry.py +4 -0
  34. dstack/_internal/proxy/lib/models.py +3 -0
  35. dstack/_internal/proxy/lib/services/service_connection.py +8 -1
  36. dstack/_internal/server/background/tasks/process_instances.py +72 -33
  37. dstack/_internal/server/background/tasks/process_metrics.py +9 -9
  38. dstack/_internal/server/background/tasks/process_running_jobs.py +73 -26
  39. dstack/_internal/server/background/tasks/process_runs.py +2 -12
  40. dstack/_internal/server/background/tasks/process_submitted_jobs.py +109 -42
  41. dstack/_internal/server/background/tasks/process_terminating_jobs.py +1 -1
  42. dstack/_internal/server/migrations/versions/1338b788b612_reverse_job_instance_relationship.py +71 -0
  43. dstack/_internal/server/migrations/versions/1e76fb0dde87_add_jobmodel_inactivity_secs.py +32 -0
  44. dstack/_internal/server/migrations/versions/51d45659d574_add_instancemodel_blocks_fields.py +43 -0
  45. dstack/_internal/server/migrations/versions/63c3f19cb184_add_jobterminationreason_inactivity_.py +83 -0
  46. dstack/_internal/server/models.py +10 -4
  47. dstack/_internal/server/routers/runs.py +1 -0
  48. dstack/_internal/server/schemas/runner.py +1 -0
  49. dstack/_internal/server/services/backends/configurators/azure.py +34 -8
  50. dstack/_internal/server/services/config.py +9 -0
  51. dstack/_internal/server/services/fleets.py +27 -2
  52. dstack/_internal/server/services/gateways/client.py +9 -1
  53. dstack/_internal/server/services/jobs/__init__.py +215 -43
  54. dstack/_internal/server/services/jobs/configurators/base.py +47 -2
  55. dstack/_internal/server/services/offers.py +91 -5
  56. dstack/_internal/server/services/pools.py +95 -11
  57. dstack/_internal/server/services/proxy/repo.py +17 -3
  58. dstack/_internal/server/services/runner/client.py +1 -1
  59. dstack/_internal/server/services/runner/ssh.py +33 -5
  60. dstack/_internal/server/services/runs.py +48 -179
  61. dstack/_internal/server/services/services/__init__.py +9 -1
  62. dstack/_internal/server/statics/index.html +1 -1
  63. dstack/_internal/server/statics/{main-11ec5e4a00ea6ec833e3.js → main-2ac66bfcbd2e39830b88.js} +30 -31
  64. dstack/_internal/server/statics/{main-11ec5e4a00ea6ec833e3.js.map → main-2ac66bfcbd2e39830b88.js.map} +1 -1
  65. dstack/_internal/server/statics/{main-fc56d1f4af8e57522a1c.css → main-ad5150a441de98cd8987.css} +1 -1
  66. dstack/_internal/server/testing/common.py +117 -52
  67. dstack/_internal/utils/common.py +22 -8
  68. dstack/_internal/utils/env.py +14 -0
  69. dstack/_internal/utils/ssh.py +1 -1
  70. dstack/api/server/_fleets.py +25 -1
  71. dstack/api/server/_runs.py +23 -2
  72. dstack/api/server/_volumes.py +12 -1
  73. dstack/version.py +1 -1
  74. {dstack-0.18.40rc1.dist-info → dstack-0.18.41.dist-info}/METADATA +1 -1
  75. {dstack-0.18.40rc1.dist-info → dstack-0.18.41.dist-info}/RECORD +98 -89
  76. tests/_internal/cli/services/configurators/test_profile.py +3 -3
  77. tests/_internal/core/services/ssh/test_tunnel.py +56 -4
  78. tests/_internal/proxy/gateway/routers/test_registry.py +30 -7
  79. tests/_internal/server/background/tasks/test_process_instances.py +138 -20
  80. tests/_internal/server/background/tasks/test_process_metrics.py +12 -0
  81. tests/_internal/server/background/tasks/test_process_running_jobs.py +192 -0
  82. tests/_internal/server/background/tasks/test_process_runs.py +27 -3
  83. tests/_internal/server/background/tasks/test_process_submitted_jobs.py +48 -3
  84. tests/_internal/server/background/tasks/test_process_terminating_jobs.py +126 -13
  85. tests/_internal/server/routers/test_fleets.py +15 -2
  86. tests/_internal/server/routers/test_pools.py +6 -0
  87. tests/_internal/server/routers/test_runs.py +27 -0
  88. tests/_internal/server/services/jobs/__init__.py +0 -0
  89. tests/_internal/server/services/jobs/configurators/__init__.py +0 -0
  90. tests/_internal/server/services/jobs/configurators/test_base.py +72 -0
  91. tests/_internal/server/services/test_pools.py +4 -0
  92. tests/_internal/server/services/test_runs.py +5 -41
  93. tests/_internal/utils/test_common.py +21 -0
  94. tests/_internal/utils/test_env.py +38 -0
  95. {dstack-0.18.40rc1.dist-info → dstack-0.18.41.dist-info}/LICENSE.md +0 -0
  96. {dstack-0.18.40rc1.dist-info → dstack-0.18.41.dist-info}/WHEEL +0 -0
  97. {dstack-0.18.40rc1.dist-info → dstack-0.18.41.dist-info}/entry_points.txt +0 -0
  98. {dstack-0.18.40rc1.dist-info → dstack-0.18.41.dist-info}/top_level.txt +0 -0
@@ -340,6 +340,7 @@ class TestCreateFleet:
340
340
  },
341
341
  "backends": None,
342
342
  "regions": None,
343
+ "availability_zones": None,
343
344
  "instance_types": None,
344
345
  "spot_policy": None,
345
346
  "retry": None,
@@ -350,10 +351,12 @@ class TestCreateFleet:
350
351
  "type": "fleet",
351
352
  "name": "test-fleet",
352
353
  "reservation": None,
354
+ "blocks": 1,
353
355
  },
354
356
  "profile": {
355
357
  "backends": None,
356
358
  "regions": None,
359
+ "availability_zones": None,
357
360
  "instance_types": None,
358
361
  "spot_policy": None,
359
362
  "retry": None,
@@ -393,15 +396,18 @@ class TestCreateFleet:
393
396
  "pool_name": None,
394
397
  "backend": None,
395
398
  "region": None,
399
+ "availability_zone": None,
396
400
  "instance_type": None,
397
401
  "price": None,
402
+ "total_blocks": 1,
403
+ "busy_blocks": 0,
398
404
  }
399
405
  ],
400
406
  }
401
407
  res = await session.execute(select(FleetModel))
402
408
  assert res.scalar_one()
403
409
  res = await session.execute(select(InstanceModel))
404
- assert res.scalar_one()
410
+ assert res.unique().scalar_one()
405
411
 
406
412
  @pytest.mark.asyncio
407
413
  @pytest.mark.parametrize("test_db", ["sqlite", "postgres"], indirect=True)
@@ -444,6 +450,7 @@ class TestCreateFleet:
444
450
  "port": None,
445
451
  "identity_file": None,
446
452
  "ssh_key": None, # should not return ssh_key
453
+ "proxy_jump": None,
447
454
  "hosts": ["1.1.1.1"],
448
455
  "network": None,
449
456
  },
@@ -458,6 +465,7 @@ class TestCreateFleet:
458
465
  },
459
466
  "backends": None,
460
467
  "regions": None,
468
+ "availability_zones": None,
461
469
  "instance_types": None,
462
470
  "spot_policy": None,
463
471
  "retry": None,
@@ -468,10 +476,12 @@ class TestCreateFleet:
468
476
  "type": "fleet",
469
477
  "name": spec.configuration.name,
470
478
  "reservation": None,
479
+ "blocks": 1,
471
480
  },
472
481
  "profile": {
473
482
  "backends": None,
474
483
  "regions": None,
484
+ "availability_zones": None,
475
485
  "instance_types": None,
476
486
  "spot_policy": None,
477
487
  "retry": None,
@@ -522,14 +532,17 @@ class TestCreateFleet:
522
532
  "termination_reason": None,
523
533
  "created": "2023-01-02T03:04:00+00:00",
524
534
  "region": "remote",
535
+ "availability_zone": None,
525
536
  "price": 0.0,
537
+ "total_blocks": 1,
538
+ "busy_blocks": 0,
526
539
  }
527
540
  ],
528
541
  }
529
542
  res = await session.execute(select(FleetModel))
530
543
  assert res.scalar_one()
531
544
  res = await session.execute(select(InstanceModel))
532
- instance = res.scalar_one()
545
+ instance = res.unique().scalar_one()
533
546
  assert instance.remote_connection_info is not None
534
547
 
535
548
  @pytest.mark.asyncio
@@ -332,7 +332,10 @@ class TestShowPool:
332
332
  "created": "2023-01-02T03:04:00+00:00",
333
333
  "pool_name": None,
334
334
  "region": "en",
335
+ "availability_zone": None,
335
336
  "price": 1,
337
+ "total_blocks": 1,
338
+ "busy_blocks": 0,
336
339
  }
337
340
  ],
338
341
  }
@@ -503,7 +506,10 @@ class TestRemoveInstance:
503
506
  "created": "2023-01-02T03:04:00+00:00",
504
507
  "pool_name": None,
505
508
  "region": "en",
509
+ "availability_zone": None,
506
510
  "price": 1,
511
+ "total_blocks": 1,
512
+ "busy_blocks": 0,
507
513
  }
508
514
  ],
509
515
  }
@@ -18,6 +18,7 @@ from dstack._internal.core.models.gateways import GatewayStatus
18
18
  from dstack._internal.core.models.instances import (
19
19
  InstanceAvailability,
20
20
  InstanceOfferWithAvailability,
21
+ InstanceStatus,
21
22
  InstanceType,
22
23
  Resources,
23
24
  )
@@ -47,7 +48,9 @@ from dstack._internal.server.testing.common import (
47
48
  create_backend,
48
49
  create_gateway,
49
50
  create_gateway_compute,
51
+ create_instance,
50
52
  create_job,
53
+ create_pool,
51
54
  create_project,
52
55
  create_repo,
53
56
  create_run,
@@ -85,6 +88,7 @@ def get_dev_env_run_plan_dict(
85
88
  "working_dir": None,
86
89
  "home_dir": "/root",
87
90
  "ide": "vscode",
91
+ "inactivity_duration": None,
88
92
  "version": None,
89
93
  "image": None,
90
94
  "user": None,
@@ -107,6 +111,7 @@ def get_dev_env_run_plan_dict(
107
111
  "volumes": [json.loads(v.json()) for v in volumes],
108
112
  "backends": ["local", "aws", "azure", "gcp", "lambda", "runpod"],
109
113
  "regions": ["us"],
114
+ "availability_zones": None,
110
115
  "instance_types": None,
111
116
  "creation_policy": None,
112
117
  "instance_name": None,
@@ -127,6 +132,7 @@ def get_dev_env_run_plan_dict(
127
132
  "profile": {
128
133
  "backends": ["local", "aws", "azure", "gcp", "lambda", "runpod"],
129
134
  "regions": ["us"],
135
+ "availability_zones": None,
130
136
  "instance_types": None,
131
137
  "creation_policy": None,
132
138
  "default": False,
@@ -198,6 +204,7 @@ def get_dev_env_run_plan_dict(
198
204
  "reservation": None,
199
205
  },
200
206
  "retry": None,
207
+ "volumes": volumes,
201
208
  "retry_policy": {"retry": False, "duration": None},
202
209
  "working_dir": ".",
203
210
  },
@@ -238,6 +245,7 @@ def get_dev_env_run_dict(
238
245
  "home_dir": "/root",
239
246
  "working_dir": None,
240
247
  "ide": "vscode",
248
+ "inactivity_duration": None,
241
249
  "version": None,
242
250
  "image": None,
243
251
  "user": None,
@@ -260,6 +268,7 @@ def get_dev_env_run_dict(
260
268
  "volumes": [],
261
269
  "backends": ["local", "aws", "azure", "gcp", "lambda"],
262
270
  "regions": ["us"],
271
+ "availability_zones": None,
263
272
  "instance_types": None,
264
273
  "creation_policy": None,
265
274
  "instance_name": None,
@@ -280,6 +289,7 @@ def get_dev_env_run_dict(
280
289
  "profile": {
281
290
  "backends": ["local", "aws", "azure", "gcp", "lambda"],
282
291
  "regions": ["us"],
292
+ "availability_zones": None,
283
293
  "instance_types": None,
284
294
  "creation_policy": None,
285
295
  "default": False,
@@ -351,6 +361,7 @@ def get_dev_env_run_dict(
351
361
  "reservation": None,
352
362
  },
353
363
  "retry": None,
364
+ "volumes": [],
354
365
  "retry_policy": {"retry": False, "duration": None},
355
366
  "working_dir": ".",
356
367
  },
@@ -361,6 +372,7 @@ def get_dev_env_run_dict(
361
372
  "submitted_at": submitted_at,
362
373
  "last_processed_at": last_processed_at,
363
374
  "finished_at": finished_at,
375
+ "inactivity_secs": None,
364
376
  "status": "submitted",
365
377
  "termination_reason": None,
366
378
  "termination_reason_message": None,
@@ -375,6 +387,7 @@ def get_dev_env_run_dict(
375
387
  "submission_num": 0,
376
388
  "submitted_at": submitted_at,
377
389
  "last_processed_at": last_processed_at,
390
+ "inactivity_secs": None,
378
391
  "finished_at": finished_at,
379
392
  "status": "submitted",
380
393
  "termination_reason": None,
@@ -487,6 +500,7 @@ class TestListRuns:
487
500
  "submitted_at": run1_submitted_at.isoformat(),
488
501
  "last_processed_at": run1_submitted_at.isoformat(),
489
502
  "finished_at": None,
503
+ "inactivity_secs": None,
490
504
  "status": "submitted",
491
505
  "termination_reason": None,
492
506
  "termination_reason_message": None,
@@ -502,6 +516,7 @@ class TestListRuns:
502
516
  "submitted_at": run1_submitted_at.isoformat(),
503
517
  "last_processed_at": run1_submitted_at.isoformat(),
504
518
  "finished_at": None,
519
+ "inactivity_secs": None,
505
520
  "status": "submitted",
506
521
  "termination_reason_message": None,
507
522
  "termination_reason": None,
@@ -1303,11 +1318,20 @@ class TestStopRuns:
1303
1318
  user=user,
1304
1319
  status=RunStatus.RUNNING,
1305
1320
  )
1321
+ pool = await create_pool(session=session, project=project)
1322
+ instance = await create_instance(
1323
+ session=session,
1324
+ project=project,
1325
+ pool=pool,
1326
+ status=InstanceStatus.BUSY,
1327
+ )
1306
1328
  job = await create_job(
1307
1329
  session=session,
1308
1330
  run=run,
1309
1331
  job_provisioning_data=get_job_provisioning_data(),
1310
1332
  status=JobStatus.RUNNING,
1333
+ instance=instance,
1334
+ instance_assigned=True,
1311
1335
  )
1312
1336
  with patch("dstack._internal.server.services.jobs._stop_runner") as stop_runner:
1313
1337
  response = await client.post(
@@ -1533,7 +1557,10 @@ class TestCreateInstance:
1533
1557
  "created": result["created"],
1534
1558
  "pool_name": None,
1535
1559
  "region": None,
1560
+ "availability_zone": None,
1536
1561
  "price": None,
1562
+ "total_blocks": 1,
1563
+ "busy_blocks": 0,
1537
1564
  }
1538
1565
  assert result == expected
1539
1566
 
File without changes
@@ -0,0 +1,72 @@
1
+ from typing import Union
2
+
3
+ import pytest
4
+
5
+ from dstack._internal.core.errors import ServerClientError
6
+ from dstack._internal.core.models.volumes import InstanceMountPoint, MountPoint, VolumeMountPoint
7
+ from dstack._internal.server.services.jobs.configurators.base import interpolate_job_volumes
8
+
9
+
10
+ class TestInterpolateJobVolumes:
11
+ @pytest.mark.parametrize(
12
+ ["run_volumes", "job_num", "job_volumes"],
13
+ [
14
+ pytest.param(
15
+ [VolumeMountPoint(name="volume", path="/volume")],
16
+ 0,
17
+ [VolumeMountPoint(name=["volume"], path="/volume")],
18
+ id="no_interpolation",
19
+ ),
20
+ pytest.param(
21
+ [InstanceMountPoint(instance_path="/volume", path="/volume")],
22
+ 0,
23
+ [InstanceMountPoint(instance_path="/volume", path="/volume")],
24
+ id="instance_mount",
25
+ ),
26
+ pytest.param(
27
+ [
28
+ VolumeMountPoint(
29
+ name="job${{dstack.job_num}}-rank${{dstack.node_rank}}", path="/volume"
30
+ )
31
+ ],
32
+ 2,
33
+ [VolumeMountPoint(name=["job2-rank2"], path="/volume")],
34
+ id="job_num_and_node_rank",
35
+ ),
36
+ ],
37
+ )
38
+ def test_interpolates_volumes(
39
+ self,
40
+ run_volumes: list[Union[MountPoint, str]],
41
+ job_num: int,
42
+ job_volumes: list[MountPoint],
43
+ ):
44
+ assert interpolate_job_volumes(run_volumes, job_num) == job_volumes
45
+
46
+ @pytest.mark.parametrize(
47
+ ["run_volumes", "job_num"],
48
+ [
49
+ pytest.param(
50
+ [VolumeMountPoint(name="${{}", path="/volume")],
51
+ 0,
52
+ id="invalid_syntax",
53
+ ),
54
+ pytest.param(
55
+ [VolumeMountPoint(name="${{ unknown.namespace }}", path="/volume")],
56
+ 0,
57
+ id="unknown_namespace",
58
+ ),
59
+ pytest.param(
60
+ [VolumeMountPoint(name="${{ dstack.var }}", path="/volume")],
61
+ 0,
62
+ id="unknown_var",
63
+ ),
64
+ ],
65
+ )
66
+ def test_raises_server_client_error(
67
+ self,
68
+ run_volumes: list[Union[MountPoint, str]],
69
+ job_num: int,
70
+ ):
71
+ with pytest.raises(ServerClientError):
72
+ assert interpolate_job_volumes(run_volumes, job_num)
@@ -66,6 +66,8 @@ class TestInstanceModelToInstance:
66
66
  created=created,
67
67
  region="eu-west-1",
68
68
  price=1.0,
69
+ total_blocks=1,
70
+ busy_blocks=0,
69
71
  )
70
72
  im = InstanceModel(
71
73
  id=instance_id,
@@ -78,6 +80,8 @@ class TestInstanceModelToInstance:
78
80
  pool=None,
79
81
  job_provisioning_data='{"ssh_proxy":null, "backend":"local","hostname":"hostname_test","region":"eu-west","price":1.0,"username":"user1","ssh_port":12345,"dockerized":false,"instance_id":"test_instance","instance_type": {"name": "instance", "resources": {"cpus": 1, "memory_mib": 512, "gpus": [], "spot": false, "disk": {"size_mib": 102400}, "description":""}}}',
80
82
  offer='{"price":"LOCAL", "price":1.0, "backend":"local", "region":"eu-west-1", "availability":"available","instance": {"name": "instance", "resources": {"cpus": 1, "memory_mib": 512, "gpus": [], "spot": false, "disk": {"size_mib": 102400}, "description":""}}}',
83
+ total_blocks=1,
84
+ busy_blocks=0,
81
85
  )
82
86
  instance = services_pools.instance_model_to_instance(im)
83
87
  assert instance == expected_instance
@@ -10,9 +10,9 @@ from dstack._internal.core.models.configurations import ScalingSpec, ServiceConf
10
10
  from dstack._internal.core.models.profiles import Profile
11
11
  from dstack._internal.core.models.resources import Range
12
12
  from dstack._internal.core.models.runs import JobStatus, JobTerminationReason, RunStatus
13
- from dstack._internal.core.models.volumes import VolumeMountPoint
14
13
  from dstack._internal.server.models import RunModel
15
- from dstack._internal.server.services.runs import check_can_attach_run_volumes, scale_run_replicas
14
+ from dstack._internal.server.services.jobs import check_can_attach_job_volumes
15
+ from dstack._internal.server.services.runs import scale_run_replicas
16
16
  from dstack._internal.server.testing.common import (
17
17
  create_job,
18
18
  create_pool,
@@ -241,19 +241,7 @@ class TestCanAttachRunVolumes:
241
241
  vol22.configuration.backend = BackendType.AWS
242
242
  vol22.configuration.region = "eu-west-2"
243
243
  volumes = [[vol11, vol12], [vol21, vol22]]
244
- run_spec = get_run_spec(
245
- run_name="test_run",
246
- repo_id="test_repo",
247
- configuration=ServiceConfiguration(
248
- port=80,
249
- commands=[""],
250
- volumes=[
251
- VolumeMountPoint(name=["vol11", "vol12"], path="/vol1"),
252
- VolumeMountPoint(name=["vol21", "vol22"], path="/vol2"),
253
- ],
254
- ),
255
- )
256
- check_can_attach_run_volumes(run_spec, volumes)
244
+ check_can_attach_job_volumes(volumes)
257
245
 
258
246
  @pytest.mark.asyncio
259
247
  async def test_cannot_attach_different_mount_points_with_different_backends_regions(self):
@@ -264,20 +252,8 @@ class TestCanAttachRunVolumes:
264
252
  vol2.configuration.backend = BackendType.AWS
265
253
  vol2.configuration.region = "eu-west-2"
266
254
  volumes = [[vol1], [vol2]]
267
- run_spec = get_run_spec(
268
- run_name="test_run",
269
- repo_id="test_repo",
270
- configuration=ServiceConfiguration(
271
- port=80,
272
- commands=[""],
273
- volumes=[
274
- VolumeMountPoint(name=["vol1"], path="/vol1"),
275
- VolumeMountPoint(name=["vol2"], path="/vol2"),
276
- ],
277
- ),
278
- )
279
255
  with pytest.raises(ServerClientError):
280
- check_can_attach_run_volumes(run_spec, volumes)
256
+ check_can_attach_job_volumes(volumes)
281
257
 
282
258
  @pytest.mark.asyncio
283
259
  async def test_cannot_attach_same_volume_at_different_mount_points(self):
@@ -285,17 +261,5 @@ class TestCanAttachRunVolumes:
285
261
  vol1.configuration.backend = BackendType.AWS
286
262
  vol1.configuration.region = "eu-west-1"
287
263
  volumes = [[vol1], [vol1]]
288
- run_spec = get_run_spec(
289
- run_name="test_run",
290
- repo_id="test_repo",
291
- configuration=ServiceConfiguration(
292
- port=80,
293
- commands=[""],
294
- volumes=[
295
- VolumeMountPoint(name=["vol1"], path="/vol1"),
296
- VolumeMountPoint(name=["vol1"], path="/vol2"),
297
- ],
298
- ),
299
- )
300
264
  with pytest.raises(ServerClientError):
301
- check_can_attach_run_volumes(run_spec, volumes)
265
+ check_can_attach_job_volumes(volumes)
@@ -6,6 +6,7 @@ from freezegun import freeze_time
6
6
 
7
7
  from dstack._internal.utils.common import (
8
8
  concat_url_path,
9
+ format_duration_multiunit,
9
10
  local_time,
10
11
  make_proxy_url,
11
12
  parse_memory,
@@ -87,6 +88,26 @@ class TestPrettyDate:
87
88
  assert pretty_date(future_time) == ""
88
89
 
89
90
 
91
+ class TestFormatDurationMultiunit:
92
+ @pytest.mark.parametrize(
93
+ ("input", "output"),
94
+ [
95
+ (0, "0s"),
96
+ (59, "59s"),
97
+ (60, "1m"),
98
+ (61, "1m 1s"),
99
+ (694861, "1w 1d 1h 1m 1s"),
100
+ (86401, "1d 1s"),
101
+ ],
102
+ )
103
+ def test(self, input: int, output: str) -> None:
104
+ assert format_duration_multiunit(input) == output
105
+
106
+ def test_forbids_negative(self) -> None:
107
+ with pytest.raises(ValueError):
108
+ format_duration_multiunit(-1)
109
+
110
+
90
111
  class TestParseMemory:
91
112
  @pytest.mark.parametrize(
92
113
  "memory,as_units,expected",
@@ -0,0 +1,38 @@
1
+ import pytest
2
+
3
+ from dstack._internal.utils.env import get_bool
4
+
5
+
6
+ @pytest.mark.parametrize(
7
+ ["value", "expected"],
8
+ [
9
+ ["0", False],
10
+ ["1", True],
11
+ ["true", True],
12
+ ["True", True],
13
+ ["FALSE", False],
14
+ ["off", False],
15
+ ["ON", True],
16
+ ],
17
+ )
18
+ def test_get_bool_is_set(monkeypatch: pytest.MonkeyPatch, value: str, expected: bool):
19
+ monkeypatch.setenv("VAR", value)
20
+ assert get_bool("VAR") is expected
21
+
22
+
23
+ def test_get_bool_not_set_default_not_set(monkeypatch: pytest.MonkeyPatch):
24
+ monkeypatch.delenv("VAR", raising=False)
25
+ assert get_bool("VAR") is False
26
+
27
+
28
+ @pytest.mark.parametrize("default", [False, True])
29
+ def test_get_bool_not_set_default_is_set(monkeypatch: pytest.MonkeyPatch, default: bool):
30
+ monkeypatch.delenv("VAR", raising=False)
31
+ assert get_bool("VAR", default) is default
32
+
33
+
34
+ @pytest.mark.parametrize("value", ["", "2", "foo"])
35
+ def test_get_bool_error_value(monkeypatch: pytest.MonkeyPatch, value: str):
36
+ monkeypatch.setenv("VAR", value)
37
+ with pytest.raises(ValueError, match=f"VAR={value}"):
38
+ assert get_bool("VAR")