import os import subprocess from pathlib import Path from unittest import mock import pytest from click.testing import CliRunner from lightning_app.cli import cmd_install, lightning_cli from lightning_app.testing.helpers import _RunIf @mock.patch("lightning_app.cli.cmd_install.subprocess", mock.MagicMock()) def test_valid_org_app_name(): """Valid organization name.""" runner = CliRunner() # assert a bad app name should fail fake_app = "fakeuser/impossible/name" result = runner.invoke(lightning_cli.install_app, [fake_app]) assert "app name format must have organization/app-name" in result.output # assert a good name (but unavailable name) should work fake_app = "fakeuser/ALKKLJAUHREKJ21234KLAKJDLF" result = runner.invoke(lightning_cli.install_app, [fake_app]) assert f"app: '{fake_app}' is not available on ⚡ Lightning AI ⚡" in result.output assert result.exit_code # assert a good (and availablea name) works # This should be an app that's always in the gallery real_app = "lightning/invideo" result = runner.invoke(lightning_cli.install_app, [real_app]) assert "Press enter to continue:" in result.output @pytest.mark.skip(reason="need to figure out how to authorize git clone from the private repo") def test_valid_unpublished_app_name(): runner = CliRunner() # assert warning of non official app given real_app = "https://github.com/Lightning-AI/install-app" try: subprocess.check_output(f"lightning install app {real_app}", shell=True, stderr=subprocess.STDOUT) # this condition should never be hit assert False except subprocess.CalledProcessError as e: assert "WARNING" in str(e.output) # assert aborted install result = runner.invoke(lightning_cli.install_app, [real_app], input="q") assert "Installation aborted!" in result.output # assert a bad app name should fail fake_app = "https://github.com/Lightning-AI/install-appdd" result = runner.invoke(lightning_cli.install_app, [fake_app, "--yes"]) assert "Looks like the github url was not found" in result.output # assert a good (and availablea name) works result = runner.invoke(lightning_cli.install_app, [real_app]) assert "Press enter to continue:" in result.output @pytest.mark.skip(reason="need to figure out how to authorize git clone from the private repo") def test_app_install(tmpdir, monkeypatch): """Tests unpublished app install.""" monkeypatch.chdir(tmpdir) real_app = "https://github.com/Lightning-AI/install-app" test_app_pip_name = "install-app" # install app and verify it's in the env subprocess.check_output(f"lightning install app {real_app} --yes", shell=True) new_env_output = subprocess.check_output("pip freeze", shell=True) assert test_app_pip_name in str(new_env_output), f"{test_app_pip_name} should be in the env" @mock.patch("lightning_app.cli.cmd_install.subprocess", mock.MagicMock()) def test_valid_org_component_name(): runner = CliRunner() # assert a bad name should fail fake_component = "fakeuser/impossible/name" result = runner.invoke(lightning_cli.install_component, [fake_component]) assert "component name format must have organization/component-name" in result.output # assert a good name (but unavailable name) should work fake_component = "fakeuser/ALKKLJAUHREKJ21234KLAKJDLF" result = runner.invoke(lightning_cli.install_component, [fake_component]) assert f"component: '{fake_component}' is not available on ⚡ Lightning AI ⚡" in result.output # assert a good (and availablea name) works fake_component = "lightning/lit-slack-messenger" result = runner.invoke(lightning_cli.install_component, [fake_component]) assert "Press enter to continue:" in result.output def test_unpublished_component_url_parsing(): runner = CliRunner() # assert a bad name should fail (no git@) fake_component = "https://github.com/Lightning-AI/LAI-slack-messenger" result = runner.invoke(lightning_cli.install_component, [fake_component]) assert "Error, your github url must be in the following format" in result.output # assert a good (and availablea name) works sha = "14f333456ffb6758bd19458e6fa0bf12cf5575e1" real_component = f"git+https://github.com/Lightning-AI/LAI-slack-messenger.git@{sha}" result = runner.invoke(lightning_cli.install_component, [real_component]) assert "Press enter to continue:" in result.output @pytest.mark.skip(reason="need to figure out how to authorize pip install from the private repo") @pytest.mark.parametrize( "real_component, test_component_pip_name", [ ("lightning/lit-slack-messenger", "lit-slack"), ( "git+https://github.com/Lightning-AI/LAI-slack-messenger.git@14f333456ffb6758bd19458e6fa0bf12cf5575e1", "lit-slack", ), ], ) def test_component_install(real_component, test_component_pip_name): """Tests both published and unpublished component installs.""" # uninstall component just in case and verify it's not in the pip output env_output = subprocess.check_output(f"pip uninstall {test_component_pip_name} --yes && pip freeze", shell=True) assert test_component_pip_name not in str(env_output), f"{test_component_pip_name} should not be in the env" # install component and verify it's in the env new_env_output = subprocess.check_output( f"lightning install component {real_component} --yes && pip freeze", shell=True ) assert test_component_pip_name in str(new_env_output), f"{test_component_pip_name} should be in the env" # clean up for test subprocess.run(f"pip uninstall {test_component_pip_name} --yes", shell=True) env_output = subprocess.check_output("pip freeze", shell=True) assert test_component_pip_name not in str( env_output ), f"{test_component_pip_name} should not be in the env after cleanup" def test_prompt_actions(): # TODO: each of these installs must check that a package is installed in the environment correctly app_to_use = "lightning/invideo" runner = CliRunner() # assert that the user can cancel the command with any letter other than y result = runner.invoke(lightning_cli.install_app, [app_to_use], input="b") assert "Installation aborted!" in result.output # assert that the install happens with --yes # result = runner.invoke(lightning_cli.install_app, [app_to_use, "--yes"]) # assert result.exit_code == 0 # assert that the install happens with y # result = runner.invoke(lightning_cli.install_app, [app_to_use], input='y') # assert result.exit_code == 0 # # assert that the install happens with yes # result = runner.invoke(lightning_cli.install_app, [app_to_use], input='yes') # assert result.exit_code == 0 # assert that the install happens with pressing enter # result = runner.invoke(lightning_cli.install_app, [app_to_use]) # TODO: how to check the output when the user types ctrl+c? # result = runner.invoke(lightning_cli.install_app, [app_to_use], input='') @mock.patch("lightning_app.cli.cmd_install.subprocess", mock.MagicMock()) def test_version_arg_component(tmpdir, monkeypatch): monkeypatch.chdir(tmpdir) runner = CliRunner() # Version does not exist component_name = "lightning/lit-slack-messenger" version_arg = "NOT-EXIST" result = runner.invoke(lightning_cli.install_component, [component_name, f"--version={version_arg}"]) assert f"component: 'Version {version_arg} for {component_name}' is not" in str(result.exception) assert result.exit_code == 1 # Version exists # This somwehow fail in test but not when you actually run it version_arg = "0.0.1" runner = CliRunner() result = runner.invoke(lightning_cli.install_component, [component_name, f"--version={version_arg}", "--yes"]) assert result.exit_code == 0 @mock.patch("lightning_app.cli.cmd_install.subprocess", mock.MagicMock()) @mock.patch("lightning_app.cli.cmd_install.os.chdir", mock.MagicMock()) def test_version_arg_app(tmpdir): # Version does not exist app_name = "lightning/invideo" version_arg = "NOT-EXIST" runner = CliRunner() result = runner.invoke(lightning_cli.install_app, [app_name, f"--version={version_arg}"]) assert f"app: 'Version {version_arg} for {app_name}' is not" in str(result.exception) assert result.exit_code == 1 # Version exists version_arg = "0.0.2" runner = CliRunner() result = runner.invoke(lightning_cli.install_app, [app_name, f"--version={version_arg}", "--yes"]) assert result.exit_code == 0 @mock.patch("lightning_app.cli.cmd_install.subprocess", mock.MagicMock()) @mock.patch("lightning_app.cli.cmd_install.os.chdir", mock.MagicMock()) @mock.patch("lightning_app.cli.cmd_install._show_install_app_prompt") def test_install_resolve_latest_version(mock_show_install_app_prompt, tmpdir): app_name = "lightning/invideo" runner = CliRunner() with mock.patch("lightning_app.cli.cmd_install.requests.get") as get_api_mock: get_api_mock.return_value.json.return_value = { "apps": [ { "canDownloadSourceCode": True, "version": "0.0.2", "name": "lightning/invideo", }, { "canDownloadSourceCode": True, "version": "0.0.4", "name": "lightning/invideo", }, { "canDownloadSourceCode": True, "version": "0.0.5", "name": "another_app", }, ] } runner.invoke(lightning_cli.install_app, [app_name, "--yes"]) # no version specified so latest is installed assert mock_show_install_app_prompt.called assert mock_show_install_app_prompt.call_args[0][0]["version"] == "0.0.4" def test_proper_url_parsing(): name = "lightning/invideo" # make sure org/app-name name is correct org, app = cmd_install._validate_name(name, resource_type="app", example="lightning/lit-slack-component") assert org == "lightning" assert app == "invideo" # resolve registry (orgs can have a private registry through their environment variables) registry_url = cmd_install._resolve_app_registry() assert registry_url == "https://lightning.ai/v1/apps" # load the component resource component_entry = cmd_install._resolve_resource(registry_url, name=name, version_arg="latest", resource_type="app") source_url, git_url, folder_name, git_sha = cmd_install._show_install_app_prompt( component_entry, app, org, True, resource_type="app" ) assert folder_name == "LAI-InVideo-search-App" # FixMe: this need to be updated after release with updated org rename assert source_url == "https://github.com/Lightning-AI/LAI-InVideo-search-App" assert "#ref" not in git_url assert git_sha @_RunIf(skip_windows=True) def test_install_app_shows_error(tmpdir): app_folder_dir = Path(tmpdir / "some_random_directory").absolute() app_folder_dir.mkdir() with pytest.raises(SystemExit, match=f"Folder {str(app_folder_dir)} exists, please delete it and try again."): cmd_install._install_app( source_url=mock.ANY, git_url=mock.ANY, folder_name=str(app_folder_dir), overwrite=False ) # def test_env_creation(tmpdir): # cwd = os.getcwd() # os.chdir(tmpdir) # # install app # cmd_install.app("lightning/install-app", True, cwd=tmpdir) # # assert app folder is installed with venv # assert "python" in set(os.listdir(os.path.join(tmpdir, "install-app/bin"))) # # assert the deps are in the env # env_output = subprocess.check_output("source bin/activate && pip freeze", shell=True) # non_env_output = subprocess.check_output("pip freeze", shell=True) # # assert envs are not the same # assert env_output != non_env_output # # assert the reqs are in the env created and NOT in the non env # reqs = open(os.path.join(tmpdir, "install-app/requirements.txt")).read() # assert reqs in str(env_output) and reqs not in str(non_env_output) # # setup.py installs numpy # assert "numpy" in str(env_output) # # run the python script to make sure the file works (in a folder) # app_file = os.path.join(tmpdir, "install-app/src/app.py") # app_output = subprocess.check_output(f"source bin/activate && python {app_file}", shell=True) # assert "b'printed a\\ndeps loaded\\n'" == str(app_output) # # run the python script to make sure the file works (in root) # app_file = os.path.join(tmpdir, "install-app/app_b.py") # app_output = subprocess.check_output(f"source bin/activate && python {app_file}", shell=True) # assert "b'printed a\\n'" == str(app_output) # # reset dir # os.chdir(cwd) @mock.patch.dict(os.environ, {"LIGHTNING_APP_REGISTRY": "https://TODO/other_non_PL_registry"}) def test_private_app_registry(): registry = cmd_install._resolve_app_registry() assert registry == "https://TODO/other_non_PL_registry" def test_public_app_registry(): registry = cmd_install._resolve_app_registry() assert registry == "https://lightning.ai/v1/apps" def test_public_component_registry(): registry = cmd_install._resolve_component_registry() assert registry == "https://lightning.ai/v1/components" @mock.patch.dict(os.environ, {"LIGHTNING_COMPONENT_REGISTRY": "https://TODO/other_non_PL_registry"}) def test_private_component_registry(): registry = cmd_install._resolve_component_registry() assert registry == "https://TODO/other_non_PL_registry" @mock.patch("lightning_app.cli.cmd_install.subprocess") @mock.patch("lightning_app.cli.cmd_install.os.chdir", mock.MagicMock()) @pytest.mark.parametrize( "source_url, git_url, git_sha", [ ( "https://github.com/PyTorchLightning/lightning-quick-start", "https://@github.com/PyTorchLightning/lightning-quick-start", None, ), ( "https://github.com/PyTorchLightning/lightning-quick-start", "https://@github.com/PyTorchLightning/lightning-quick-start", "git_sha", ), ], ) def test_install_app_process(subprocess_mock, source_url, git_url, git_sha, tmpdir): app_folder_dir = Path(tmpdir / "some_random_directory").absolute() app_folder_dir.mkdir() cmd_install._install_app(source_url, git_url, folder_name=str(app_folder_dir), overwrite=True, git_sha=git_sha) assert subprocess_mock.check_output.call_args_list[0].args == (["git", "clone", git_url],) if git_sha: assert subprocess_mock.check_output.call_args_list[1].args == (["git", "checkout", git_sha],) assert subprocess_mock.call.call_args_list[0].args == ("pip install -r requirements.txt",) assert subprocess_mock.call.call_args_list[1].args == ("pip install -e .",)