From 28c89b554f0c3a3f7053c42c9a269eb48e9fa9f1 Mon Sep 17 00:00:00 2001 From: Fabio Zadrozny Date: Thu, 12 Dec 2024 10:07:06 -0300 Subject: [PATCH] Make sure that `data_server_cli` fixture is not run when running other tests, separate tests requiring RCC environments from the other tests. --- ...ts-sema4ai-sdk-data-server-integration.yml | 3 +- .../tests-sema4ai-sdk-rcc-integration.yml | 84 ++++++++++++ .github/workflows/tests-sema4ai-sdk.yml | 2 +- .../src/sema4ai_ls_core/core_log.py | 10 +- .../src/sema4ai_ls_core/system_mutex.py | 2 +- .../sema4ai_code/robocorp_language_server.py | 125 ++++++++++-------- sema4ai/tests/pytest.ini | 1 + .../data_server_fixtures.py | 9 ++ .../test_compute_data_sources_state.py | 2 +- .../test_data_sources_available.py | 0 .../test_language_server_directly.py | 16 ++- sema4ai/tests/sema4ai_code_tests/test_rcc.py | 3 + .../test_resolve_interpreter.py | 3 + .../test_vscode_integration.py | 15 ++- 14 files changed, 199 insertions(+), 76 deletions(-) create mode 100644 .github/workflows/tests-sema4ai-sdk-rcc-integration.yml delete mode 100644 sema4ai/tests/sema4ai_code_tests/test_data_sources_available.py diff --git a/.github/workflows/tests-sema4ai-sdk-data-server-integration.yml b/.github/workflows/tests-sema4ai-sdk-data-server-integration.yml index 0a3a0cbe0..af4376296 100644 --- a/.github/workflows/tests-sema4ai-sdk-data-server-integration.yml +++ b/.github/workflows/tests-sema4ai-sdk-data-server-integration.yml @@ -68,8 +68,9 @@ jobs: # Big timeout to create environment in windows. RUN_TESTS_TIMEOUT: 3000 ACTION_SERVER_TEST_ACCESS_CREDENTIALS: ${{ secrets.ACTION_SERVER_TEST_ACCESS_CREDENTIALS }} + PYTEST_CAN_RUN_DATA_SERVER: 1 - run: poetry run python -u ../../sema4ai-python-ls-core/tests/run_tests.py -rfE -otests_output_data_server -vv -m data_server . + run: poetry run python -u ../../sema4ai-python-ls-core/tests/run_tests.py -rfE -n 1 -otests_output_data_server -vv -m data_server . - uses: actions/upload-artifact@v4 if: always() diff --git a/.github/workflows/tests-sema4ai-sdk-rcc-integration.yml b/.github/workflows/tests-sema4ai-sdk-rcc-integration.yml new file mode 100644 index 000000000..83b36fddd --- /dev/null +++ b/.github/workflows/tests-sema4ai-sdk-rcc-integration.yml @@ -0,0 +1,84 @@ +name: Tests - Sema4.ai RCC Integration (sema4ai) + +on: + push: + paths: + - sema4ai/** + - sema4ai-python-ls-core/** + - .github/** + + pull_request: + paths: + - sema4ai/** + - sema4ai-python-ls-core/** + - .github/** + +jobs: + build: + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + name: [ + "ubuntu-py311", + "windows-py311", +# "mac-py311", mac disabled because it takes too long for runners to pick the mac job up. + ] + + include: + - name: "ubuntu-py311" + python: "3.11" + os: ubuntu-latest + - name: "windows-py311" + python: "3.11" + os: windows-latest +# - name: "mac-py311" +# python: "3.11" +# os: mac-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Set up Python ${{ matrix.python }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python }} + - name: Upgrade pip + run: python -m pip install --upgrade pip + - name: Vendor sema4ai_ls_core + working-directory: ./sema4ai + run: | + pip install fire poetry + python -m dev vendor-robocorp-ls-core + - name: poetry install + working-directory: ./sema4ai + run: poetry install + - name: Run codegen + run: python -m dev codegen + working-directory: ./sema4ai + env: + PYTHONPATH: src + - name: Test + working-directory: ./sema4ai/tests + env: + PYTHONPATH: . + CI_CREDENTIALS: ${{ secrets.CI_CREDENTIALS }} + CI_ENDPOINT: ${{ secrets.CI_ENDPOINT }} + # Big timeout to create environment in windows. + RUN_TESTS_TIMEOUT: 3000 + ACTION_SERVER_TEST_ACCESS_CREDENTIALS: ${{ secrets.ACTION_SERVER_TEST_ACCESS_CREDENTIALS }} + + run: poetry run python -u ../../sema4ai-python-ls-core/tests/run_tests.py -rfE -n 1 -otests_output_rcc -vv -m rcc_env . + + - uses: actions/upload-artifact@v4 + if: always() + with: + name: tests_output_rcc.${{ matrix.name }}.txt + path: sema4ai/tests/tests_output_rcc + - uses: actions/upload-artifact@v4 + if: always() + with: + name: log_rcc.${{ matrix.name }}.html + path: sema4ai/tests/output/log.html + diff --git a/.github/workflows/tests-sema4ai-sdk.yml b/.github/workflows/tests-sema4ai-sdk.yml index e40a08ba3..5f4fbdb72 100644 --- a/.github/workflows/tests-sema4ai-sdk.yml +++ b/.github/workflows/tests-sema4ai-sdk.yml @@ -69,7 +69,7 @@ jobs: RUN_TESTS_TIMEOUT: 3000 ACTION_SERVER_TEST_ACCESS_CREDENTIALS: ${{ secrets.ACTION_SERVER_TEST_ACCESS_CREDENTIALS }} - run: poetry run python -u ../../sema4ai-python-ls-core/tests/run_tests.py -rfE -otests_output -vv -m "not data_server" . + run: poetry run python -u ../../sema4ai-python-ls-core/tests/run_tests.py -rfE -otests_output -vv -n 1 -m "not data_server and not rcc_env" . - uses: actions/upload-artifact@v4 if: always() diff --git a/sema4ai-python-ls-core/src/sema4ai_ls_core/core_log.py b/sema4ai-python-ls-core/src/sema4ai_ls_core/core_log.py index 4bf3b9413..c53f96f62 100644 --- a/sema4ai-python-ls-core/src/sema4ai_ls_core/core_log.py +++ b/sema4ai-python-ls-core/src/sema4ai_ls_core/core_log.py @@ -22,13 +22,13 @@ """ import os.path -import traceback -import threading import sys +import threading +import traceback from datetime import datetime -from sema4ai_ls_core.protocols import ILog -from typing import Dict + from sema4ai_ls_core.constants import NULL +from sema4ai_ls_core.protocols import ILog name_to_logger: dict[str, ILog] = {} @@ -39,7 +39,7 @@ def _as_str(s): return s -MAX_LOG_MSG_SIZE = 2000 +MAX_LOG_MSG_SIZE = 20000 try: MAX_LOG_MSG_SIZE = int(os.environ.get("MAX_LOG_MSG_SIZE", MAX_LOG_MSG_SIZE)) except Exception: diff --git a/sema4ai-python-ls-core/src/sema4ai_ls_core/system_mutex.py b/sema4ai-python-ls-core/src/sema4ai_ls_core/system_mutex.py index 707dfbca5..465584ec9 100644 --- a/sema4ai-python-ls-core/src/sema4ai_ls_core/system_mutex.py +++ b/sema4ai-python-ls-core/src/sema4ai_ls_core/system_mutex.py @@ -234,7 +234,7 @@ def __init__( try: try: with open(filename) as stream: - curr_pid = stream.readline().strip()[-1] + curr_pid = stream.readline().strip() except: log.exception("Unable to get locking pid.") curr_pid = "" diff --git a/sema4ai/src/sema4ai_code/robocorp_language_server.py b/sema4ai/src/sema4ai_code/robocorp_language_server.py index 6e2af2bc6..3dcfa25be 100644 --- a/sema4ai/src/sema4ai_code/robocorp_language_server.py +++ b/sema4ai/src/sema4ai_code/robocorp_language_server.py @@ -609,6 +609,11 @@ def _get_linked_account_info(self, params=None) -> ActionResultDict: @command_dispatcher(commands.SEMA4AI_CLOUD_LIST_WORKSPACES_INTERNAL) def _cloud_list_workspaces( self, params: CloudListWorkspaceDict + ) -> ListWorkspacesActionResultDict: + return require_monitor(partial(self._cloud_list_workspaces_impl, params)) + + def _cloud_list_workspaces_impl( + self, params: CloudListWorkspaceDict, monitor: IMonitor ) -> ListWorkspacesActionResultDict: from sema4ai_ls_core.progress_report import progress_context @@ -663,72 +668,80 @@ def _cloud_list_workspaces( last_error_result = None - with progress_context( - self._endpoint, "Listing Control Room workspaces", self._dir_cache - ): - ws: IRccWorkspace - ret: list[WorkspaceInfoDict] = [] - result = self._rcc.cloud_list_workspaces() - if not result.success: - # It's an error, so, the data should be None. - return typing.cast( - ActionResultDict[list[WorkspaceInfoDict]], result.as_dict() - ) - - workspaces = result.result - for ws in workspaces or []: - packages: list[PackageInfoDict] = [] + try: + with progress_context( + self._endpoint, "Listing Control Room workspaces", self._dir_cache + ): + ws: IRccWorkspace + ret: list[WorkspaceInfoDict] = [] + result = self._rcc.cloud_list_workspaces() + if not result.success: + # It's an error, so, the data should be None. + return typing.cast( + ActionResultDict[list[WorkspaceInfoDict]], result.as_dict() + ) - activity_package: IRccRobotMetadata - activities_result = self._rcc.cloud_list_workspace_robots( - ws.workspace_id - ) - if not activities_result.success: - # If we can't list the robots of a specific workspace, just skip it - # (the log should still show it but we can proceed to list the - # contents of other workspaces). - last_error_result = activities_result - continue + workspaces = result.result + for ws in workspaces or []: + packages: list[PackageInfoDict] = [] - workspace_activities = activities_result.result - for activity_package in workspace_activities or []: - key = (ws.workspace_id, activity_package.robot_id) - sort_key = "%05d%s" % ( - ws_id_and_pack_id_to_lru_index.get(key, DEFAULT_SORT_KEY), - activity_package.robot_name.lower(), + activity_package: IRccRobotMetadata + activities_result = self._rcc.cloud_list_workspace_robots( + ws.workspace_id ) + if not activities_result.success: + # If we can't list the robots of a specific workspace, just skip it + # (the log should still show it but we can proceed to list the + # contents of other workspaces). + last_error_result = activities_result + continue + + workspace_activities = activities_result.result + for activity_package in workspace_activities or []: + key = (ws.workspace_id, activity_package.robot_id) + sort_key = "%05d%s" % ( + ws_id_and_pack_id_to_lru_index.get(key, DEFAULT_SORT_KEY), + activity_package.robot_name.lower(), + ) - package_info = { - "name": activity_package.robot_name, - "id": activity_package.robot_id, - "sortKey": sort_key, + package_info = { + "name": activity_package.robot_name, + "id": activity_package.robot_id, + "sortKey": sort_key, + "organizationName": ws.organization_name, + "workspaceId": ws.workspace_id, + "workspaceName": ws.workspace_name, + } + packages.append(package_info) + + ws_dict = { "organizationName": ws.organization_name, - "workspaceId": ws.workspace_id, "workspaceName": ws.workspace_name, + "workspaceId": ws.workspace_id, + "packages": packages, } - packages.append(package_info) + ret.append(ws_dict) - ws_dict = { - "organizationName": ws.organization_name, - "workspaceName": ws.workspace_name, - "workspaceId": ws.workspace_id, - "packages": packages, - } - ret.append(ws_dict) - - if not ret and last_error_result is not None: - # It's an error, so, the data should be None. - return typing.cast( - ActionResultDict[list[WorkspaceInfoDict]], last_error_result.as_dict() - ) + if not ret and last_error_result is not None: + # It's an error, so, the data should be None. + return typing.cast( + ActionResultDict[list[WorkspaceInfoDict]], + last_error_result.as_dict(), + ) - if ret: # Only store if we got something. - store: ListWorkspaceCachedInfoDict = { - "ws_info": ret, - "account_cache_key": account_cache_key, + if ret: # Only store if we got something. + store: ListWorkspaceCachedInfoDict = { + "ws_info": ret, + "account_cache_key": account_cache_key, + } + self._dir_cache.store(self.CLOUD_LIST_WORKSPACE_CACHE_KEY, store) + return {"success": True, "message": None, "result": ret} + except Exception as e: + return { + "success": False, + "message": f"Error listing workspaces: {e}", + "result": None, } - self._dir_cache.store(self.CLOUD_LIST_WORKSPACE_CACHE_KEY, store) - return {"success": True, "message": None, "result": ret} @command_dispatcher(commands.SEMA4AI_CREATE_ROBOT_INTERNAL) def _create_robot(self, params: CreateRobotParamsDict) -> ActionResultDict: diff --git a/sema4ai/tests/pytest.ini b/sema4ai/tests/pytest.ini index d2095193f..162655efa 100644 --- a/sema4ai/tests/pytest.ini +++ b/sema4ai/tests/pytest.ini @@ -5,3 +5,4 @@ timeout=1800 ; https://docs.pytest.org/en/stable/how-to/mark.html markers = data_server: marks data server tests (deselect with '-m "not data_server"') + rcc_env: marks rcc environment tests (deselect with '-m "not rcc_env"') diff --git a/sema4ai/tests/sema4ai_code_tests/data_server_fixtures.py b/sema4ai/tests/sema4ai_code_tests/data_server_fixtures.py index 95f92a715..5aeae24a5 100644 --- a/sema4ai/tests/sema4ai_code_tests/data_server_fixtures.py +++ b/sema4ai/tests/sema4ai_code_tests/data_server_fixtures.py @@ -169,6 +169,15 @@ def _is_current_db_data_valid( @pytest.fixture(scope="session") def data_server_cli(request, tmpdir_factory) -> Iterator["DataServerCliWrapper"]: + import os + + if os.getenv("GITHUB_ACTIONS"): + # Detect if running in github actions + if not os.getenv("PYTEST_CAN_RUN_DATA_SERVER"): + raise RuntimeError( + "Test must be marked with @pytest.mark.data_server to run in github actions to use `data_server_cli` fixture" + ) + from sema4ai_code_tests.data_server_cli_wrapper import DataServerCliWrapper from sema4ai_ls_core.system_mutex import timed_acquire_mutex diff --git a/sema4ai/tests/sema4ai_code_tests/test_compute_data_sources_state.py b/sema4ai/tests/sema4ai_code_tests/test_compute_data_sources_state.py index 10756c4dd..016cd9fae 100644 --- a/sema4ai/tests/sema4ai_code_tests/test_compute_data_sources_state.py +++ b/sema4ai/tests/sema4ai_code_tests/test_compute_data_sources_state.py @@ -262,7 +262,6 @@ def setup_data_source(datasource: DatasourceInfoTypedDict): data_regression.check(fixed_result, basename="missing_data_source_none") -@pytest.mark.data_server def setup_sqlite_data_source(data_server_cli, tmpdir): import json @@ -275,6 +274,7 @@ def setup_sqlite_data_source(data_server_cli, tmpdir): ) +@pytest.mark.data_server def test_compute_data_sources_state( data_server_cli: DataServerCliWrapper, ws_root_path, diff --git a/sema4ai/tests/sema4ai_code_tests/test_data_sources_available.py b/sema4ai/tests/sema4ai_code_tests/test_data_sources_available.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/sema4ai/tests/sema4ai_code_tests/test_language_server_directly.py b/sema4ai/tests/sema4ai_code_tests/test_language_server_directly.py index 272cd3f80..eaa9b235d 100644 --- a/sema4ai/tests/sema4ai_code_tests/test_language_server_directly.py +++ b/sema4ai/tests/sema4ai_code_tests/test_language_server_directly.py @@ -39,15 +39,21 @@ def test_cloud_list_workspaces_cache_invalidate( rcc = language_server._rcc rcc._last_verified_account_info = AccountInfo("default account", "123", "", "") - assert language_server._cloud_list_workspaces({"refresh": False})["success"] + assert language_server._cloud_list_workspaces({"refresh": False})(monitor=NULL)[ + "success" + ] rcc_patch.disallow_calls() - assert language_server._cloud_list_workspaces({"refresh": False})["success"] + assert language_server._cloud_list_workspaces({"refresh": False})(monitor=NULL)[ + "success" + ] rcc._last_verified_account_info = AccountInfo("another account", "123", "", "") # As account changed, the data should be fetched (as we can't due to the patching # the error is expected). - with pytest.raises(AssertionError) as e: - assert not language_server._cloud_list_workspaces({"refresh": False})["success"] + result = language_server._cloud_list_workspaces({"refresh": False})(monitor=NULL) + assert not result["success"] - assert "This should not be called at this time (data should be cached)." in str(e) + assert "This should not be called at this time (data should be cached)." in str( + result["message"] + ) diff --git a/sema4ai/tests/sema4ai_code_tests/test_rcc.py b/sema4ai/tests/sema4ai_code_tests/test_rcc.py index 057c9f85d..d4de15f1d 100644 --- a/sema4ai/tests/sema4ai_code_tests/test_rcc.py +++ b/sema4ai/tests/sema4ai_code_tests/test_rcc.py @@ -103,6 +103,7 @@ def custom_handler(args, *sargs, **kwargs): assert [ws.workspace_name for ws in workspaces_listed] == ["Ex3"] +@pytest.mark.rcc_env def test_rcc_cloud(rcc: IRcc, ci_credentials: str, tmpdir): """ Note on the setup: @@ -243,6 +244,7 @@ def conda_yaml_contents(self): return self.conda_yaml.read_text("utf-8") +@pytest.mark.rcc_env def test_get_robot_yaml_environ(rcc: IRcc, datadir, holotree_manager): from sema4ai_ls_core.protocols import ActionResult @@ -386,6 +388,7 @@ def test_get_robot_yaml_environ(rcc: IRcc, datadir, holotree_manager): ) +@pytest.mark.rcc_env def test_get_robot_yaml_environ_not_ok(rcc: IRcc, datadir, holotree_manager): # Test what happens when things go don't go as planned (i.e.: an environment # cannot be created). diff --git a/sema4ai/tests/sema4ai_code_tests/test_resolve_interpreter.py b/sema4ai/tests/sema4ai_code_tests/test_resolve_interpreter.py index b3b700776..c6f7a0f10 100644 --- a/sema4ai/tests/sema4ai_code_tests/test_resolve_interpreter.py +++ b/sema4ai/tests/sema4ai_code_tests/test_resolve_interpreter.py @@ -8,6 +8,7 @@ from sema4ai_ls_core.unittest_tools.cases_fixture import CasesFixture +@pytest.mark.rcc_env def test_resolve_interpreter_with_changing_pythonpath( config_provider: IConfigProvider, rcc_conda_installed, datadir ) -> None: @@ -63,6 +64,7 @@ def test_resolve_interpreter_with_changing_pythonpath( assert additional_pythonpath_entries2 == (datadir / "robot4" / "new-path",) +@pytest.mark.rcc_env def test_resolve_interpreter_relocate_robot_root( config_provider: IConfigProvider, rcc_conda_installed, datadir ) -> None: @@ -125,6 +127,7 @@ def test_resolve_interpreter_relocate_robot_root( assert Path(interpreter_info2.get_interpreter_id()) == path2 +@pytest.mark.rcc_env def test_resolve_interpreter_environment_config( config_provider: IConfigProvider, rcc_conda_installed, datadir ) -> None: diff --git a/sema4ai/tests/sema4ai_code_tests/test_vscode_integration.py b/sema4ai/tests/sema4ai_code_tests/test_vscode_integration.py index 4de6735f7..edb0e40bf 100644 --- a/sema4ai/tests/sema4ai_code_tests/test_vscode_integration.py +++ b/sema4ai/tests/sema4ai_code_tests/test_vscode_integration.py @@ -8,6 +8,8 @@ from unittest import mock import pytest +from sema4ai_code_tests.fixtures import RCC_TEMPLATE_NAMES, RccPatch +from sema4ai_code_tests.protocols import IRobocorpLanguageServerClient from sema4ai_ls_core import uris from sema4ai_ls_core.basic import wait_for_condition from sema4ai_ls_core.callbacks import Callback @@ -31,8 +33,6 @@ LocalPackageMetadataInfoDict, WorkspaceInfoDict, ) -from sema4ai_code_tests.fixtures import RCC_TEMPLATE_NAMES, RccPatch -from sema4ai_code_tests.protocols import IRobocorpLanguageServerClient log = logging.getLogger(__name__) @@ -640,6 +640,7 @@ def custom_handler(args, *sargs, **kwargs): data_regression.check(result3, basename="test_cloud_list_workspaces_basic") +@pytest.mark.rcc_env def test_cloud_list_workspaces_errors_no_ws_available( language_server_initialized: IRobocorpLanguageServerClient, rcc_patch: RccPatch ): @@ -662,6 +663,7 @@ def custom_handler(args, *sargs, **kwargs): assert not result1["success"] +@pytest.mark.rcc_env def test_upload_to_cloud( language_server_initialized: IRobocorpLanguageServerClient, ci_credentials: str, @@ -1080,11 +1082,10 @@ def test_hover_image_integration( ): import base64 + from sema4ai_code_tests.fixtures import IMAGE_IN_BASE64 from sema4ai_ls_core import uris from sema4ai_ls_core.workspace import Document - from sema4ai_code_tests.fixtures import IMAGE_IN_BASE64 - locators_json = tmpdir.join("locators.json") locators_json.write_text("", "utf-8") @@ -1680,11 +1681,10 @@ def test_web_inspector_integrated( This test should be a reference spanning all the APIs that are available for the inspector webview to use. """ - from sema4ai_ls_core import uris - from sema4ai_code_tests.robocode_language_server_client import ( RobocorpLanguageServerClient, ) + from sema4ai_ls_core import uris cases.copy_to("robots", ws_root_path) ls_client: RobocorpLanguageServerClient = language_server_initialized @@ -1983,6 +1983,7 @@ def test_list_organizations( @pytest.mark.timeout(60 * 5) +@pytest.mark.rcc_env def test_package_build( language_server_initialized: IRobocorpLanguageServerClient, cases: CasesFixture, @@ -2119,6 +2120,7 @@ def test_package_set_changelog( @pytest.mark.timeout(60) +@pytest.mark.rcc_env def test_package_metadata( language_server_initialized: IRobocorpLanguageServerClient, cases: CasesFixture, @@ -2929,6 +2931,7 @@ def test_import_action_packages(language_server_initialized, tmpdir, datadir) -> assert os.listdir(Path(created_dir)) +@pytest.mark.rcc_env def test_run_dev_task( language_server_initialized, cases: CasesFixture, data_regression ) -> None: