diff --git a/.azure/app-cloud-e2e.yml b/.azure/app-cloud-e2e.yml index a7ec682250..b0eb838149 100644 --- a/.azure/app-cloud-e2e.yml +++ b/.azure/app-cloud-e2e.yml @@ -58,8 +58,8 @@ jobs: name: "template_streamlit_ui" 'App: template_react_ui': name: "template_react_ui" - 'App: template_jupyterlab': # TODO: clarify where these files lives - name: "template_jupyterlab" + # 'App: template_jupyterlab': # TODO: clarify where these files lives + # name: "template_jupyterlab" 'App: idle_timeout': name: "idle_timeout" 'App: collect_failures': @@ -70,10 +70,10 @@ jobs: name: "drive" 'App: payload': name: "payload" - # 'App: commands_and_api': - # name: "commands_and_api" - 'App: quick_start': - name: "quick_start" + 'App: commands_and_api': + name: "commands_and_api" + # 'App: quick_start': # TODO: Failed during installation + # name: "quick_start" timeoutInMinutes: "30" cancelTimeoutInMinutes: "2" # values: https://docs.microsoft.com/en-us/azure/devops/pipelines/process/phases?view=azure-devops&tabs=yaml#workspace @@ -101,12 +101,10 @@ jobs: path: $(pip_cache_dir) displayName: Cache pip - - bash: git restore . && python -m pip install -e .[ui] --find-links https://download.pytorch.org/whl/cpu/torch_stable.html - env: - PACKAGE_NAME: app - displayName: 'Install lightning app' + - bash: git restore . && python -m pip install -e . --find-links https://download.pytorch.org/whl/cpu/torch_stable.html + displayName: 'Install Lightning' - - bash: python -m pip install -r requirements/app/test.txt --find-links https://download.pytorch.org/whl/cpu/torch_stable.html + - bash: python -m pip install -r requirements/app/test.txt -r requirements/app/ui.txt --find-links https://download.pytorch.org/whl/cpu/torch_stable.html env: PACKAGE_NAME: pytorch displayName: 'Install dependencies' @@ -116,14 +114,14 @@ jobs: python -m playwright install # --with-deps displayName: 'Install Playwright system dependencies' - - bash: | - rm -rf examples/app_template_jupyterlab || true - git clone https://github.com/Lightning-AI/LAI-lightning-template-jupyterlab-App examples/app_template_jupyterlab - cp examples/app_template_jupyterlab/tests/test_template_jupyterlab.py tests/tests_app_examples/test_template_jupyterlab.py - condition: eq(variables['name'], 'template_jupyterlab') - displayName: 'Clone Template Jupyter Lab Repo' + # - bash: | + # rm -rf examples/app_template_jupyterlab || true + # git clone https://github.com/Lightning-AI/LAI-lightning-template-jupyterlab-App examples/app_template_jupyterlab + # cp examples/app_template_jupyterlab/tests/test_template_jupyterlab.py tests/tests_app_examples/test_template_jupyterlab.py + # condition: eq(variables['name'], 'template_jupyterlab') + # displayName: 'Clone Template Jupyter Lab Repo' - - bash: | + - bash: | rm -rf examples/app_template_react_ui || true git clone https://github.com/Lightning-AI/lightning-template-react examples/app_template_react_ui condition: eq(variables['name'], 'template_react_ui') diff --git a/examples/app_drive/app.py b/examples/app_drive/app.py index 0dd0257d47..6000484793 100644 --- a/examples/app_drive/app.py +++ b/examples/app_drive/app.py @@ -48,4 +48,4 @@ class Flow(L.LightningFlow): self._exit("Application End!") -app = L.LightningApp(Flow(), debug=True) +app = L.LightningApp(Flow()) diff --git a/src/lightning_app/testing/testing.py b/src/lightning_app/testing/testing.py index 21ee0c8605..2794b5231e 100644 --- a/src/lightning_app/testing/testing.py +++ b/src/lightning_app/testing/testing.py @@ -27,8 +27,10 @@ from lightning_app.utilities.app_logs import _app_logs_reader from lightning_app.utilities.cloud import _get_project from lightning_app.utilities.enum import CacheCallsKeys from lightning_app.utilities.imports import _is_playwright_available, requires +from lightning_app.utilities.log import get_logfile from lightning_app.utilities.logs_socket_api import _LightningLogsSocketAPI from lightning_app.utilities.network import _configure_session, LightningClient +from lightning_app.utilities.packaging.lightning_utils import get_dist_path_if_editable_install from lightning_app.utilities.proxies import ProxyWorkRun if _is_playwright_available(): @@ -222,6 +224,10 @@ def run_app_in_cloud( basename = app_folder.split("/")[-1] PR_NUMBER = os.getenv("PR_NUMBER", None) + is_editable_mode = get_dist_path_if_editable_install("lightning") + if not is_editable_mode and PR_NUMBER is not None: + raise Exception("Lightning requires to be installed in editable mode in the CI.") + TEST_APP_NAME = os.getenv("TEST_APP_NAME", basename) os.environ["TEST_APP_NAME"] = TEST_APP_NAME @@ -253,29 +259,35 @@ def run_app_in_cloud( env_copy["LIGHTNING_DEBUG"] = "1" shutil.copytree(app_folder, tmpdir, dirs_exist_ok=True) # TODO - add -no-cache to the command line. - process = Popen( - ( - [ - sys.executable, - "-m", - "lightning", - "run", - "app", - app_name, - "--cloud", - "--name", - name, - "--open-ui", - "false", - ] - + extra_args - ), - cwd=tmpdir, - env=env_copy, - stdout=sys.stdout, - stderr=sys.stderr, - ) - process.wait() + stdout_path = get_logfile(f"run_app_in_cloud_{name}") + with open(stdout_path, "w") as stdout: + cmd = [ + sys.executable, + "-m", + "lightning", + "run", + "app", + app_name, + "--cloud", + "--name", + name, + "--open-ui", + "false", + ] + process = Popen((cmd + extra_args), cwd=tmpdir, env=env_copy, stdout=stdout, stderr=sys.stderr) + process.wait() + + if is_editable_mode: + # Added to ensure the current code is properly uploaded. + # Otherwise, it could result in un-tested PRs. + pkg_found = False + with open(stdout_path) as fo: + for line in fo.readlines(): + if "Packaged Lightning with your application" in line: + pkg_found = True + print(line) # TODO: use logging + assert pkg_found + os.remove(stdout_path) # 5. Print your application name print(f"The Lightning App Name is: [bold magenta]{name}[/bold magenta]") diff --git a/src/lightning_app/utilities/app_logs.py b/src/lightning_app/utilities/app_logs.py index 0a63949fbd..369adc5d09 100644 --- a/src/lightning_app/utilities/app_logs.py +++ b/src/lightning_app/utilities/app_logs.py @@ -86,7 +86,7 @@ def _app_logs_reader( th.start() # Print logs from queue when log event is available - flow = "Your app has started. View it in your browser" + flow = "Your app has started." work = "USER_RUN_WORK" start_timestamps = {} @@ -94,6 +94,7 @@ def _app_logs_reader( try: while True: log_event: _LogEvent = read_queue.get(timeout=None if follow else 1.0) + token = flow if log_event.component_name == "flow" else work if token in log_event.message: start_timestamps[log_event.component_name] = log_event.timestamp diff --git a/src/lightning_app/utilities/packaging/cloud_compute.py b/src/lightning_app/utilities/packaging/cloud_compute.py index 885d40d351..f3b162ed04 100644 --- a/src/lightning_app/utilities/packaging/cloud_compute.py +++ b/src/lightning_app/utilities/packaging/cloud_compute.py @@ -84,6 +84,9 @@ class CloudCompute: if self._internal_id is None: self._internal_id = "default" if self.name == "default" else uuid4().hex[:7] + # Internal arguments for now. + self.preemptible = False + def to_dict(self) -> dict: _verify_mount_root_dirs_are_unique(self.mounts) return {"type": __CLOUD_COMPUTE_IDENTIFIER__, **asdict(self)} diff --git a/src/lightning_app/utilities/packaging/lightning_utils.py b/src/lightning_app/utilities/packaging/lightning_utils.py index 4688aa2017..224b8e6614 100644 --- a/src/lightning_app/utilities/packaging/lightning_utils.py +++ b/src/lightning_app/utilities/packaging/lightning_utils.py @@ -56,6 +56,7 @@ def _prepare_wheel(path): ["rm", "-r", "dist"], stdout=logfile, stderr=logfile, bufsize=0, close_fds=True, cwd=path ) as proc: proc.wait() + with subprocess.Popen( ["python", "setup.py", "sdist"], stdout=logfile, @@ -85,26 +86,30 @@ def get_dist_path_if_editable_install(project_name) -> str: for path_item in sys.path: if not os.path.isdir(path_item): continue + egg_info = os.path.join(path_item, project_name + ".egg-info") if os.path.isdir(egg_info): return path_item return "" -def _prepare_lightning_wheels_and_requirements(root: Path) -> Optional[Callable]: +def _prepare_lightning_wheels_and_requirements(root: Path, package_name: str = "lightning") -> Optional[Callable]: """This function determines if lightning is installed in editable mode (for developers) and packages the current lightning source along with the app. For normal users who install via PyPi or Conda, then this function does not do anything. """ - - if not get_dist_path_if_editable_install("lightning-app"): + if not get_dist_path_if_editable_install(package_name): return + # this is patch for installing `lightning-app` as standalone package + if package_name == "lightning_app": + os.environ["PACKAGE_NAME"] = "app" + # Packaging the Lightning codebase happens only inside the `lightning` repo. git_dir_name = get_dir_name() if check_github_repository() else None - is_lightning = git_dir_name and git_dir_name == "lightning_app" + is_lightning = git_dir_name and git_dir_name == package_name if (PACKAGE_LIGHTNING is None and not is_lightning) or PACKAGE_LIGHTNING == "0": return @@ -112,7 +117,8 @@ def _prepare_lightning_wheels_and_requirements(root: Path) -> Optional[Callable] download_frontend(_PROJECT_ROOT) _prepare_wheel(_PROJECT_ROOT) - logger.info(f"Packaged Lightning with your application. Version: {version}") + # todo: check why logging.info is missing in outputs + print(f"Packaged Lightning with your application. Version: {version}") tar_name = _copy_tar(_PROJECT_ROOT, root) @@ -125,7 +131,8 @@ def _prepare_lightning_wheels_and_requirements(root: Path) -> Optional[Callable] if launcher_project_path: from lightning_launcher.__version__ import __version__ as launcher_version - logger.info(f"Packaged Lightning Launcher with your application. Version: {launcher_version}") + # todo: check why logging.info is missing in outputs + print(f"Packaged Lightning Launcher with your application. Version: {launcher_version}") _prepare_wheel(launcher_project_path) tar_name = _copy_tar(launcher_project_path, root) tar_files.append(os.path.join(root, tar_name)) @@ -135,7 +142,8 @@ def _prepare_lightning_wheels_and_requirements(root: Path) -> Optional[Callable] if lightning_cloud_project_path: from lightning_cloud.__version__ import __version__ as cloud_version - logger.info(f"Packaged Lightning Cloud with your application. Version: {cloud_version}") + # todo: check why logging.info is missing in outputs + print(f"Packaged Lightning Cloud with your application. Version: {cloud_version}") _prepare_wheel(lightning_cloud_project_path) tar_name = _copy_tar(lightning_cloud_project_path, root) tar_files.append(os.path.join(root, tar_name)) diff --git a/tests/tests_app/utilities/packaging/test_lightning_utils.py b/tests/tests_app/utilities/packaging/test_lightning_utils.py index d9b0a14901..d171e043fc 100644 --- a/tests/tests_app/utilities/packaging/test_lightning_utils.py +++ b/tests/tests_app/utilities/packaging/test_lightning_utils.py @@ -1,6 +1,8 @@ +import os from unittest import mock import pytest +from lightning_utilities.core.imports import module_available from lightning_app.testing.helpers import RunIf from lightning_app.utilities.packaging import lightning_utils @@ -9,17 +11,27 @@ from lightning_app.utilities.packaging.lightning_utils import ( _verify_lightning_version, ) -# TODO: this is very sensitive test, need to be done -# def test_prepare_lightning_wheels_and_requirement(tmpdir): -# """This test ensures the lightning source gets packaged inside the lightning repo.""" -# -# cleanup_handle = _prepare_lightning_wheels_and_requirements(tmpdir) -# from lightning_app.__version__ import version -# -# tar_name = f"lightning-app-{version}.tar.gz" -# assert sorted(os.listdir(tmpdir))[0] == tar_name -# cleanup_handle() -# assert os.listdir(tmpdir) == [] + +# TODO: Resolve this sensitive test. +@pytest.mark.skipif(True, reason="Currently broken") +def test_prepare_lightning_wheels_and_requirement(tmpdir): + """This test ensures the lightning source gets packaged inside the lightning repo.""" + + package_name = "lightning" if module_available("lightning") else "lightning-app" + + if package_name == "lightning": + from lightning.__version__ import version + + tar_name = f"lightning-{version}.tar.gz" + else: + from lightning_app.__version__ import version + + tar_name = f"lightning-app-{version}.tar.gz" + + cleanup_handle = _prepare_lightning_wheels_and_requirements(tmpdir, package_name=package_name) + assert sorted(os.listdir(tmpdir))[0] == tar_name + cleanup_handle() + assert os.listdir(tmpdir) == [] def _mocked_get_dist_path_if_editable_install(*args, **kwargs): diff --git a/tests/tests_app_examples/test_boring_app.py b/tests/tests_app_examples/test_boring_app.py index e1dfc33d32..cabecb83d3 100644 --- a/tests/tests_app_examples/test_boring_app.py +++ b/tests/tests_app_examples/test_boring_app.py @@ -17,7 +17,7 @@ def test_boring_app_example_cloud() -> None: ) as ( _, view_page, - fetch_logs, + _, name, ): @@ -34,5 +34,5 @@ def test_boring_app_example_cloud() -> None: assert result.exit_code == 0 assert result.exception is None - assert any("http://0.0.0.0:8080" in line for line in lines) + assert any("http://0.0.0.0:1111" in line for line in lines) print("Succeeded App!") diff --git a/tests/tests_app_examples/test_commands_and_api.py b/tests/tests_app_examples/test_commands_and_api.py index 4d96816b88..25ca40d6d1 100644 --- a/tests/tests_app_examples/test_commands_and_api.py +++ b/tests/tests_app_examples/test_commands_and_api.py @@ -21,16 +21,14 @@ def test_commands_and_api_example_cloud() -> None: # 1: Collect the app_id app_id = admin_page.url.split("/")[-1] - # 2: Connect to the App - Popen(f"python -m lightning connect {app_id} -y", shell=True).wait() - - # 3: Send the first command with the client - cmd = "python -m lightning command with client --name=this" - Popen(cmd, shell=True).wait() - - # 4: Send the second command without a client - cmd = "python -m lightning command without client --name=is" - Popen(cmd, shell=True).wait() + # 2: Connect to the App and send the first & second command with the client + # Requires to be run within the same process. + cmd_1 = f"python -m lightning connect {app_id} -y" + cmd_2 = "python -m lightning command with client --name=this" + cmd_3 = "python -m lightning command without client --name=is" + cmd_4 = "lightning disconnect" + process = Popen(" && ".join([cmd_1, cmd_2, cmd_3, cmd_4]), shell=True) + process.wait() # This prevents some flakyness in the CI. Couldn't reproduce it locally. sleep(5) @@ -47,6 +45,3 @@ def test_commands_and_api_example_cloud() -> None: if "['this', 'is', 'awesome']" in log: has_logs = True sleep(1) - - # 7: Disconnect from the App - Popen("lightning disconnect", shell=True).wait() diff --git a/tests/tests_app_examples/test_drive.py b/tests/tests_app_examples/test_drive.py index dde68d1a85..630e76b550 100644 --- a/tests/tests_app_examples/test_drive.py +++ b/tests/tests_app_examples/test_drive.py @@ -18,7 +18,7 @@ def test_drive_example_cloud() -> None: has_logs = False while not has_logs: - for log in fetch_logs(): + for log in fetch_logs(["flow"]): if "Application End!" in log: has_logs = True sleep(1)