lightning/tests/tests_app/runners/test_cloud.py

2114 lines
95 KiB
Python

import contextlib
import logging
import os
import pathlib
import re
import sys
from copy import copy
from pathlib import Path
from unittest import mock
from unittest.mock import MagicMock
import pytest
from lightning.app import BuildConfig, LightningApp, LightningFlow, LightningWork
from lightning.app.runners import CloudRuntime, backends, cloud
from lightning.app.source_code.copytree import _copytree, _parse_lightningignore
from lightning.app.source_code.local import LocalSourceCodeDir
from lightning.app.storage import Drive, Mount
from lightning.app.testing.helpers import EmptyWork
from lightning.app.utilities.cloud import _get_project
from lightning.app.utilities.dependency_caching import get_hash
from lightning.app.utilities.packaging.cloud_compute import CloudCompute
from lightning_cloud.openapi import (
CloudspaceIdRunsBody,
Externalv1Cluster,
Externalv1LightningappInstance,
Gridv1ImageSpec,
IdGetBody1,
ProjectIdProjectclustersbindingsBody,
V1BuildSpec,
V1CloudSpace,
V1CloudSpaceInstanceConfig,
V1ClusterSpec,
V1ClusterType,
V1DataConnectionMount,
V1DependencyFileInfo,
V1Drive,
V1DriveSpec,
V1DriveStatus,
V1DriveType,
V1EnvVar,
V1GetUserResponse,
V1LightningappInstanceSpec,
V1LightningappInstanceState,
V1LightningappInstanceStatus,
V1LightningAuth,
V1LightningBasicAuth,
V1LightningRun,
V1LightningworkDrives,
V1LightningworkSpec,
V1ListCloudSpacesResponse,
V1ListClustersResponse,
V1ListLightningappInstancesResponse,
V1ListMembershipsResponse,
V1ListProjectClusterBindingsResponse,
V1Membership,
V1Metadata,
V1NetworkConfig,
V1PackageManager,
V1ProjectClusterBinding,
V1PythonDependencyInfo,
V1QueueServerType,
V1SourceType,
V1UserFeatures,
V1UserRequestedComputeConfig,
V1UserRequestedFlowComputeConfig,
V1Work,
)
class MyWork(LightningWork):
def run(self):
print("my run")
class WorkWithSingleDrive(LightningWork):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.drive = None
def run(self):
pass
class WorkWithTwoDrives(LightningWork):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.lit_drive_1 = None
self.lit_drive_2 = None
def run(self):
pass
def get_cloud_runtime_request_body(**kwargs) -> "CloudspaceIdRunsBody":
default_request_body = {
"app_entrypoint_file": mock.ANY,
"enable_app_server": True,
"is_headless": True,
"should_mount_cloudspace_content": False,
"flow_servers": [],
"image_spec": None,
"works": [],
"local_source": True,
"dependency_cache_key": mock.ANY,
"user_requested_flow_compute_config": V1UserRequestedFlowComputeConfig(
name="flow-lite",
preemptible=False,
shm_size=0,
),
}
if kwargs.get("user_requested_flow_compute_config") is not None:
default_request_body["user_requested_flow_compute_config"] = kwargs["user_requested_flow_compute_config"]
return CloudspaceIdRunsBody(**default_request_body)
@pytest.fixture()
def cloud_backend(monkeypatch):
cloud_backend = mock.MagicMock()
monkeypatch.setattr(cloud, "LocalSourceCodeDir", mock.MagicMock())
monkeypatch.setattr(cloud, "_prepare_lightning_wheels_and_requirements", mock.MagicMock())
monkeypatch.setattr(backends, "CloudBackend", mock.MagicMock(return_value=cloud_backend))
return cloud_backend
@pytest.fixture()
def project_id():
return "test-project-id"
DEFAULT_CLUSTER = "litng-ai-03"
class TestAppCreationClient:
"""Testing the calls made using GridRestClient to create the app."""
def test_run_on_deleted_cluster(self, cloud_backend):
app_name = "test-app"
mock_client = mock.MagicMock()
mock_client.projects_service_list_memberships.return_value = V1ListMembershipsResponse(
memberships=[V1Membership(name="Default Project", project_id=project_id)]
)
mock_client.cluster_service_list_clusters.return_value = V1ListClustersResponse([
Externalv1Cluster(id=DEFAULT_CLUSTER)
])
cloud_backend.client = mock_client
app = mock.MagicMock()
app.flows = []
app.frontend = {}
existing_instance = MagicMock()
existing_instance.status.phase = V1LightningappInstanceState.STOPPED
existing_instance.spec.cluster_id = DEFAULT_CLUSTER
mock_client.lightningapp_instance_service_list_lightningapp_instances.return_value = (
V1ListLightningappInstancesResponse(lightningapps=[existing_instance])
)
cloud_runtime = cloud.CloudRuntime(app=app, entrypoint=Path("entrypoint.py"))
cloud_runtime._check_uploaded_folder = mock.MagicMock()
with pytest.raises(ValueError, match="that cluster doesn't exist"):
cloud_runtime.dispatch(name=app_name, cluster_id="unknown-cluster")
@pytest.mark.parametrize(
("old_cluster", "new_cluster"),
[
("test", "other"),
("test", "test"),
(None, None),
(None, "litng-ai-03"),
("litng-ai-03", None),
],
)
def test_new_instance_on_different_cluster(self, tmpdir, cloud_backend, project_id, old_cluster, new_cluster):
entrypoint = Path(tmpdir) / "entrypoint.py"
entrypoint.touch()
app_name = "test-app"
mock_client = mock.MagicMock()
mock_client.projects_service_list_memberships.return_value = V1ListMembershipsResponse(
memberships=[V1Membership(name="Default Project", project_id=project_id)]
)
mock_client.lightningapp_v2_service_create_lightningapp_release.return_value = V1LightningRun(
cluster_id=new_cluster
)
# Note:
# backend converts "None" cluster to "litng-ai-03"
# dispatch should receive None, but API calls should return "litng-ai-03"
mock_client.cluster_service_list_clusters.return_value = V1ListClustersResponse([
Externalv1Cluster(id=old_cluster or DEFAULT_CLUSTER),
Externalv1Cluster(id=new_cluster or DEFAULT_CLUSTER),
])
mock_client.projects_service_list_project_cluster_bindings.return_value = V1ListProjectClusterBindingsResponse(
clusters=[
V1ProjectClusterBinding(cluster_id=old_cluster or DEFAULT_CLUSTER),
V1ProjectClusterBinding(cluster_id=new_cluster or DEFAULT_CLUSTER),
]
)
# Mock all clusters as global clusters
mock_client.cluster_service_get_cluster.side_effect = lambda cluster_id: Externalv1Cluster(
id=cluster_id, spec=V1ClusterSpec(cluster_type=V1ClusterType.GLOBAL)
)
cloud_backend.client = mock_client
app = mock.MagicMock()
app.flows = []
app.frontend = {}
existing_app = MagicMock()
existing_app.name = app_name
existing_app.id = "test-id"
mock_client.cloud_space_service_list_cloud_spaces.return_value = V1ListCloudSpacesResponse(
cloudspaces=[existing_app]
)
existing_instance = MagicMock()
existing_instance.name = app_name
existing_instance.status.phase = V1LightningappInstanceState.STOPPED
existing_instance.spec.cluster_id = old_cluster or DEFAULT_CLUSTER
mock_client.lightningapp_instance_service_list_lightningapp_instances.return_value = (
V1ListLightningappInstancesResponse(lightningapps=[existing_instance])
)
cloud_runtime = cloud.CloudRuntime(app=app, entrypoint=entrypoint)
cloud_runtime._check_uploaded_folder = mock.MagicMock()
# This is the main assertion:
# we have an existing instance on `cluster-001`
# but we want to run this app on `cluster-002`
cloud_runtime.dispatch(name=app_name, cluster_id=new_cluster)
if new_cluster != old_cluster and None not in (old_cluster, new_cluster):
# If we switched cluster, check that a new name was used which starts with the old name
mock_client.cloud_space_service_create_lightning_run_instance.assert_called_once()
args = mock_client.cloud_space_service_create_lightning_run_instance.call_args
assert args[1]["body"].name != app_name
assert args[1]["body"].name.startswith(app_name)
assert args[1]["body"].cluster_id == new_cluster
def test_running_deleted_app(self, tmpdir, cloud_backend, project_id):
"""Deleted apps show up in list apps but not in list instances.
This tests that we don't try to reacreate a previously deleted app.
"""
entrypoint = Path(tmpdir) / "entrypoint.py"
entrypoint.touch()
app_name = "test-app"
mock_client = mock.MagicMock()
mock_client.projects_service_list_memberships.return_value = V1ListMembershipsResponse(
memberships=[V1Membership(name="Default Project", project_id=project_id)]
)
mock_client.lightningapp_v2_service_create_lightningapp_release.return_value = V1LightningRun(
cluster_id=DEFAULT_CLUSTER
)
mock_client.cluster_service_list_clusters.return_value = V1ListClustersResponse([
Externalv1Cluster(id=DEFAULT_CLUSTER)
])
mock_client.projects_service_list_project_cluster_bindings.return_value = V1ListProjectClusterBindingsResponse(
clusters=[V1ProjectClusterBinding(cluster_id=DEFAULT_CLUSTER)]
)
# Mock all clusters as global clusters
mock_client.cluster_service_get_cluster.side_effect = lambda cluster_id: Externalv1Cluster(
id=cluster_id, spec=V1ClusterSpec(cluster_type=V1ClusterType.GLOBAL)
)
cloud_backend.client = mock_client
app = mock.MagicMock()
app.flows = []
app.frontend = {}
existing_app = MagicMock()
existing_app.name = app_name
existing_app.id = "test-id"
mock_client.cloud_space_service_list_cloud_spaces.return_value = V1ListCloudSpacesResponse(
cloudspaces=[existing_app]
)
# Simulate the app as deleted so no instance to return
mock_client.lightningapp_instance_service_list_lightningapp_instances.return_value = (
V1ListLightningappInstancesResponse(lightningapps=[])
)
cloud_runtime = cloud.CloudRuntime(app=app, entrypoint=entrypoint)
cloud_runtime._check_uploaded_folder = mock.MagicMock()
cloud_runtime.dispatch(name=app_name)
# Check that a new name was used which starts with and does not equal the old name
mock_client.cloud_space_service_create_lightning_run_instance.assert_called_once()
args = mock_client.cloud_space_service_create_lightning_run_instance.call_args
assert args[1]["body"].name != app_name
assert args[1]["body"].name.startswith(app_name)
@pytest.mark.parametrize("flow_cloud_compute", [None, CloudCompute(name="t2.medium")])
@mock.patch("lightning.app.runners.backends.cloud.LightningClient", mock.MagicMock())
def test_run_with_default_flow_compute_config(self, tmpdir, monkeypatch, flow_cloud_compute):
entrypoint = Path(tmpdir) / "entrypoint.py"
entrypoint.touch()
mock_client = mock.MagicMock()
mock_client.projects_service_list_memberships.return_value = V1ListMembershipsResponse(
memberships=[V1Membership(name="test-project", project_id="test-project-id")]
)
mock_client.lightningapp_instance_service_list_lightningapp_instances.return_value = (
V1ListLightningappInstancesResponse(lightningapps=[])
)
mock_client.lightningapp_v2_service_create_lightningapp_release.return_value = V1LightningRun(cluster_id="test")
mock_client.cluster_service_list_clusters.return_value = V1ListClustersResponse([Externalv1Cluster(id="test")])
cloud_backend = mock.MagicMock()
cloud_backend.client = mock_client
monkeypatch.setattr(backends, "CloudBackend", mock.MagicMock(return_value=cloud_backend))
monkeypatch.setattr(cloud, "LocalSourceCodeDir", mock.MagicMock())
dummy_flow = mock.MagicMock()
monkeypatch.setattr(dummy_flow, "run", lambda *args, **kwargs: None)
if flow_cloud_compute is None:
app = LightningApp(dummy_flow)
else:
app = LightningApp(dummy_flow, flow_cloud_compute=flow_cloud_compute)
cloud_runtime = cloud.CloudRuntime(app=app, entrypoint=entrypoint)
cloud_runtime._check_uploaded_folder = mock.MagicMock()
cloud_runtime.dispatch()
user_requested_flow_compute_config = None
if flow_cloud_compute is not None:
user_requested_flow_compute_config = V1UserRequestedFlowComputeConfig(
name=flow_cloud_compute.name, preemptible=False, shm_size=0
)
body = get_cloud_runtime_request_body(user_requested_flow_compute_config=user_requested_flow_compute_config)
cloud_runtime.backend.client.cloud_space_service_create_lightning_run.assert_called_once_with(
project_id="test-project-id", cloudspace_id=mock.ANY, body=body
)
@mock.patch("lightning.app.runners.backends.cloud.LightningClient", mock.MagicMock())
def test_run_on_byoc_cluster(self, tmpdir, monkeypatch):
entrypoint = Path(tmpdir) / "entrypoint.py"
entrypoint.touch()
mock_client = mock.MagicMock()
mock_client.projects_service_list_memberships.return_value = V1ListMembershipsResponse(
memberships=[V1Membership(name="Default Project", project_id="default-project-id")]
)
mock_client.lightningapp_instance_service_list_lightningapp_instances.return_value = (
V1ListLightningappInstancesResponse(lightningapps=[])
)
mock_client.cloud_space_service_create_lightning_run.return_value = V1LightningRun(cluster_id="test1234")
mock_client.cluster_service_list_clusters.return_value = V1ListClustersResponse([
Externalv1Cluster(id="test1234")
])
cloud_backend = mock.MagicMock()
cloud_backend.client = mock_client
monkeypatch.setattr(backends, "CloudBackend", mock.MagicMock(return_value=cloud_backend))
monkeypatch.setattr(cloud, "LocalSourceCodeDir", mock.MagicMock())
monkeypatch.setattr(cloud, "_prepare_lightning_wheels_and_requirements", mock.MagicMock())
app = mock.MagicMock()
app.is_headless = False
app.flows = []
app.frontend = {}
cloud_runtime = cloud.CloudRuntime(app=app, entrypoint=entrypoint)
cloud_runtime._check_uploaded_folder = mock.MagicMock()
cloud_runtime.dispatch(cluster_id="test1234")
body = CloudspaceIdRunsBody(
cluster_id="test1234",
app_entrypoint_file=mock.ANY,
enable_app_server=True,
is_headless=False,
should_mount_cloudspace_content=False,
flow_servers=[],
image_spec=None,
works=[],
local_source=True,
dependency_cache_key=mock.ANY,
user_requested_flow_compute_config=mock.ANY,
)
cloud_runtime.backend.client.cloud_space_service_create_lightning_run.assert_called_once_with(
project_id="default-project-id", cloudspace_id=mock.ANY, body=body
)
cloud_runtime.backend.client.projects_service_create_project_cluster_binding.assert_called_once_with(
project_id="default-project-id",
body=ProjectIdProjectclustersbindingsBody(cluster_id="test1234"),
)
@mock.patch("lightning.app.runners.backends.cloud.LightningClient", mock.MagicMock())
def test_requirements_file(self, tmpdir, monkeypatch):
entrypoint = Path(tmpdir) / "entrypoint.py"
entrypoint.touch()
mock_client = mock.MagicMock()
mock_client.projects_service_list_memberships.return_value = V1ListMembershipsResponse(
memberships=[V1Membership(name="test-project", project_id="test-project-id")]
)
mock_client.lightningapp_instance_service_list_lightningapp_instances.return_value = (
V1ListLightningappInstancesResponse(lightningapps=[])
)
mock_client.cloud_space_service_create_lightning_run.return_value = V1LightningRun()
mock_client.cluster_service_list_clusters.return_value = V1ListClustersResponse([Externalv1Cluster(id="test")])
cloud_backend = mock.MagicMock()
cloud_backend.client = mock_client
monkeypatch.setattr(backends, "CloudBackend", mock.MagicMock(return_value=cloud_backend))
monkeypatch.setattr(cloud, "LocalSourceCodeDir", mock.MagicMock())
monkeypatch.setattr(cloud, "_prepare_lightning_wheels_and_requirements", mock.MagicMock())
app = mock.MagicMock()
app.is_headless = False
app.flows = []
app.frontend = {}
cloud_runtime = cloud.CloudRuntime(app=app, entrypoint=entrypoint)
cloud_runtime._check_uploaded_folder = mock.MagicMock()
# Without requirements file
cloud_runtime.dispatch()
body = CloudspaceIdRunsBody(
app_entrypoint_file=mock.ANY,
enable_app_server=True,
is_headless=False,
should_mount_cloudspace_content=False,
flow_servers=[],
image_spec=None,
works=[],
local_source=True,
dependency_cache_key=mock.ANY,
user_requested_flow_compute_config=mock.ANY,
)
cloud_runtime.backend.client.cloud_space_service_create_lightning_run.assert_called_once_with(
project_id="test-project-id", cloudspace_id=mock.ANY, body=body
)
# with requirements file
requirements = Path(tmpdir) / "requirements.txt"
requirements.touch()
cloud_runtime.dispatch(no_cache=True)
body.image_spec = Gridv1ImageSpec(
dependency_file_info=V1DependencyFileInfo(package_manager=V1PackageManager.PIP, path="requirements.txt")
)
cloud_runtime.backend.client.cloud_space_service_create_lightning_run.assert_called_with(
project_id="test-project-id", cloudspace_id=mock.ANY, body=body
)
@mock.patch("lightning.app.runners.backends.cloud.LightningClient", mock.MagicMock())
def test_basic_auth_enabled(self, tmpdir, monkeypatch):
entrypoint = Path(tmpdir) / "entrypoint.py"
entrypoint.touch()
mock_client = mock.MagicMock()
mock_client.projects_service_list_memberships.return_value = V1ListMembershipsResponse(
memberships=[V1Membership(name="test-project", project_id="test-project-id")]
)
mock_client.lightningapp_instance_service_list_lightningapp_instances.return_value = (
V1ListLightningappInstancesResponse(lightningapps=[])
)
mock_client.cloud_space_service_create_lightning_run.return_value = V1LightningRun()
mock_client.cluster_service_list_clusters.return_value = V1ListClustersResponse([Externalv1Cluster(id="test")])
cloud_backend = mock.MagicMock()
cloud_backend.client = mock_client
monkeypatch.setattr(backends, "CloudBackend", mock.MagicMock(return_value=cloud_backend))
monkeypatch.setattr(cloud, "LocalSourceCodeDir", mock.MagicMock())
monkeypatch.setattr(cloud, "_prepare_lightning_wheels_and_requirements", mock.MagicMock())
app = mock.MagicMock()
app.is_headless = False
app.flows = []
app.frontend = {}
cloud_runtime = cloud.CloudRuntime(app=app, entrypoint=entrypoint)
cloud_runtime._check_uploaded_folder = mock.MagicMock()
# Set cloud_runtime.enable_basic_auth to be not empty:
cloud_runtime.enable_basic_auth = "username:password"
cloud_runtime.dispatch()
mock_client = cloud_runtime.backend.client
body = CloudspaceIdRunsBody(
app_entrypoint_file=mock.ANY,
enable_app_server=True,
is_headless=False,
should_mount_cloudspace_content=False,
flow_servers=[],
image_spec=None,
works=[],
local_source=True,
dependency_cache_key=mock.ANY,
user_requested_flow_compute_config=mock.ANY,
)
mock_client.cloud_space_service_create_lightning_run.assert_called_once_with(
project_id="test-project-id", cloudspace_id=mock.ANY, body=body
)
mock_client.cloud_space_service_create_lightning_run_instance.assert_called_once_with(
project_id="test-project-id",
cloudspace_id=mock.ANY,
id=mock.ANY,
body=IdGetBody1(
desired_state=mock.ANY,
name=mock.ANY,
env=mock.ANY,
queue_server_type=mock.ANY,
auth=V1LightningAuth(basic=V1LightningBasicAuth(username="username", password="password")),
),
)
@mock.patch("lightning.app.runners.backends.cloud.LightningClient", mock.MagicMock())
def test_no_cache(self, tmpdir, monkeypatch):
entrypoint = Path(tmpdir) / "entrypoint.py"
entrypoint.touch()
requirements = Path(tmpdir) / "requirements.txt"
requirements.touch()
mock_client = mock.MagicMock()
mock_client.projects_service_list_memberships.return_value = V1ListMembershipsResponse(
memberships=[V1Membership(name="test-project", project_id="test-project-id")]
)
mock_client.lightningapp_instance_service_list_lightningapp_instances.return_value = (
V1ListLightningappInstancesResponse(lightningapps=[])
)
mock_client.cloud_space_service_create_lightning_run.return_value = V1LightningRun(cluster_id="test")
mock_client.cluster_service_list_clusters.return_value = V1ListClustersResponse([Externalv1Cluster(id="test")])
cloud_backend = mock.MagicMock()
cloud_backend.client = mock_client
monkeypatch.setattr(backends, "CloudBackend", mock.MagicMock(return_value=cloud_backend))
monkeypatch.setattr(cloud, "LocalSourceCodeDir", mock.MagicMock())
monkeypatch.setattr(cloud, "_prepare_lightning_wheels_and_requirements", mock.MagicMock())
monkeypatch.setattr(cloud, "get_hash", lambda *args, **kwargs: "dummy-hash")
app = mock.MagicMock()
app.flows = []
app.frontend = {}
cloud_runtime = cloud.CloudRuntime(app=app, entrypoint=entrypoint)
cloud_runtime._check_uploaded_folder = mock.MagicMock()
# testing with no-cache False
cloud_runtime.dispatch(no_cache=False)
_, _, kwargs = cloud_runtime.backend.client.cloud_space_service_create_lightning_run.mock_calls[0]
body = kwargs["body"]
assert body.dependency_cache_key == "dummy-hash"
# testing with no-cache True
mock_client.reset_mock()
cloud_runtime.dispatch(no_cache=True)
_, _, kwargs = cloud_runtime.backend.client.cloud_space_service_create_lightning_run.mock_calls[0]
body = kwargs["body"]
assert body.dependency_cache_key is None
@mock.patch("lightning.app.runners.backends.cloud.LightningClient", mock.MagicMock())
@pytest.mark.parametrize(
("lightningapps", "start_with_flow"),
[([], False), ([MagicMock()], False), ([MagicMock()], True)],
)
def test_call_with_work_app(self, lightningapps, start_with_flow, monkeypatch, tmpdir):
source_code_root_dir = Path(tmpdir / "src").absolute()
source_code_root_dir.mkdir()
Path(source_code_root_dir / ".lightning").write_text("name: myapp")
requirements_file = Path(source_code_root_dir / "requirements.txt")
Path(requirements_file).touch()
(source_code_root_dir / "entrypoint.py").touch()
mock_client = mock.MagicMock()
if lightningapps:
lightningapps[0].name = "myapp"
lightningapps[0].status.phase = V1LightningappInstanceState.STOPPED
lightningapps[0].spec.cluster_id = "test"
mock_client.cloud_space_service_list_cloud_spaces.return_value = V1ListCloudSpacesResponse(
cloudspaces=lightningapps
)
mock_client.lightningapp_instance_service_list_lightningapp_instances.return_value = (
V1ListLightningappInstancesResponse(lightningapps=lightningapps)
)
mock_client.projects_service_list_project_cluster_bindings.return_value = V1ListProjectClusterBindingsResponse(
clusters=[V1ProjectClusterBinding(cluster_id="test")]
)
mock_client.cluster_service_get_cluster.side_effect = lambda cluster_id: Externalv1Cluster(
id=cluster_id, spec=V1ClusterSpec(cluster_type=V1ClusterType.GLOBAL)
)
mock_client.cloud_space_service_create_lightning_run_instance.return_value = V1LightningRun()
mock_client.cluster_service_list_clusters.return_value = V1ListClustersResponse([Externalv1Cluster(id="test")])
mock_client.cloud_space_service_create_lightning_run_instance.return_value = MagicMock()
existing_instance = MagicMock()
existing_instance.status.phase = V1LightningappInstanceState.STOPPED
mock_client.lightningapp_service_get_lightningapp = MagicMock(return_value=existing_instance)
cloud_backend = mock.MagicMock()
cloud_backend.client = mock_client
monkeypatch.setattr(backends, "CloudBackend", mock.MagicMock(return_value=cloud_backend))
monkeypatch.setattr(cloud, "LocalSourceCodeDir", mock.MagicMock())
monkeypatch.setattr(cloud, "_prepare_lightning_wheels_and_requirements", mock.MagicMock())
app = mock.MagicMock()
app.is_headless = False
work = MyWork(start_with_flow=start_with_flow, cloud_compute=CloudCompute("custom"))
work._name = "test-work"
work._cloud_build_config.build_commands = lambda: ["echo 'start'"]
work._cloud_build_config.requirements = ["torch==1.0.0", "numpy==1.0.0"]
work._cloud_build_config.image = "random_base_public_image"
work._cloud_compute.disk_size = 0
work._port = 8080
app.works = [work]
cloud_runtime = cloud.CloudRuntime(app=app, entrypoint=(source_code_root_dir / "entrypoint.py"))
monkeypatch.setattr(
"lightning.app.runners.cloud._get_project",
lambda _, project_id: V1Membership(name="test-project", project_id="test-project-id"),
)
cloud_runtime.dispatch()
if lightningapps:
expected_body = CloudspaceIdRunsBody(
description=None,
local_source=True,
app_entrypoint_file="entrypoint.py",
enable_app_server=True,
is_headless=False,
should_mount_cloudspace_content=False,
flow_servers=[],
dependency_cache_key=get_hash(requirements_file),
user_requested_flow_compute_config=mock.ANY,
cluster_id="test",
image_spec=Gridv1ImageSpec(
dependency_file_info=V1DependencyFileInfo(
package_manager=V1PackageManager.PIP, path="requirements.txt"
)
),
)
if start_with_flow:
expected_body.works = [
V1Work(
name="test-work",
display_name="",
spec=V1LightningworkSpec(
build_spec=V1BuildSpec(
commands=["echo 'start'"],
python_dependencies=V1PythonDependencyInfo(
package_manager=V1PackageManager.PIP, packages="torch==1.0.0\nnumpy==1.0.0"
),
image="random_base_public_image",
),
drives=[],
user_requested_compute_config=V1UserRequestedComputeConfig(
name="custom",
count=1,
disk_size=0,
shm_size=0,
preemptible=False,
),
network_config=[V1NetworkConfig(name=mock.ANY, host=None, port=8080)],
data_connection_mounts=[],
),
)
]
else:
expected_body.works = []
mock_client.cloud_space_service_create_lightning_run.assert_called_once_with(
project_id="test-project-id", cloudspace_id=mock.ANY, body=expected_body
)
# running dispatch with disabled dependency cache
mock_client.reset_mock()
monkeypatch.setattr(cloud, "DISABLE_DEPENDENCY_CACHE", True)
expected_body.dependency_cache_key = None
cloud_runtime.dispatch()
mock_client.cloud_space_service_create_lightning_run.assert_called_once_with(
project_id="test-project-id", cloudspace_id=mock.ANY, body=expected_body
)
else:
mock_client.cloud_space_service_create_lightning_run_instance.assert_called_once_with(
project_id="test-project-id", cloudspace_id=mock.ANY, id=mock.ANY, body=mock.ANY
)
@mock.patch("lightning.app.runners.backends.cloud.LightningClient", mock.MagicMock())
@pytest.mark.parametrize("lightningapps", [[], [MagicMock()]])
def test_call_with_queue_server_type_specified(self, tmpdir, lightningapps, monkeypatch):
entrypoint = Path(tmpdir) / "entrypoint.py"
entrypoint.touch()
mock_client = mock.MagicMock()
mock_client.projects_service_list_memberships.return_value = V1ListMembershipsResponse(
memberships=[V1Membership(name="test-project", project_id="test-project-id")]
)
mock_client.lightningapp_instance_service_list_lightningapp_instances.return_value = (
V1ListLightningappInstancesResponse(lightningapps=[])
)
mock_client.cloud_space_service_create_lightning_run.return_value = V1LightningRun()
mock_client.cluster_service_list_clusters.return_value = V1ListClustersResponse([Externalv1Cluster(id="test")])
cloud_backend = mock.MagicMock()
cloud_backend.client = mock_client
monkeypatch.setattr(backends, "CloudBackend", mock.MagicMock(return_value=cloud_backend))
monkeypatch.setattr(cloud, "LocalSourceCodeDir", mock.MagicMock())
monkeypatch.setattr(cloud, "_prepare_lightning_wheels_and_requirements", mock.MagicMock())
app = mock.MagicMock()
app.flows = []
app.frontend = {}
cloud_runtime = cloud.CloudRuntime(app=app, entrypoint=entrypoint)
cloud_runtime._check_uploaded_folder = mock.MagicMock()
cloud_runtime.dispatch()
# calling with no env variable set
body = IdGetBody1(
desired_state=V1LightningappInstanceState.STOPPED,
env=[],
name=mock.ANY,
queue_server_type=V1QueueServerType.UNSPECIFIED,
)
client = cloud_runtime.backend.client
client.cloud_space_service_create_lightning_run_instance.assert_called_once_with(
project_id="test-project-id", cloudspace_id=mock.ANY, id=mock.ANY, body=body
)
# calling with env variable set to http
monkeypatch.setitem(os.environ, "LIGHTNING_CLOUD_QUEUE_TYPE", "http")
cloud_runtime.backend.client.reset_mock()
cloud_runtime.dispatch()
body = IdGetBody1(
desired_state=V1LightningappInstanceState.STOPPED,
env=mock.ANY,
name=mock.ANY,
queue_server_type=V1QueueServerType.HTTP,
)
client = cloud_runtime.backend.client
client.cloud_space_service_create_lightning_run_instance.assert_called_once_with(
project_id="test-project-id", cloudspace_id=mock.ANY, id=mock.ANY, body=body
)
@mock.patch("lightning.app.runners.backends.cloud.LightningClient", mock.MagicMock())
@pytest.mark.parametrize("lightningapps", [[], [MagicMock()]])
def test_call_with_work_app_and_attached_drives(self, lightningapps, monkeypatch, tmpdir):
source_code_root_dir = Path(tmpdir / "src").absolute()
source_code_root_dir.mkdir()
Path(source_code_root_dir / ".lightning").write_text("name: myapp")
requirements_file = Path(source_code_root_dir / "requirements.txt")
Path(requirements_file).touch()
(source_code_root_dir / "entrypoint.py").touch()
mock_client = mock.MagicMock()
if lightningapps:
lightningapps[0].name = "myapp"
lightningapps[0].status.phase = V1LightningappInstanceState.STOPPED
lightningapps[0].spec.cluster_id = "test"
mock_client.cloud_space_service_list_cloud_spaces.return_value = V1ListCloudSpacesResponse(
cloudspaces=lightningapps
)
mock_client.lightningapp_instance_service_list_lightningapp_instances.return_value = (
V1ListLightningappInstancesResponse(lightningapps=lightningapps)
)
mock_client.projects_service_list_project_cluster_bindings.return_value = V1ListProjectClusterBindingsResponse(
clusters=[V1ProjectClusterBinding(cluster_id="test")]
)
mock_client.cluster_service_get_cluster.side_effect = lambda cluster_id: Externalv1Cluster(
id=cluster_id, spec=V1ClusterSpec(cluster_type=V1ClusterType.GLOBAL)
)
mock_client.cloud_space_service_create_lightning_run_instance.return_value = V1LightningRun()
mock_client.cluster_service_list_clusters.return_value = V1ListClustersResponse([Externalv1Cluster(id="test")])
lit_app_instance = MagicMock()
mock_client.cloud_space_service_create_lightning_run_instance = MagicMock(return_value=lit_app_instance)
existing_instance = MagicMock()
existing_instance.status.phase = V1LightningappInstanceState.STOPPED
mock_client.lightningapp_service_get_lightningapp = MagicMock(return_value=existing_instance)
cloud_backend = mock.MagicMock()
cloud_backend.client = mock_client
monkeypatch.setattr(backends, "CloudBackend", mock.MagicMock(return_value=cloud_backend))
monkeypatch.setattr(cloud, "LocalSourceCodeDir", mock.MagicMock())
monkeypatch.setattr(cloud, "_prepare_lightning_wheels_and_requirements", mock.MagicMock())
app = mock.MagicMock()
app.is_headless = False
mocked_drive = MagicMock(spec=Drive)
setattr(mocked_drive, "id", "foobar")
setattr(mocked_drive, "protocol", "lit://")
setattr(mocked_drive, "component_name", "test-work")
setattr(mocked_drive, "allow_duplicates", False)
setattr(mocked_drive, "root_folder", tmpdir)
# deepcopy on a MagicMock instance will return an empty magicmock instance. To
# overcome this we set the __deepcopy__ method `return_value` to equal what
# should be the results of the deepcopy operation (an instance of the original class)
mocked_drive.__deepcopy__.return_value = copy(mocked_drive)
work = WorkWithSingleDrive(cloud_compute=CloudCompute("custom"))
monkeypatch.setattr(work, "drive", mocked_drive)
monkeypatch.setattr(work, "_state", {"_port", "drive"})
monkeypatch.setattr(work, "_name", "test-work")
monkeypatch.setattr(work._cloud_build_config, "build_commands", lambda: ["echo 'start'"])
monkeypatch.setattr(work._cloud_build_config, "requirements", ["torch==1.0.0", "numpy==1.0.0"])
monkeypatch.setattr(work._cloud_build_config, "image", "random_base_public_image")
monkeypatch.setattr(work._cloud_compute, "disk_size", 0)
monkeypatch.setattr(work, "_port", 8080)
app.works = [work]
cloud_runtime = cloud.CloudRuntime(app=app, entrypoint=(source_code_root_dir / "entrypoint.py"))
monkeypatch.setattr(
"lightning.app.runners.cloud._get_project",
lambda _, project_id: V1Membership(name="test-project", project_id="test-project-id"),
)
cloud_runtime.dispatch()
if lightningapps:
expected_body = CloudspaceIdRunsBody(
description=None,
local_source=True,
app_entrypoint_file="entrypoint.py",
enable_app_server=True,
is_headless=False,
should_mount_cloudspace_content=False,
flow_servers=[],
dependency_cache_key=get_hash(requirements_file),
user_requested_flow_compute_config=mock.ANY,
cluster_id="test",
image_spec=Gridv1ImageSpec(
dependency_file_info=V1DependencyFileInfo(
package_manager=V1PackageManager.PIP, path="requirements.txt"
)
),
works=[
V1Work(
name="test-work",
display_name="",
spec=V1LightningworkSpec(
build_spec=V1BuildSpec(
commands=["echo 'start'"],
python_dependencies=V1PythonDependencyInfo(
package_manager=V1PackageManager.PIP, packages="torch==1.0.0\nnumpy==1.0.0"
),
image="random_base_public_image",
),
drives=[
V1LightningworkDrives(
drive=V1Drive(
metadata=V1Metadata(
name="test-work.drive",
),
spec=V1DriveSpec(
drive_type=V1DriveType.NO_MOUNT_S3,
source_type=V1SourceType.S3,
source="lit://foobar",
),
status=V1DriveStatus(),
),
),
],
user_requested_compute_config=V1UserRequestedComputeConfig(
name="custom", count=1, disk_size=0, shm_size=0, preemptible=False
),
network_config=[V1NetworkConfig(name=mock.ANY, host=None, port=8080)],
data_connection_mounts=[],
),
)
],
)
mock_client.cloud_space_service_create_lightning_run.assert_called_once_with(
project_id="test-project-id", cloudspace_id=mock.ANY, body=expected_body
)
# running dispatch with disabled dependency cache
mock_client.reset_mock()
monkeypatch.setattr(cloud, "DISABLE_DEPENDENCY_CACHE", True)
expected_body.dependency_cache_key = None
cloud_runtime.dispatch()
mock_client.cloud_space_service_create_lightning_run.assert_called_once_with(
project_id="test-project-id", cloudspace_id=mock.ANY, body=expected_body
)
else:
mock_client.cloud_space_service_create_lightning_run_instance.assert_called_once_with(
project_id="test-project-id", cloudspace_id=mock.ANY, id=mock.ANY, body=mock.ANY
)
@mock.patch("lightning.app.runners.backends.cloud.LightningClient", mock.MagicMock())
@mock.patch("lightning.app.core.constants.ENABLE_APP_COMMENT_COMMAND_EXECUTION", True)
@pytest.mark.parametrize("lightningapps", [[], [MagicMock()]])
def test_call_with_work_app_and_app_comment_command_execution_set(self, lightningapps, monkeypatch, tmpdir):
source_code_root_dir = Path(tmpdir / "src").absolute()
source_code_root_dir.mkdir()
Path(source_code_root_dir / ".lightning").write_text("name: myapp")
requirements_file = Path(source_code_root_dir / "requirements.txt")
Path(requirements_file).touch()
(source_code_root_dir / "entrypoint.py").touch()
mock_client = mock.MagicMock()
if lightningapps:
lightningapps[0].name = "myapp"
lightningapps[0].status.phase = V1LightningappInstanceState.STOPPED
lightningapps[0].spec.cluster_id = "test"
mock_client.projects_service_list_project_cluster_bindings.return_value = (
V1ListProjectClusterBindingsResponse(clusters=[V1ProjectClusterBinding(cluster_id="test")])
)
mock_client.cluster_service_get_cluster.side_effect = lambda cluster_id: Externalv1Cluster(
id=cluster_id, spec=V1ClusterSpec(cluster_type=V1ClusterType.GLOBAL)
)
mock_client.cloud_space_service_list_cloud_spaces.return_value = V1ListCloudSpacesResponse(
cloudspaces=lightningapps
)
mock_client.lightningapp_instance_service_list_lightningapp_instances.return_value = (
V1ListLightningappInstancesResponse(lightningapps=lightningapps)
)
mock_client.cloud_space_service_create_lightning_run_instance.return_value = V1LightningRun()
mock_client.cluster_service_list_clusters.return_value = V1ListClustersResponse([Externalv1Cluster(id="test")])
lit_app_instance = MagicMock()
mock_client.cloud_space_service_create_lightning_run_instance = MagicMock(return_value=lit_app_instance)
existing_instance = MagicMock()
existing_instance.status.phase = V1LightningappInstanceState.STOPPED
mock_client.lightningapp_service_get_lightningapp = MagicMock(return_value=existing_instance)
cloud_backend = mock.MagicMock()
cloud_backend.client = mock_client
monkeypatch.setattr(backends, "CloudBackend", mock.MagicMock(return_value=cloud_backend))
monkeypatch.setattr(cloud, "LocalSourceCodeDir", mock.MagicMock())
monkeypatch.setattr(cloud, "_prepare_lightning_wheels_and_requirements", mock.MagicMock())
app = mock.MagicMock()
app.is_headless = False
work = MyWork(cloud_compute=CloudCompute("custom"))
work._state = {"_port"}
work._name = "test-work"
work._cloud_build_config.build_commands = lambda: ["echo 'start'"]
work._cloud_build_config.requirements = ["torch==1.0.0", "numpy==1.0.0"]
work._cloud_build_config.image = "random_base_public_image"
work._cloud_compute.disk_size = 0
work._port = 8080
app.works = [work]
cloud_runtime = cloud.CloudRuntime(app=app, entrypoint=(source_code_root_dir / "entrypoint.py"))
monkeypatch.setattr(
"lightning.app.runners.cloud._get_project",
lambda _, project_id: V1Membership(name="test-project", project_id="test-project-id"),
)
cloud_runtime.run_app_comment_commands = True
cloud_runtime.dispatch()
if lightningapps:
expected_body = CloudspaceIdRunsBody(
description=None,
local_source=True,
app_entrypoint_file="entrypoint.py",
enable_app_server=True,
is_headless=False,
should_mount_cloudspace_content=False,
flow_servers=[],
dependency_cache_key=get_hash(requirements_file),
user_requested_flow_compute_config=mock.ANY,
cluster_id="test",
image_spec=Gridv1ImageSpec(
dependency_file_info=V1DependencyFileInfo(
package_manager=V1PackageManager.PIP, path="requirements.txt"
)
),
works=[
V1Work(
name="test-work",
display_name="",
spec=V1LightningworkSpec(
build_spec=V1BuildSpec(
commands=["echo 'start'"],
python_dependencies=V1PythonDependencyInfo(
package_manager=V1PackageManager.PIP, packages="torch==1.0.0\nnumpy==1.0.0"
),
image="random_base_public_image",
),
drives=[],
user_requested_compute_config=V1UserRequestedComputeConfig(
name="custom", count=1, disk_size=0, shm_size=0, preemptible=mock.ANY
),
network_config=[V1NetworkConfig(name=mock.ANY, host=None, port=8080)],
cluster_id=mock.ANY,
data_connection_mounts=[],
),
)
],
)
mock_client.cloud_space_service_create_lightning_run.assert_called_once_with(
project_id="test-project-id", cloudspace_id=mock.ANY, body=expected_body
)
# running dispatch with disabled dependency cache
mock_client.reset_mock()
monkeypatch.setattr(cloud, "DISABLE_DEPENDENCY_CACHE", True)
expected_body.dependency_cache_key = None
cloud_runtime.dispatch()
mock_client.cloud_space_service_create_lightning_run.assert_called_once_with(
project_id="test-project-id", cloudspace_id=mock.ANY, body=expected_body
)
else:
mock_client.cloud_space_service_create_lightning_run_instance.assert_called_once_with(
project_id="test-project-id",
cloudspace_id=mock.ANY,
id=mock.ANY,
body=IdGetBody1(
desired_state=V1LightningappInstanceState.STOPPED,
name=mock.ANY,
env=[V1EnvVar(name="ENABLE_APP_COMMENT_COMMAND_EXECUTION", value="1")],
queue_server_type=mock.ANY,
),
)
@mock.patch("lightning.app.runners.backends.cloud.LightningClient", mock.MagicMock())
@pytest.mark.parametrize("lightningapps", [[], [MagicMock()]])
def test_call_with_work_app_and_multiple_attached_drives(self, lightningapps, monkeypatch, tmpdir):
source_code_root_dir = Path(tmpdir / "src").absolute()
source_code_root_dir.mkdir()
Path(source_code_root_dir / ".lightning").write_text("name: myapp")
requirements_file = Path(source_code_root_dir / "requirements.txt")
Path(requirements_file).touch()
(source_code_root_dir / "entrypoint.py").touch()
mock_client = mock.MagicMock()
if lightningapps:
lightningapps[0].name = "myapp"
lightningapps[0].status.phase = V1LightningappInstanceState.STOPPED
lightningapps[0].spec.cluster_id = "test"
mock_client.projects_service_list_project_cluster_bindings.return_value = (
V1ListProjectClusterBindingsResponse(
clusters=[
V1ProjectClusterBinding(cluster_id="test"),
]
)
)
mock_client.cluster_service_get_cluster.side_effect = lambda cluster_id: Externalv1Cluster(
id=cluster_id, spec=V1ClusterSpec(cluster_type=V1ClusterType.GLOBAL)
)
mock_client.cloud_space_service_list_cloud_spaces.return_value = V1ListCloudSpacesResponse(
cloudspaces=lightningapps
)
mock_client.lightningapp_instance_service_list_lightningapp_instances.return_value = (
V1ListLightningappInstancesResponse(lightningapps=lightningapps)
)
mock_client.cloud_space_service_create_lightning_run_instance.return_value = V1LightningRun(cluster_id="test")
mock_client.cluster_service_list_clusters.return_value = V1ListClustersResponse([Externalv1Cluster(id="test")])
lit_app_instance = MagicMock()
mock_client.cloud_space_service_create_lightning_run_instance = MagicMock(return_value=lit_app_instance)
existing_instance = MagicMock()
existing_instance.status.phase = V1LightningappInstanceState.STOPPED
mock_client.lightningapp_service_get_lightningapp = MagicMock(return_value=existing_instance)
cloud_backend = mock.MagicMock()
cloud_backend.client = mock_client
monkeypatch.setattr(backends, "CloudBackend", mock.MagicMock(return_value=cloud_backend))
monkeypatch.setattr(cloud, "LocalSourceCodeDir", mock.MagicMock())
monkeypatch.setattr(cloud, "_prepare_lightning_wheels_and_requirements", mock.MagicMock())
app = mock.MagicMock()
app.is_headless = False
mocked_lit_drive = MagicMock(spec=Drive)
setattr(mocked_lit_drive, "id", "foobar")
setattr(mocked_lit_drive, "protocol", "lit://")
setattr(mocked_lit_drive, "component_name", "test-work")
setattr(mocked_lit_drive, "allow_duplicates", False)
setattr(mocked_lit_drive, "root_folder", tmpdir)
# deepcopy on a MagicMock instance will return an empty magicmock instance. To
# overcome this we set the __deepcopy__ method `return_value` to equal what
# should be the results of the deepcopy operation (an instance of the original class)
mocked_lit_drive.__deepcopy__.return_value = copy(mocked_lit_drive)
work = WorkWithTwoDrives(cloud_compute=CloudCompute("custom"))
work.lit_drive_1 = mocked_lit_drive
work.lit_drive_2 = mocked_lit_drive
work._state = {"_port", "_name", "lit_drive_1", "lit_drive_2"}
work._name = "test-work"
work._cloud_build_config.build_commands = lambda: ["echo 'start'"]
work._cloud_build_config.requirements = ["torch==1.0.0", "numpy==1.0.0"]
work._cloud_build_config.image = "random_base_public_image"
work._cloud_compute.disk_size = 0
work._port = 8080
app.works = [work]
cloud_runtime = cloud.CloudRuntime(app=app, entrypoint=(source_code_root_dir / "entrypoint.py"))
monkeypatch.setattr(
"lightning.app.runners.cloud._get_project",
lambda _, project_id: V1Membership(name="test-project", project_id="test-project-id"),
)
cloud_runtime.dispatch()
if lightningapps:
lit_drive_1_spec = V1LightningworkDrives(
drive=V1Drive(
metadata=V1Metadata(
name="test-work.lit_drive_1",
),
spec=V1DriveSpec(
drive_type=V1DriveType.NO_MOUNT_S3,
source_type=V1SourceType.S3,
source="lit://foobar",
),
status=V1DriveStatus(),
),
)
lit_drive_2_spec = V1LightningworkDrives(
drive=V1Drive(
metadata=V1Metadata(
name="test-work.lit_drive_2",
),
spec=V1DriveSpec(
drive_type=V1DriveType.NO_MOUNT_S3,
source_type=V1SourceType.S3,
source="lit://foobar",
),
status=V1DriveStatus(),
),
)
# order of drives in the spec is non-deterministic, so there are two options
# depending for the expected body value on which drive is ordered in the list first.
expected_body_option_1 = CloudspaceIdRunsBody(
description=None,
local_source=True,
app_entrypoint_file="entrypoint.py",
enable_app_server=True,
is_headless=False,
should_mount_cloudspace_content=False,
flow_servers=[],
dependency_cache_key=get_hash(requirements_file),
user_requested_flow_compute_config=mock.ANY,
cluster_id="test",
image_spec=Gridv1ImageSpec(
dependency_file_info=V1DependencyFileInfo(
package_manager=V1PackageManager.PIP, path="requirements.txt"
)
),
works=[
V1Work(
name="test-work",
display_name="",
spec=V1LightningworkSpec(
build_spec=V1BuildSpec(
commands=["echo 'start'"],
python_dependencies=V1PythonDependencyInfo(
package_manager=V1PackageManager.PIP, packages="torch==1.0.0\nnumpy==1.0.0"
),
image="random_base_public_image",
),
drives=[lit_drive_2_spec, lit_drive_1_spec],
user_requested_compute_config=V1UserRequestedComputeConfig(
name="custom",
count=1,
disk_size=0,
shm_size=0,
preemptible=False,
),
network_config=[V1NetworkConfig(name=mock.ANY, host=None, port=8080)],
data_connection_mounts=[],
),
)
],
)
expected_body_option_2 = CloudspaceIdRunsBody(
description=None,
local_source=True,
app_entrypoint_file="entrypoint.py",
enable_app_server=True,
is_headless=False,
should_mount_cloudspace_content=False,
flow_servers=[],
dependency_cache_key=get_hash(requirements_file),
user_requested_flow_compute_config=mock.ANY,
cluster_id="test",
image_spec=Gridv1ImageSpec(
dependency_file_info=V1DependencyFileInfo(
package_manager=V1PackageManager.PIP, path="requirements.txt"
)
),
works=[
V1Work(
name="test-work",
display_name="",
spec=V1LightningworkSpec(
build_spec=V1BuildSpec(
commands=["echo 'start'"],
python_dependencies=V1PythonDependencyInfo(
package_manager=V1PackageManager.PIP, packages="torch==1.0.0\nnumpy==1.0.0"
),
image="random_base_public_image",
),
drives=[lit_drive_1_spec, lit_drive_2_spec],
user_requested_compute_config=V1UserRequestedComputeConfig(
name="custom",
count=1,
disk_size=0,
shm_size=0,
preemptible=False,
),
network_config=[V1NetworkConfig(name=mock.ANY, host=None, port=8080)],
data_connection_mounts=[],
),
)
],
)
# try both options for the expected body to avoid false
# positive test failures depending on system randomness
expected_body = expected_body_option_1
try:
mock_client.cloud_space_service_create_lightning_run.assert_called_once_with(
project_id="test-project-id", cloudspace_id=mock.ANY, body=expected_body
)
except Exception:
expected_body = expected_body_option_2
mock_client.cloud_space_service_create_lightning_run.assert_called_once_with(
project_id="test-project-id", cloudspace_id=mock.ANY, body=expected_body
)
# running dispatch with disabled dependency cache
mock_client.reset_mock()
monkeypatch.setattr(cloud, "DISABLE_DEPENDENCY_CACHE", True)
expected_body.dependency_cache_key = None
cloud_runtime.dispatch()
mock_client.cloud_space_service_create_lightning_run.assert_called_once_with(
project_id="test-project-id", cloudspace_id=mock.ANY, body=expected_body
)
else:
mock_client.cloud_space_service_create_lightning_run_instance.assert_called_once_with(
project_id="test-project-id", cloudspace_id=mock.ANY, id=mock.ANY, body=mock.ANY
)
@mock.patch("lightning.app.runners.backends.cloud.LightningClient", mock.MagicMock())
@pytest.mark.parametrize("lightningapps", [[], [MagicMock()]])
def test_call_with_work_app_and_attached_mount_and_drive(self, lightningapps, monkeypatch, tmpdir):
source_code_root_dir = Path(tmpdir / "src").absolute()
source_code_root_dir.mkdir()
Path(source_code_root_dir / ".lightning").write_text("name: myapp")
requirements_file = Path(source_code_root_dir / "requirements.txt")
Path(requirements_file).touch()
(source_code_root_dir / "entrypoint.py").touch()
mock_client = mock.MagicMock()
if lightningapps:
lightningapps[0].name = "myapp"
lightningapps[0].status.phase = V1LightningappInstanceState.STOPPED
lightningapps[0].spec.cluster_id = "test"
mock_client.projects_service_list_project_cluster_bindings.return_value = (
V1ListProjectClusterBindingsResponse(clusters=[V1ProjectClusterBinding(cluster_id="test")])
)
mock_client.cluster_service_get_cluster.side_effect = lambda cluster_id: Externalv1Cluster(
id=cluster_id, spec=V1ClusterSpec(cluster_type=V1ClusterType.GLOBAL)
)
mock_client.cloud_space_service_list_cloud_spaces.return_value = V1ListCloudSpacesResponse(
cloudspaces=lightningapps
)
mock_client.lightningapp_instance_service_list_lightningapp_instances.return_value = (
V1ListLightningappInstancesResponse(lightningapps=lightningapps)
)
mock_client.cloud_space_service_create_lightning_run_instance.return_value = V1LightningRun(cluster_id="test")
mock_client.cluster_service_list_clusters.return_value = V1ListClustersResponse([Externalv1Cluster(id="test")])
lit_app_instance = MagicMock()
mock_client.cloud_space_service_create_lightning_run_instance = MagicMock(return_value=lit_app_instance)
existing_instance = MagicMock()
existing_instance.status.phase = V1LightningappInstanceState.STOPPED
existing_instance.spec.cluster_id = None
mock_client.lightningapp_service_get_lightningapp = MagicMock(return_value=existing_instance)
cloud_backend = mock.MagicMock()
cloud_backend.client = mock_client
monkeypatch.setattr(backends, "CloudBackend", mock.MagicMock(return_value=cloud_backend))
monkeypatch.setattr(cloud, "LocalSourceCodeDir", mock.MagicMock())
monkeypatch.setattr(cloud, "_prepare_lightning_wheels_and_requirements", mock.MagicMock())
app = mock.MagicMock()
app.is_headless = False
mocked_drive = MagicMock(spec=Drive)
setattr(mocked_drive, "id", "foobar")
setattr(mocked_drive, "protocol", "lit://")
setattr(mocked_drive, "component_name", "test-work")
setattr(mocked_drive, "allow_duplicates", False)
setattr(mocked_drive, "root_folder", tmpdir)
# deepcopy on a MagicMock instance will return an empty magicmock instance. To
# overcome this we set the __deepcopy__ method `return_value` to equal what
# should be the results of the deepcopy operation (an instance of the original class)
mocked_drive.__deepcopy__.return_value = copy(mocked_drive)
mocked_mount = MagicMock(spec=Mount)
setattr(mocked_mount, "source", "s3://foo/")
setattr(mocked_mount, "mount_path", "/content/foo")
setattr(mocked_mount, "protocol", "s3://")
work = WorkWithSingleDrive(cloud_compute=CloudCompute("custom"))
monkeypatch.setattr(work, "drive", mocked_drive)
monkeypatch.setattr(work, "_state", {"_port", "drive"})
monkeypatch.setattr(work, "_name", "test-work")
monkeypatch.setattr(work._cloud_build_config, "build_commands", lambda: ["echo 'start'"])
monkeypatch.setattr(work._cloud_build_config, "requirements", ["torch==1.0.0", "numpy==1.0.0"])
monkeypatch.setattr(work._cloud_build_config, "image", "random_base_public_image")
monkeypatch.setattr(work._cloud_compute, "disk_size", 0)
monkeypatch.setattr(work._cloud_compute, "mounts", mocked_mount)
monkeypatch.setattr(work, "_port", 8080)
app.works = [work]
cloud_runtime = cloud.CloudRuntime(app=app, entrypoint=(source_code_root_dir / "entrypoint.py"))
monkeypatch.setattr(
"lightning.app.runners.cloud._get_project",
lambda _, project_id: V1Membership(name="test-project", project_id="test-project-id"),
)
cloud_runtime.dispatch()
if lightningapps:
expected_body = CloudspaceIdRunsBody(
description=None,
local_source=True,
app_entrypoint_file="entrypoint.py",
enable_app_server=True,
is_headless=False,
should_mount_cloudspace_content=False,
flow_servers=[],
dependency_cache_key=get_hash(requirements_file),
image_spec=Gridv1ImageSpec(
dependency_file_info=V1DependencyFileInfo(
package_manager=V1PackageManager.PIP, path="requirements.txt"
)
),
user_requested_flow_compute_config=mock.ANY,
cluster_id="test",
works=[
V1Work(
name="test-work",
display_name="",
spec=V1LightningworkSpec(
build_spec=V1BuildSpec(
commands=["echo 'start'"],
python_dependencies=V1PythonDependencyInfo(
package_manager=V1PackageManager.PIP, packages="torch==1.0.0\nnumpy==1.0.0"
),
image="random_base_public_image",
),
drives=[
V1LightningworkDrives(
drive=V1Drive(
metadata=V1Metadata(
name="test-work.drive",
),
spec=V1DriveSpec(
drive_type=V1DriveType.NO_MOUNT_S3,
source_type=V1SourceType.S3,
source="lit://foobar",
),
status=V1DriveStatus(),
),
),
V1LightningworkDrives(
drive=V1Drive(
metadata=V1Metadata(
name="test-work",
),
spec=V1DriveSpec(
drive_type=V1DriveType.INDEXED_S3,
source_type=V1SourceType.S3,
source="s3://foo/",
),
status=V1DriveStatus(),
),
mount_location="/content/foo",
),
],
user_requested_compute_config=V1UserRequestedComputeConfig(
name="custom",
count=1,
disk_size=0,
shm_size=0,
preemptible=False,
),
network_config=[V1NetworkConfig(name=mock.ANY, host=None, port=8080)],
data_connection_mounts=[],
),
)
],
)
mock_client.cloud_space_service_create_lightning_run.assert_called_once_with(
project_id="test-project-id", cloudspace_id=mock.ANY, body=expected_body
)
# running dispatch with disabled dependency cache
mock_client.reset_mock()
monkeypatch.setattr(cloud, "DISABLE_DEPENDENCY_CACHE", True)
expected_body.dependency_cache_key = None
cloud_runtime.dispatch()
mock_client.cloud_space_service_create_lightning_run.assert_called_once_with(
project_id="test-project-id", cloudspace_id=mock.ANY, body=expected_body
)
else:
mock_client.cloud_space_service_create_lightning_run_instance.assert_called_once_with(
project_id="test-project-id", cloudspace_id=mock.ANY, id=mock.ANY, body=mock.ANY
)
class TestOpen:
def test_open(self, monkeypatch):
"""Tests that the open method calls the expected API endpoints."""
mock_client = mock.MagicMock()
mock_client.auth_service_get_user.return_value = V1GetUserResponse(
username="tester", features=V1UserFeatures(code_tab=True)
)
mock_client.projects_service_list_memberships.return_value = V1ListMembershipsResponse(
memberships=[V1Membership(name="test-project", project_id="test-project-id")]
)
mock_client.lightningapp_instance_service_list_lightningapp_instances.return_value = (
V1ListLightningappInstancesResponse(lightningapps=[])
)
mock_client.cloud_space_service_create_cloud_space.return_value = V1CloudSpace(id="cloudspace_id")
mock_client.cloud_space_service_create_lightning_run.return_value = V1LightningRun(id="run_id")
mock_client.cluster_service_list_clusters.return_value = V1ListClustersResponse([Externalv1Cluster(id="test")])
cloud_backend = mock.MagicMock()
cloud_backend.client = mock_client
monkeypatch.setattr(backends, "CloudBackend", mock.MagicMock(return_value=cloud_backend))
mock_local_source = mock.MagicMock()
monkeypatch.setattr(cloud, "LocalSourceCodeDir", mock_local_source)
cloud_runtime = cloud.CloudRuntime(entrypoint=Path("."))
cloud_runtime.open("test_space")
mock_client.cloud_space_service_create_cloud_space.assert_called_once_with(
project_id="test-project-id", body=mock.ANY
)
mock_client.cloud_space_service_create_lightning_run.assert_called_once_with(
project_id="test-project-id", cloudspace_id="cloudspace_id", body=mock.ANY
)
assert mock_client.cloud_space_service_create_cloud_space.call_args.kwargs["body"].name == "test_space"
@pytest.mark.parametrize(
("path", "expected_root", "entries", "expected_filtered_entries"),
[(".", ".", ["a.py", "b.ipynb"], ["a.py", "b.ipynb"]), ("a.py", ".", ["a.py", "b.ipynb"], ["a.py"])],
)
def test_open_repo(self, tmpdir, monkeypatch, path, expected_root, entries, expected_filtered_entries):
"""Tests that the local source code repo is set up with the correct path and ignore functions."""
tmpdir = Path(tmpdir)
for entry in entries:
(tmpdir / entry).touch()
mock_client = mock.MagicMock()
mock_client.auth_service_get_user.return_value = V1GetUserResponse(
username="tester", features=V1UserFeatures(code_tab=True)
)
mock_client.projects_service_list_memberships.return_value = V1ListMembershipsResponse(
memberships=[V1Membership(name="test-project", project_id="test-project-id")]
)
mock_client.lightningapp_instance_service_list_lightningapp_instances.return_value = (
V1ListLightningappInstancesResponse(lightningapps=[])
)
mock_client.lightningapp_v2_service_create_lightningapp_release.return_value = V1LightningRun(cluster_id="test")
mock_client.cluster_service_list_clusters.return_value = V1ListClustersResponse([Externalv1Cluster(id="test")])
cloud_backend = mock.MagicMock()
cloud_backend.client = mock_client
monkeypatch.setattr(backends, "CloudBackend", mock.MagicMock(return_value=cloud_backend))
mock_local_source = mock.MagicMock()
monkeypatch.setattr(cloud, "LocalSourceCodeDir", mock_local_source)
cloud_runtime = cloud.CloudRuntime(entrypoint=tmpdir / path)
cloud_runtime.open("test_space")
mock_local_source.assert_called_once()
repo_call = mock_local_source.call_args
assert repo_call.kwargs["path"] == (tmpdir / expected_root).absolute()
ignore_functions = repo_call.kwargs["ignore_functions"]
if len(ignore_functions) > 0:
filtered = ignore_functions[0]("", [tmpdir / entry for entry in entries])
else:
filtered = [tmpdir / entry for entry in entries]
filtered = [entry.absolute() for entry in filtered]
expected_filtered_entries = [(tmpdir / entry).absolute() for entry in expected_filtered_entries]
assert filtered == expected_filtered_entries
def test_reopen(self, monkeypatch, capsys):
"""Tests that the open method calls the expected API endpoints when the CloudSpace already exists."""
mock_client = mock.MagicMock()
mock_client.auth_service_get_user.return_value = V1GetUserResponse(
username="tester", features=V1UserFeatures(code_tab=True)
)
mock_client.projects_service_list_memberships.return_value = V1ListMembershipsResponse(
memberships=[V1Membership(name="test-project", project_id="test-project-id")]
)
mock_client.cloud_space_service_list_cloud_spaces.return_value = V1ListCloudSpacesResponse(
cloudspaces=[V1CloudSpace(id="cloudspace_id", name="test_space")]
)
running_instance = Externalv1LightningappInstance(
id="instance_id",
name="test_space",
spec=V1LightningappInstanceSpec(cluster_id="test"),
status=V1LightningappInstanceStatus(phase=V1LightningappInstanceState.RUNNING),
)
stopped_instance = Externalv1LightningappInstance(
id="instance_id",
name="test_space",
spec=V1LightningappInstanceSpec(cluster_id="test"),
status=V1LightningappInstanceStatus(phase=V1LightningappInstanceState.STOPPED),
)
mock_client.lightningapp_instance_service_list_lightningapp_instances.return_value = (
V1ListLightningappInstancesResponse(lightningapps=[running_instance])
)
mock_client.lightningapp_instance_service_update_lightningapp_instance.return_value = running_instance
mock_client.lightningapp_instance_service_get_lightningapp_instance.return_value = stopped_instance
mock_client.cloud_space_service_create_cloud_space.return_value = V1CloudSpace(id="cloudspace_id")
mock_client.cloud_space_service_create_lightning_run.return_value = V1LightningRun(id="run_id")
cluster = Externalv1Cluster(id="test", spec=V1ClusterSpec(cluster_type=V1ClusterType.GLOBAL))
mock_client.projects_service_list_project_cluster_bindings.return_value = V1ListProjectClusterBindingsResponse(
clusters=[V1ProjectClusterBinding(cluster_id="test")],
)
mock_client.cluster_service_list_clusters.return_value = V1ListClustersResponse([cluster])
mock_client.cluster_service_get_cluster.return_value = cluster
cloud_backend = mock.MagicMock()
cloud_backend.client = mock_client
monkeypatch.setattr(backends, "CloudBackend", mock.MagicMock(return_value=cloud_backend))
mock_local_source = mock.MagicMock()
monkeypatch.setattr(cloud, "LocalSourceCodeDir", mock_local_source)
cloud_runtime = cloud.CloudRuntime(entrypoint=Path("."))
cloud_runtime.open("test_space")
mock_client.cloud_space_service_create_lightning_run_instance.assert_not_called()
mock_client.cloud_space_service_create_cloud_space.assert_not_called()
mock_client.cloud_space_service_create_lightning_run.assert_called_once_with(
project_id="test-project-id", cloudspace_id="cloudspace_id", body=mock.ANY
)
def test_not_enabled(self, monkeypatch, capsys):
"""Tests that an error is printed and the call exits if the feature isn't enabled for the user."""
mock_client = mock.MagicMock()
mock_client.auth_service_get_user.return_value = V1GetUserResponse(
username="tester",
features=V1UserFeatures(code_tab=False),
)
cloud_backend = mock.MagicMock()
cloud_backend.client = mock_client
monkeypatch.setattr(backends, "CloudBackend", mock.MagicMock(return_value=cloud_backend))
cloud_runtime = cloud.CloudRuntime(entrypoint=Path("."))
monkeypatch.setattr(cloud, "Path", Path)
exited = False
try:
cloud_runtime.open("test_space")
except SystemExit:
# Expected behaviour
exited = True
out, _ = capsys.readouterr()
assert exited
assert "`lightning_app open` command has not been enabled" in out
class TestCloudspaceDispatch:
@mock.patch.object(pathlib.Path, "exists")
@pytest.mark.parametrize(
("custom_env_sync_path_value", "cloudspace"),
[
(None, V1CloudSpace(id="test_id", code_config=V1CloudSpaceInstanceConfig())),
(
Path("/tmp/sys-customizations-sync"),
V1CloudSpace(id="test_id", code_config=V1CloudSpaceInstanceConfig()),
),
(
Path("/tmp/sys-customizations-sync"),
V1CloudSpace(
id="test_id",
code_config=V1CloudSpaceInstanceConfig(data_connection_mounts=[V1DataConnectionMount(id="test")]),
),
),
],
)
def test_cloudspace_dispatch(self, custom_env_sync_root, custom_env_sync_path_value, cloudspace, monkeypatch):
"""Tests that the cloudspace_dispatch method calls the expected API endpoints."""
mock_client = mock.MagicMock()
mock_client.auth_service_get_user.return_value = V1GetUserResponse(
username="tester",
features=V1UserFeatures(),
)
mock_client.projects_service_list_memberships.return_value = V1ListMembershipsResponse(
memberships=[V1Membership(name="project", project_id="project_id")]
)
mock_client.cloud_space_service_create_lightning_run.return_value = V1LightningRun(id="run_id")
mock_client.cloud_space_service_create_lightning_run_instance.return_value = Externalv1LightningappInstance(
id="instance_id"
)
cluster = Externalv1Cluster(id="test", spec=V1ClusterSpec(cluster_type=V1ClusterType.GLOBAL))
mock_client.projects_service_list_project_cluster_bindings.return_value = V1ListProjectClusterBindingsResponse(
clusters=[V1ProjectClusterBinding(cluster_id="cluster_id")],
)
mock_client.cluster_service_list_clusters.return_value = V1ListClustersResponse([cluster])
mock_client.cluster_service_get_cluster.return_value = cluster
mock_client.cloud_space_service_get_cloud_space.return_value = cloudspace
cloud_backend = mock.MagicMock()
cloud_backend.client = mock_client
monkeypatch.setattr(backends, "CloudBackend", mock.MagicMock(return_value=cloud_backend))
mock_repo = mock.MagicMock()
mock_local_source = mock.MagicMock(return_value=mock_repo)
monkeypatch.setattr(cloud, "LocalSourceCodeDir", mock_local_source)
custom_env_sync_root.return_value = custom_env_sync_path_value
mock_app = mock.MagicMock()
mock_app.works = [mock.MagicMock()]
cloud_runtime = cloud.CloudRuntime(app=mock_app, entrypoint=Path("."))
app = cloud_runtime.cloudspace_dispatch("project_id", "cloudspace_id", "run_name", "cluster_id")
assert app.id == "instance_id"
mock_client.cloud_space_service_get_cloud_space.assert_called_once_with(
project_id="project_id", id="cloudspace_id"
)
mock_client.cloud_space_service_create_lightning_run.assert_called_once_with(
project_id="project_id", cloudspace_id="cloudspace_id", body=mock.ANY
)
assert (
mock_client.cloud_space_service_create_lightning_run.call_args.kwargs["body"]
.works[0]
.spec.data_connection_mounts
== cloudspace.code_config.data_connection_mounts
)
mock_client.cloud_space_service_create_lightning_run_instance.assert_called_once_with(
project_id="project_id", cloudspace_id="cloudspace_id", id="run_id", body=mock.ANY
)
assert mock_client.cloud_space_service_create_lightning_run_instance.call_args.kwargs["body"].name == "run_name"
@mock.patch("lightning.app.core.queues.QueuingSystem", MagicMock())
@mock.patch("lightning.app.runners.backends.cloud.LightningClient", MagicMock())
def test_get_project(monkeypatch):
mock_client = mock.MagicMock()
monkeypatch.setattr(cloud, "CloudBackend", mock.MagicMock(return_value=mock_client))
app = mock.MagicMock(spec=LightningApp)
cloud.CloudRuntime(app=app, entrypoint=Path("entrypoint.py"))
# No valid projects
mock_client.projects_service_list_memberships.return_value = V1ListMembershipsResponse(memberships=[])
with pytest.raises(ValueError, match="No valid projects found"):
_get_project(mock_client)
# One valid project
mock_client.projects_service_list_memberships.return_value = V1ListMembershipsResponse(
memberships=[V1Membership(name="test-project", project_id="test-project-id")]
)
ret = _get_project(mock_client)
assert ret.project_id == "test-project-id"
# Multiple valid projects
mock_client.projects_service_list_memberships.return_value = V1ListMembershipsResponse(
memberships=[
V1Membership(name="test-project1", project_id="test-project-id1"),
V1Membership(name="test-project2", project_id="test-project-id2"),
]
)
ret = _get_project(mock_client)
assert ret.project_id == "test-project-id1"
def write_file_of_size(path, size):
os.makedirs(os.path.dirname(path), exist_ok=True)
with open(path, "wb") as f:
f.seek(size)
f.write(b"\0")
@mock.patch("lightning.app.core.queues.QueuingSystem", MagicMock())
@mock.patch("lightning.app.runners.backends.cloud.LightningClient", MagicMock())
def test_check_uploaded_folder(monkeypatch, tmpdir, caplog):
app = MagicMock()
root = Path(tmpdir)
repo = LocalSourceCodeDir(root)
backend = cloud.CloudRuntime(app)
with caplog.at_level(logging.WARN):
backend._validate_repo(root, repo)
assert caplog.messages == []
# write some files to assert the message below.
write_file_of_size(root / "a.png", 4 * 1000 * 1000)
write_file_of_size(root / "b.txt", 5 * 1000 * 1000)
write_file_of_size(root / "c.jpg", 6 * 1000 * 1000)
repo._non_ignored_files = None # force reset
with caplog.at_level(logging.WARN):
backend._validate_repo(root, repo)
assert f"Your application folder '{root.absolute()}' is more than 2 MB" in caplog.text
assert "The total size is 15.0 MB" in caplog.text
assert "4 files were uploaded" in caplog.text
assert "files:\n6.0 MB: c.jpg\n5.0 MB: b.txt\n4.0 MB: a.png\nPerhaps" in caplog.text # tests the order
assert "adding them to `.lightningignore`." in caplog.text
assert "lightningingore` attribute in a Flow or Work" in caplog.text
@mock.patch("lightning.app.core.queues.QueuingSystem", MagicMock())
@mock.patch("lightning.app.runners.backends.cloud.LightningClient", MagicMock())
def test_project_has_sufficient_credits():
app = mock.MagicMock(spec=LightningApp)
cloud_runtime = cloud.CloudRuntime(app=app, entrypoint=Path("entrypoint.py"))
credits_and_test_value = [
[0.3, True],
[1, False],
[1.1, False],
]
for balance, result in credits_and_test_value:
project = V1Membership(name="test-project1", project_id="test-project-id1", balance=balance)
assert cloud_runtime._resolve_needs_credits(project) is result
@pytest.mark.parametrize(
"lines",
[
[
"import this_package_is_not_real",
"from lightning.app import LightningApp",
"from lightning.app.testing.helpers import EmptyWork",
"app = LightningApp(EmptyWork())",
],
[
"from this_package_is_not_real import this_module_is_not_real",
"from lightning.app import LightningApp",
"from lightning.app.testing.helpers import EmptyWork",
"app = LightningApp(EmptyWork())",
],
[
"import this_package_is_not_real",
"from this_package_is_not_real import this_module_is_not_real",
"from lightning.app import LightningApp",
"from lightning.app.testing.helpers import EmptyWork",
"app = LightningApp(EmptyWork())",
],
[
"import this_package_is_not_real",
"from lightning.app import LightningApp",
"from lightning.app.core.flow import _RootFlow",
"from lightning.app.testing.helpers import EmptyWork",
"class MyFlow(_RootFlow):",
" def configure_layout(self):",
" return [{'name': 'test', 'content': this_package_is_not_real()}]",
"app = LightningApp(MyFlow(EmptyWork()))",
],
],
)
@pytest.mark.skipif(sys.platform != "linux", reason="Causing conflicts on non-linux")
def test_load_app_from_file_mock_imports(tmpdir, lines):
path = copy(sys.path)
app_file = os.path.join(tmpdir, "app.py")
with open(app_file, "w") as f:
f.write("\n".join(lines))
app = CloudRuntime.load_app_from_file(app_file)
assert isinstance(app, LightningApp)
assert isinstance(app.root.work, EmptyWork)
# Cleanup PATH to prevent conflict with other tests
sys.path = path
os.remove(app_file)
def test_load_app_from_file():
test_script_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "core", "scripts")
app = CloudRuntime.load_app_from_file(
os.path.join(test_script_dir, "app_with_env.py"),
)
assert app.works[0].cloud_compute.name == "cpu-small"
app = CloudRuntime.load_app_from_file(
os.path.join(test_script_dir, "app_with_env.py"),
env_vars={"COMPUTE_NAME": "foo"},
)
assert app.works[0].cloud_compute.name == "foo"
@pytest.mark.parametrize(
("print_format", "expected"),
[
(
"web",
[
{
"displayName": "",
"name": "root.work",
"spec": {
"buildSpec": {
"commands": [],
"pythonDependencies": {"packageManager": "PACKAGE_MANAGER_PIP", "packages": ""},
},
"dataConnectionMounts": [],
"drives": [],
"networkConfig": [{"name": "*", "port": "*"}],
"userRequestedComputeConfig": {
"count": 1,
"diskSize": 0,
"name": "cpu-small",
"preemptible": "*",
"shmSize": 0,
},
},
}
],
),
(
"gallery",
[
{
"display_name": "",
"name": "root.work",
"spec": {
"build_spec": {
"commands": [],
"python_dependencies": {"package_manager": "PACKAGE_MANAGER_PIP", "packages": ""},
},
"data_connection_mounts": [],
"drives": [],
"network_config": [{"name": "*", "port": "*"}],
"user_requested_compute_config": {
"count": 1,
"disk_size": 0,
"name": "cpu-small",
"preemptible": "*",
"shm_size": 0,
},
},
}
],
),
],
)
def test_print_specs(tmpdir, caplog, monkeypatch, print_format, expected):
entrypoint = Path(tmpdir) / "entrypoint.py"
entrypoint.touch()
mock_client = mock.MagicMock()
mock_client.projects_service_list_memberships.return_value = V1ListMembershipsResponse(
memberships=[V1Membership(name="test-project", project_id="test-project-id")]
)
mock_client.lightningapp_instance_service_list_lightningapp_instances.return_value = (
V1ListLightningappInstancesResponse(lightningapps=[])
)
cloud_backend = mock.MagicMock(client=mock_client)
monkeypatch.setattr(backends, "CloudBackend", mock.MagicMock(return_value=cloud_backend))
cloud_runtime = cloud.CloudRuntime(app=LightningApp(EmptyWork()), entrypoint=entrypoint)
cloud.LIGHTNING_CLOUD_PRINT_SPECS = print_format
try:
with caplog.at_level(logging.INFO), contextlib.suppress(SystemExit):
cloud_runtime.dispatch()
lines = caplog.text.split("\n")
expected = re.escape(str(expected).replace("'", '"').replace(" ", "")).replace('"\\*"', "(.*)")
expected = "INFO(.*)works: " + expected
assert any(re.fullmatch(expected, line) for line in lines)
finally:
cloud.LIGHTNING_CLOUD_PRINT_SPECS = None
def test_incompatible_cloud_compute_and_build_config(monkeypatch):
"""Test that an exception is raised when a build config has a custom image defined, but the cloud compute is the
default.
This combination is not supported by the platform.
"""
mock_client = mock.MagicMock()
cloud_backend = mock.MagicMock(client=mock_client)
monkeypatch.setattr(backends, "CloudBackend", mock.MagicMock(return_value=cloud_backend))
class Work(LightningWork):
def __init__(self):
super().__init__()
self.cloud_compute = CloudCompute(name="default")
# TODO: Remove me
self.cloud_compute.name = "default"
self.cloud_build_config = BuildConfig(image="custom")
def run(self):
pass
app = MagicMock()
app.works = [Work()]
with pytest.raises(ValueError, match="You requested a custom base image for the Work with name"):
CloudRuntime(app=app)._validate_work_build_specs_and_compute()
def test_programmatic_lightningignore(monkeypatch, caplog, tmpdir):
path = Path(tmpdir)
entrypoint = path / "entrypoint.py"
entrypoint.touch()
mock_client = mock.MagicMock()
mock_client.projects_service_list_memberships.return_value = V1ListMembershipsResponse(
memberships=[V1Membership(name="test-project", project_id="test-project-id")]
)
mock_client.lightningapp_instance_service_list_lightningapp_instances.return_value = (
V1ListLightningappInstancesResponse(lightningapps=[])
)
mock_client.cloud_space_service_create_lightning_run.return_value = V1LightningRun(cluster_id="test")
cloud_backend = mock.MagicMock(client=mock_client)
monkeypatch.setattr(backends, "CloudBackend", mock.MagicMock(return_value=cloud_backend))
class MyWork(LightningWork):
def __init__(self):
super().__init__()
self.lightningignore += ("foo", "lightning_logs")
def run(self):
with pytest.raises(RuntimeError, match="w.lightningignore` does not"):
self.lightningignore += ("foobar",)
class MyFlow(LightningFlow):
def __init__(self):
super().__init__()
self.lightningignore = ("foo",)
self.w = MyWork()
def run(self):
with pytest.raises(RuntimeError, match="root.lightningignore` does not"):
self.lightningignore = ("baz",)
self.w.run()
flow = MyFlow()
app = LightningApp(flow)
monkeypatch.setattr(app, "_update_index_file", mock.MagicMock())
cloud_runtime = cloud.CloudRuntime(app=app, entrypoint=entrypoint)
monkeypatch.setattr(LocalSourceCodeDir, "upload", mock.MagicMock())
# write some files
write_file_of_size(path / "a.txt", 5 * 1000 * 1000)
write_file_of_size(path / "foo.png", 4 * 1000 * 1000)
write_file_of_size(path / "lightning_logs" / "foo.ckpt", 6 * 1000 * 1000)
# also an actual .lightningignore file
(path / ".lightningignore").write_text("foo.png")
with mock.patch(
"lightning.app.runners.cloud._parse_lightningignore", wraps=_parse_lightningignore
) as parse_mock, mock.patch(
"lightning.app.source_code.local._copytree", wraps=_copytree
) as copy_mock, caplog.at_level(logging.WARN):
cloud_runtime.dispatch()
parse_mock.assert_called_once_with(("foo", "foo", "lightning_logs"))
assert copy_mock.mock_calls[0].kwargs["ignore_functions"][0].args[1] == {"lightning_logs", "foo"}
assert f"Your application folder '{path.absolute()}' is more than 2 MB" in caplog.text
assert "The total size is 5.0 MB" in caplog.text
assert "2 files were uploaded" # a.txt and .lightningignore
assert "files:\n5.0 MB: a.txt\nPerhaps" in caplog.text # only this file appears
flow.run()
def test_default_lightningignore(monkeypatch, caplog, tmpdir):
path = Path(tmpdir)
entrypoint = path / "entrypoint.py"
entrypoint.touch()
mock_client = mock.MagicMock()
mock_client.projects_service_list_memberships.return_value = V1ListMembershipsResponse(
memberships=[V1Membership(name="test-project", project_id="test-project-id")]
)
mock_client.lightningapp_instance_service_list_lightningapp_instances.return_value = (
V1ListLightningappInstancesResponse(lightningapps=[])
)
mock_client.cloud_space_service_create_lightning_run.return_value = V1LightningRun(cluster_id="test")
cloud_backend = mock.MagicMock(client=mock_client)
monkeypatch.setattr(backends, "CloudBackend", mock.MagicMock(return_value=cloud_backend))
class MyWork(LightningWork):
def run(self):
pass
app = LightningApp(MyWork())
cloud_runtime = cloud.CloudRuntime(app=app, entrypoint=entrypoint)
monkeypatch.setattr(LocalSourceCodeDir, "upload", mock.MagicMock())
# write some files
write_file_of_size(path / "a.txt", 5 * 1000 * 1000)
write_file_of_size(path / "venv" / "foo.txt", 4 * 1000 * 1000)
assert not (path / ".lightningignore").exists()
with mock.patch(
"lightning.app.runners.cloud._parse_lightningignore", wraps=_parse_lightningignore
) as parse_mock, mock.patch(
"lightning.app.source_code.local._copytree", wraps=_copytree
) as copy_mock, caplog.at_level(logging.WARN):
cloud_runtime.dispatch()
parse_mock.assert_called_once_with(())
assert copy_mock.mock_calls[0].kwargs["ignore_functions"][0].args[1] == set()
assert (path / ".lightningignore").exists()
assert f"Your application folder '{path.absolute()}' is more than 2 MB" in caplog.text
assert "The total size is 5.0 MB" in caplog.text
assert "2 files were uploaded" # a.txt and .lightningignore
assert "files:\n5.0 MB: a.txt\nPerhaps" in caplog.text # only this file appears
@pytest.mark.parametrize(
("project", "run_instance", "user", "tab", "lightning_cloud_url", "expected_url"),
[
# Old style
(
V1Membership(),
Externalv1LightningappInstance(id="test-app-id"),
V1GetUserResponse(username="tester", features=V1UserFeatures()),
"logs",
"https://lightning.ai",
"https://lightning.ai/tester/apps/test-app-id/logs",
),
(
V1Membership(),
Externalv1LightningappInstance(id="test-app-id"),
V1GetUserResponse(username="tester", features=V1UserFeatures()),
"logs",
"http://localhost:9800",
"http://localhost:9800/tester/apps/test-app-id/logs",
),
# New style
(
V1Membership(name="tester's project"),
Externalv1LightningappInstance(name="test/job"),
V1GetUserResponse(username="tester", features=V1UserFeatures(project_selector=True)),
"logs",
"https://lightning.ai",
"https://lightning.ai/tester/tester%27s%20project/jobs/test%2Fjob/logs",
),
(
V1Membership(name="tester's project"),
Externalv1LightningappInstance(name="test/job"),
V1GetUserResponse(username="tester", features=V1UserFeatures(project_selector=True)),
"logs",
"https://localhost:9800",
"https://localhost:9800/tester/tester%27s%20project/jobs/test%2Fjob/logs",
),
],
)
def test_get_app_url(monkeypatch, project, run_instance, user, tab, lightning_cloud_url, expected_url):
mock_client = mock.MagicMock()
mock_client.auth_service_get_user.return_value = user
cloud_backend = mock.MagicMock(client=mock_client)
monkeypatch.setattr(backends, "CloudBackend", mock.MagicMock(return_value=cloud_backend))
runtime = CloudRuntime()
with mock.patch(
"lightning.app.runners.cloud.get_lightning_cloud_url", mock.MagicMock(return_value=lightning_cloud_url)
):
assert runtime._get_app_url(project, run_instance, tab) == expected_url
@pytest.mark.parametrize(
("user", "project", "cloudspace_name", "tab", "lightning_cloud_url", "expected_url"),
[
(
V1GetUserResponse(username="tester", features=V1UserFeatures()),
V1Membership(name="default-project"),
"test/cloudspace",
"code",
"https://lightning.ai",
"https://lightning.ai/tester/default-project/apps/test%2Fcloudspace/code",
),
(
V1GetUserResponse(username="tester", features=V1UserFeatures()),
V1Membership(name="Awesome Project"),
"The Best CloudSpace ever",
"web-ui",
"http://localhost:9800",
"http://localhost:9800/tester/Awesome%20Project/apps/The%20Best%20CloudSpace%20ever/web-ui",
),
],
)
def test_get_cloudspace_url(monkeypatch, user, project, cloudspace_name, tab, lightning_cloud_url, expected_url):
mock_client = mock.MagicMock()
mock_client.auth_service_get_user.return_value = user
cloud_backend = mock.MagicMock(client=mock_client)
monkeypatch.setattr(backends, "CloudBackend", mock.MagicMock(return_value=cloud_backend))
runtime = CloudRuntime()
with mock.patch(
"lightning.app.runners.cloud.get_lightning_cloud_url", mock.MagicMock(return_value=lightning_cloud_url)
):
assert runtime._get_cloudspace_url(project, cloudspace_name, tab) == expected_url