From 00a019fac7a2b4cc1bb49a570f71cb84274c56a2 Mon Sep 17 00:00:00 2001 From: Ping-Lin Chang Date: Tue, 24 Jun 2025 02:43:36 +0100 Subject: [PATCH] test(instill): improve unit test coverage --- .coveragerc | 1 + .github/CONTRIBUTING.md | 4 - .github/workflows/build.yml | 3 - .github/workflows/publish.yml | 9 +- Makefile | 32 +- README.md | 4 +- instill/clients/constant.py | 1 - instill/{configuration => config}/__init__.py | 6 +- instill/helpers/test.py | 25 - instill/resources/const.py | 5 - instill/resources/errors.py | 3 - instill/resources/model.py | 13 +- instill/resources/pipeline.py | 9 +- instill/resources/resource.py | 7 +- instill/tests/__init__.py | 2 +- {tests => instill/tests}/test_cli.py | 75 +- instill/tests/test_config.py | 531 +++++++++++++ {tests => instill/tests}/test_integration.py | 0 instill/tests/test_resources.py | 728 ++++++++++++++++++ instill/tests/test_utils.py | 515 +++++++++++++ notebooks/asyncio_example.py | 2 +- notebooks/create_pipeline.ipynb | 2 +- notebooks/serve_github_model.ipynb | 2 +- scent.py | 3 +- tests/__init__.py | 1 - tests/conftest.py | 18 - tests/test_client.py | 90 --- 27 files changed, 1874 insertions(+), 217 deletions(-) delete mode 100644 instill/clients/constant.py rename instill/{configuration => config}/__init__.py (90%) delete mode 100644 instill/helpers/test.py delete mode 100644 instill/resources/const.py delete mode 100644 instill/resources/errors.py rename {tests => instill/tests}/test_cli.py (91%) create mode 100644 instill/tests/test_config.py rename {tests => instill/tests}/test_integration.py (100%) create mode 100644 instill/tests/test_resources.py create mode 100644 instill/tests/test_utils.py delete mode 100644 tests/__init__.py delete mode 100644 tests/conftest.py delete mode 100644 tests/test_client.py diff --git a/.coveragerc b/.coveragerc index 45ea722..8511d5c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -8,6 +8,7 @@ omit = .venv/* */tests/* */__main__.py + instill/protogen/* [report] diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 231d5a0..1ad7057 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -27,10 +27,6 @@ Before delving into the details to come up with your first PR, please familiaris To confirm these system dependencies are configured correctly: -```shell -make doctor -``` - #### Installation Fetch git submodules: diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cf86047..475e251 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,9 +23,6 @@ jobs: with: poetry-version: 2.1.2 - - name: Check dependencies - run: make doctor - - uses: actions/cache@v4 with: path: .venv diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 123e48c..a394acc 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -18,12 +18,11 @@ jobs: - name: Set up Python uses: actions/setup-python@v4 with: - python-version: "3.11" + python-version: "3.12" - - uses: Gr1N/setup-poetry@v8 - - - name: Check dependencies - run: make doctor + - uses: abatilo/actions-poetry@v4 + with: + poetry-version: 2.1.2 - uses: actions/cache@v3 with: diff --git a/Makefile b/Makefile index 4779027..5b3492e 100644 --- a/Makefile +++ b/Makefile @@ -67,9 +67,6 @@ PYTEST_OPTIONS += --cov-report=xml endif PYTEST_RERUN_OPTIONS := --last-failed --exitfirst -.PHONY: test -test: test-all ## Run unit and integration tests - .PHONY: test-unit test-unit: install @ ( mv $(FAILURES) $(FAILURES).bak || true ) > /dev/null 2>&1 @@ -79,23 +76,8 @@ ifndef DISABLE_COVERAGE poetry run coveragespace update unit endif -.PHONY: test-int -test-int: install - @ if test -e $(FAILURES); then poetry run pytest tests $(PYTEST_RERUN_OPTIONS); fi - @ rm -rf $(FAILURES) - poetry run pytest tests $(PYTEST_OPTIONS) -ifndef DISABLE_COVERAGE - poetry run coveragespace update integration -endif - -.PHONY: test-all -test-all: install - @ if test -e $(FAILURES); then poetry run pytest $(PACKAGE) tests $(PYTEST_RERUN_OPTIONS); fi - @ rm -rf $(FAILURES) - poetry run pytest $(PACKAGE) tests $(PYTEST_OPTIONS) -ifndef DISABLE_COVERAGE - poetry run coveragespace update overall -endif +.PHONY: test +test: test-unit .PHONY: read-coverage read-coverage: @@ -105,8 +87,8 @@ read-coverage: .PHONY: format format: install - poetry run isort $(PACKAGE) tests notebooks - poetry run black $(PACKAGE) tests notebooks + poetry run isort $(PACKAGE) notebooks + poetry run black $(PACKAGE) notebooks @ echo .PHONY: check @@ -114,9 +96,9 @@ check: install format ## Run formatters, linters, and static analysis ifdef CI git diff --exit-code endif - poetry run mypy $(PACKAGE) tests - poetry run pylint $(PACKAGE) tests --rcfile=.pylint.ini - poetry run pydocstyle $(PACKAGE) tests + poetry run mypy $(PACKAGE) + poetry run pylint $(PACKAGE) --rcfile=.pylint.ini + poetry run pydocstyle $(PACKAGE) # DOCUMENTATION ############################################################### diff --git a/README.md b/README.md index 757ec90..4195cbc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Instill Core Python SDK -[![Unix Build Status](https://img.shields.io/github/actions/workflow/status/instill-ai/python-sdk/build.yml?branch=main&label=linux)](https://github.com/instill-ai/python-sdk/actions) [![Coverage Status](https://img.shields.io/codecov/c/gh/instill-ai/python-sdk)](https://codecov.io/gh/instill-ai/python-sdk) [![PyPI License](https://img.shields.io/pypi/l/instill-sdk.svg?color=lightgreen)](https://pypi.org/project/instill-sdk) [![PyPI Version](https://img.shields.io/pypi/v/instill-sdk.svg?color=lightgreen)](https://pypi.org/project/instill-sdk) [![PyPI Downloads](https://img.shields.io/pypi/dm/instill-sdk.svg?color=lightgreen)](https://pypistats.org/packages/instill-sdk) +[![Unix Build Status](https://img.shields.io/github/actions/workflow/status/instill-ai/python-sdk/build.yml?branch=main&label=linux)](https://github.com/instill-ai/python-sdk/actions) [![Coverage Status](https://img.shields.io/codecov/c/gh/instill-ai/python-sdk)](https://codecov.io/gh/instill-ai/python-sdk) [![PyPI License](https://img.shields.io/pypi/l/instill-sdk.svg?color=blue)](https://pypi.org/project/instill-sdk) [![PyPI Version](https://img.shields.io/pypi/v/instill-sdk.svg?color=blue)](https://pypi.org/project/instill-sdk) [![PyPI Downloads](https://img.shields.io/pypi/dm/instill-sdk.svg?color=blue)](https://pypistats.org/packages/instill-sdk) > [!IMPORTANT] > **This SDK tool is under active development** @@ -103,7 +103,7 @@ hosts: If you do not like the idea of having to create a config file, you can also setup your target instance by doing the following at the very beginning of your script. ```python -from instill.configuration import global_config +from instill.config import global_config global_config.set_default( url="api.instill-ai.com", diff --git a/instill/clients/constant.py b/instill/clients/constant.py deleted file mode 100644 index b158036..0000000 --- a/instill/clients/constant.py +++ /dev/null @@ -1 +0,0 @@ -DEFAULT_INSTANCE: str = "default" diff --git a/instill/configuration/__init__.py b/instill/config/__init__.py similarity index 90% rename from instill/configuration/__init__.py rename to instill/config/__init__.py index 0074ee9..ce2d2b8 100644 --- a/instill/configuration/__init__.py +++ b/instill/config/__init__.py @@ -48,7 +48,9 @@ def load(self) -> None: return try: with open(path, "r", encoding="utf-8") as c: - self._config = _Config.validate(yaml.load(c, Loader=yaml.FullLoader)) + self._config = _Config.model_validate( + yaml.load(c, Loader=yaml.FullLoader) + ) except Exception as e: raise BaseException(f"Invalid configuration file at '{path}'") from e @@ -60,7 +62,7 @@ def save(self) -> None: with open(path, "w", encoding="utf-8") as c: yaml.dump( json.loads( - self._config.json( + self._config.model_dump_json( exclude_none=True, ) ), diff --git a/instill/helpers/test.py b/instill/helpers/test.py deleted file mode 100644 index 02db46c..0000000 --- a/instill/helpers/test.py +++ /dev/null @@ -1,25 +0,0 @@ -import argparse -import json -import pprint - -import requests - -from instill.utils.logger import Logger - -parser = argparse.ArgumentParser() - -parser.add_argument( - "-i", - "--input", - help="inference input json", - required=True, -) - -args = parser.parse_args() - -resp = requests.post("http://127.0.0.1:8000/", json=json.loads(args.input), timeout=600) - -if resp.status_code == 200: - Logger.i(f"[Instill] Outputs:\n{pprint.pformat(resp.json())}") -else: - Logger.e(f"[Instill] Errors:\n{pprint.pformat(resp.text)}") diff --git a/instill/resources/const.py b/instill/resources/const.py deleted file mode 100644 index 0d9145f..0000000 --- a/instill/resources/const.py +++ /dev/null @@ -1,5 +0,0 @@ -import os - -INSTILL_MODEL_INTERNAL_MODE = "Internal Mode" -INSTILL_MODEL_EXTERNAL_MODE = "External Mode" -SPEC_PATH = f"{os.path.dirname(__file__)}/schema/jsons" diff --git a/instill/resources/errors.py b/instill/resources/errors.py deleted file mode 100644 index 35c564e..0000000 --- a/instill/resources/errors.py +++ /dev/null @@ -1,3 +0,0 @@ -class WrongModeException(Exception): - def __str__(self) -> str: - return "Instill Model Connector mode error" diff --git a/instill/resources/model.py b/instill/resources/model.py index eec6539..bd453fc 100644 --- a/instill/resources/model.py +++ b/instill/resources/model.py @@ -1,4 +1,7 @@ -# pylint: disable=no-member,wrong-import-position,no-name-in-module +"""Model resource module.""" + +# pylint: disable=no-member + from typing import Optional import instill.protogen.model.model.v1alpha.model_definition_pb2 as model_definition_interface @@ -54,13 +57,17 @@ def resource(self): return self._resource @resource.setter - def resource(self, resource: model_interface.Model): + def resource( + self, resource: Optional[model_interface.Model] + ): # pylint: disable=no-member self._resource = resource def _update(self): self.resource = self.client.model.get_model(model_name=self.resource.id).model - def get_definition(self) -> model_definition_interface.ModelDefinition: + def get_definition( + self, + ) -> model_definition_interface.ModelDefinition: # pylint: disable=no-member return self.resource.model_definition def delete(self, silent: bool = False): diff --git a/instill/resources/pipeline.py b/instill/resources/pipeline.py index daa52be..a303bee 100644 --- a/instill/resources/pipeline.py +++ b/instill/resources/pipeline.py @@ -1,4 +1,7 @@ -# pylint: disable=no-member,wrong-import-position,no-name-in-module +"""Pipeline resource module.""" + +# pylint: disable=no-name-in-module,no-member + from typing import Optional, Tuple, Union import grpc @@ -68,7 +71,9 @@ def resource(self): return self._resource @resource.setter - def resource(self, resource: pipeline_interface.Pipeline): + def resource( + self, resource: Optional[pipeline_interface.Pipeline] + ): # pylint: disable=no-member self._resource = resource def _update(self): diff --git a/instill/resources/resource.py b/instill/resources/resource.py index b291d91..1d40bad 100644 --- a/instill/resources/resource.py +++ b/instill/resources/resource.py @@ -1,15 +1,10 @@ """Base resource interface module.""" -# pylint: disable=no-member,wrong-import-position from abc import ABC, abstractmethod class Resource(ABC): - """Base interface class for creating resources. - - Args: - ABC (abc.ABCMeta): std abstract class - """ + """Base interface class for creating resources.""" @property @abstractmethod diff --git a/instill/tests/__init__.py b/instill/tests/__init__.py index 2a67748..a916f18 100644 --- a/instill/tests/__init__.py +++ b/instill/tests/__init__.py @@ -1 +1 @@ -"""Unit tests for the package.""" +"""Integration tests for the package.""" diff --git a/tests/test_cli.py b/instill/tests/test_cli.py similarity index 91% rename from tests/test_cli.py rename to instill/tests/test_cli.py index 253dc71..febde8b 100644 --- a/tests/test_cli.py +++ b/instill/tests/test_cli.py @@ -122,9 +122,9 @@ def test_add_build_parser(self): @patch("instill.helpers.commands.build.Logger") @patch("instill.helpers.commands.build.open", new_callable=mock_open) - def test_build_missing_config_file(self, mock_open_func, mock_logger): + def test_build_missing_config_file(self, mock_open, mock_logger): """Test build when config file is missing.""" - mock_open_func.side_effect = FileNotFoundError("Config file not found") + mock_open.side_effect = FileNotFoundError("Config file not found") args = MagicMock() args.name = "test/model" @@ -145,8 +145,9 @@ def test_build_missing_config_file(self, mock_open_func, mock_logger): assert len(error_calls) == 1 @patch("instill.helpers.commands.build.open", new_callable=mock_open) + @patch("instill.helpers.commands.build.Logger") @patch("instill.helpers.commands.build.yaml.safe_load") - def test_build_invalid_config(self, mock_yaml_load, mock_open_func): + def test_build_invalid_config(self, mock_logger, mock_yaml_load, mock_open): """Test build with invalid configuration.""" mock_yaml_load.return_value = {"build": {"gpu": None}} @@ -163,7 +164,7 @@ def test_build_invalid_config(self, mock_yaml_load, mock_open_func): @patch("instill.helpers.commands.build.open", new_callable=mock_open) @patch("instill.helpers.commands.build.Logger") @patch("instill.helpers.commands.build.yaml.safe_load") - def test_build_gpu_on_arm64(self, mock_yaml_load, mock_logger, mock_open_func): + def test_build_gpu_on_arm64(self, mock_yaml_load, mock_logger, mock_open): """Test build with GPU on ARM64 architecture.""" mock_yaml_load.return_value = { "build": {"gpu": True, "llm_runtime": "transformers"} @@ -187,23 +188,24 @@ def test_build_gpu_on_arm64(self, mock_yaml_load, mock_logger, mock_open_func): ] assert len(error_calls) == 1 + @patch("subprocess.run") @patch("instill.helpers.commands.build.open", new_callable=mock_open) - @patch("instill.helpers.commands.build.subprocess.run") @patch("instill.helpers.commands.build.shutil.copytree") @patch("instill.helpers.commands.build.shutil.copyfile") @patch("instill.helpers.commands.build.os.getcwd") @patch("instill.helpers.commands.build.tempfile.TemporaryDirectory") @patch("instill.helpers.commands.build.yaml.safe_load") - @patch("instill.__version__", "0.17.2") + @patch("instill.helpers.commands.build.Logger") def test_build_with_sdk_wheel( self, + mock_logger, mock_yaml_load, mock_tempdir, mock_getcwd, mock_copyfile, mock_copytree, + mock_open, mock_subprocess, - mock_open_func, ): """Test build with SDK wheel.""" mock_yaml_load.return_value = { @@ -218,15 +220,50 @@ def test_build_with_sdk_wheel( args.name = "test/model" args.no_cache = False args.target_arch = "amd64" - args.sdk_wheel = "/path/to/wheel.whl" + args.sdk_wheel = "/path/to/instill_sdk-0.17.2-py3-none-any.whl" args.editable_project = None build(args) - # Verify SDK wheel was copied - mock_copyfile.assert_any_call( - "/path/to/wheel.whl", "/tmp/test/instill_sdk-0.17.2dev-py3-none-any.whl" - ) + # Verify SDK wheel was copied - check for any wheel copy call + wheel_calls = [ + call + for call in mock_copyfile.call_args_list + if "instill_sdk" in str(call) and "dev-py3-none-any.whl" in str(call) + ] + assert ( + len(wheel_calls) == 1 + ), f"Expected 1 wheel copy call, found {len(wheel_calls)}" + + # Verify other expected copy operations occurred + # Check that dockerfile was copied + dockerfile_calls = [ + call + for call in mock_copyfile.call_args_list + if "Dockerfile.transformers" in str(call) + ] + assert len(dockerfile_calls) == 1 + + # Check that .dockerignore was copied + dockerignore_calls = [ + call + for call in mock_copyfile.call_args_list + if ".dockerignore" in str(call) + ] + assert len(dockerignore_calls) == 1 + + # Check that model.py was copied as _model.py + model_calls = [ + call for call in mock_copyfile.call_args_list if "_model.py" in str(call) + ] + assert len(model_calls) == 1 + + # Verify the build command was executed + mock_subprocess.assert_called_once() + build_call = mock_subprocess.call_args[0][0] + assert build_call[0] == "docker" + assert build_call[1] == "buildx" + assert build_call[2] == "build" @patch("instill.helpers.commands.build.open", new_callable=mock_open) @patch("instill.helpers.commands.build.find_project_root") @@ -236,8 +273,10 @@ def test_build_with_sdk_wheel( @patch("instill.helpers.commands.build.os.getcwd") @patch("instill.helpers.commands.build.tempfile.TemporaryDirectory") @patch("instill.helpers.commands.build.yaml.safe_load") + @patch("instill.helpers.commands.build.Logger") def test_build_with_editable_project( self, + mock_logger, mock_yaml_load, mock_tempdir, mock_getcwd, @@ -245,7 +284,7 @@ def test_build_with_editable_project( mock_copytree, mock_subprocess, mock_find_project_root, - mock_open_func, + mock_open, ): """Test build with editable project.""" mock_yaml_load.return_value = { @@ -281,8 +320,10 @@ def test_build_with_editable_project( @patch("instill.helpers.commands.build.os.getcwd") @patch("instill.helpers.commands.build.tempfile.TemporaryDirectory") @patch("instill.helpers.commands.build.yaml.safe_load") + @patch("instill.helpers.commands.build.Logger") def test_build_with_editable_project_custom_name( self, + mock_logger, mock_yaml_load, mock_tempdir, mock_getcwd, @@ -290,7 +331,7 @@ def test_build_with_editable_project_custom_name( mock_copytree, mock_subprocess, mock_find_project_root, - mock_open_func, + mock_open, ): """Test build with editable project that has a custom name.""" mock_yaml_load.return_value = { @@ -409,7 +450,8 @@ def test_add_run_parser(self): @patch("instill.helpers.commands.run.subprocess.run") @patch("instill.helpers.commands.run.uuid.uuid4") - def test_run_cpu_mode(self, mock_uuid, mock_subprocess): + @patch("instill.helpers.commands.run.Logger") + def test_run_cpu_mode(self, mock_logger, mock_uuid, mock_subprocess): """Test run in CPU mode.""" mock_uuid.return_value = "test-uuid" mock_subprocess.return_value.returncode = 0 @@ -440,7 +482,8 @@ def test_run_cpu_mode(self, mock_uuid, mock_subprocess): @patch("instill.helpers.commands.run.subprocess.run") @patch("instill.helpers.commands.run.uuid.uuid4") - def test_run_gpu_mode(self, mock_uuid, mock_subprocess): + @patch("instill.helpers.commands.run.Logger") + def test_run_gpu_mode(self, mock_logger, mock_uuid, mock_subprocess): """Test run in GPU mode.""" mock_uuid.return_value = "test-uuid" mock_subprocess.return_value.returncode = 0 diff --git a/instill/tests/test_config.py b/instill/tests/test_config.py new file mode 100644 index 0000000..5143154 --- /dev/null +++ b/instill/tests/test_config.py @@ -0,0 +1,531 @@ +"""Unit tests for the instill.config module.""" + +# pylint: disable=consider-using-with + +import importlib +import os +import shutil +import tempfile +from pathlib import Path +from unittest.mock import patch + +import pytest +import yaml + +import instill.config +from instill.config import Configuration, _Config, _InstillHost +from instill.helpers.const import HOST_URL_PROD + + +class TestInstillHost: + """Test cases for the _InstillHost model.""" + + def test_instill_host_creation(self): + """Test creating an _InstillHost instance.""" + host = _InstillHost(url="https://test.com", secure=True, token="test-token") + + assert host.url == "https://test.com" + assert host.secure is True + assert host.token == "test-token" + + def test_instill_host_default_values(self): + """Test _InstillHost with default values.""" + host = _InstillHost(url="https://test.com", secure=False, token="") + + assert host.url == "https://test.com" + assert host.secure is False + assert host.token == "" + + def test_instill_host_validation(self): + """Test _InstillHost validation.""" + # Should not raise any exceptions for valid data + _InstillHost(url="https://test.com", secure=True, token="test-token") + _InstillHost(url="http://localhost:8080", secure=False, token="") + + +class TestConfig: + """Test cases for the _Config model.""" + + def test_config_creation(self): + """Test creating an _Config instance.""" + config = _Config() + assert not config.hosts + + def test_config_with_hosts(self): + """Test _Config with hosts.""" + hosts = { + "default": _InstillHost( + url="https://test.com", secure=True, token="test-token" + ), + "custom": _InstillHost( + url="https://custom.com", secure=False, token="custom-token" + ), + } + config = _Config(hosts=hosts) + + assert len(config.hosts) == 2 + assert "default" in config.hosts + assert "custom" in config.hosts + assert config.hosts["default"].url == "https://test.com" + assert config.hosts["custom"].url == "https://custom.com" + + +class TestConfiguration: + """Test cases for the Configuration class.""" + + def setup_method(self): + """Set up test fixtures.""" + # Create a temporary directory for config files + self.temp_dir = tempfile.TemporaryDirectory() + self.config_dir = Path(self.temp_dir.name) / "instill" / "sdk" / "python" + self.config_dir.mkdir(parents=True, exist_ok=True) + self.config_file = self.config_dir / "config.yml" + + def teardown_method(self): + """Clean up test fixtures.""" + self.temp_dir.cleanup() + + def test_configuration_initialization(self): + """Test Configuration initialization.""" + with patch("instill.config.CONFIG_DIR", self.config_dir): + config = Configuration() + + # Test that config has the expected structure and default host + assert hasattr(config, "_config") + assert "default" in config.hosts + assert config.hosts["default"].url == HOST_URL_PROD + assert config.hosts["default"].secure is True + assert config.hosts["default"].token == "" + + def test_configuration_load_nonexistent_file(self): + """Test loading configuration when file doesn't exist.""" + with patch("instill.config.CONFIG_DIR", self.config_dir): + config = Configuration() + + # When no config file exists, a default host should still be created + assert "default" in config.hosts + assert config.hosts["default"].url == HOST_URL_PROD + assert config.hosts["default"].secure is True + assert config.hosts["default"].token == "" + + def test_configuration_load_valid_file(self): + """Test loading configuration from a valid YAML file.""" + with patch("instill.config.CONFIG_DIR", self.config_dir): + # Create a valid config file + config_data = { + "hosts": { + "default": { + "url": "https://custom.com", + "secure": False, + "token": "custom-token", + }, + "test": { + "url": "https://test.com", + "secure": True, + "token": "test-token", + }, + } + } + + with open(self.config_file, "w", encoding="utf-8") as f: + yaml.dump(config_data, f) + + config = Configuration() + + assert len(config.hosts) == 2 + assert config.hosts["default"].url == "https://custom.com" + assert config.hosts["default"].secure is False + assert config.hosts["default"].token == "custom-token" + assert config.hosts["test"].url == "https://test.com" + assert config.hosts["test"].secure is True + assert config.hosts["test"].token == "test-token" + + def test_configuration_load_file_without_default(self): + """Test loading configuration from file that doesn't have a default host.""" + with patch("instill.config.CONFIG_DIR", self.config_dir): + # Create a config file without a default host + config_data = { + "hosts": { + "test": { + "url": "https://test.com", + "secure": True, + "token": "test-token", + } + } + } + + with open(self.config_file, "w", encoding="utf-8") as f: + yaml.dump(config_data, f) + + config = Configuration() + + # Should have both the loaded host and a default host + assert len(config.hosts) == 2 + assert "default" in config.hosts + assert config.hosts["default"].url == HOST_URL_PROD + assert config.hosts["default"].secure is True + assert config.hosts["default"].token == "" + assert config.hosts["test"].url == "https://test.com" + assert config.hosts["test"].secure is True + assert config.hosts["test"].token == "test-token" + + def test_configuration_load_invalid_file(self): + """Test loading configuration from an invalid YAML file.""" + with patch("instill.config.CONFIG_DIR", self.config_dir): + # Create an invalid config file + with open(self.config_file, "w", encoding="utf-8") as f: + f.write("invalid: yaml: content: [") + + with pytest.raises(BaseException, match="Invalid configuration file"): + Configuration() + + def test_configuration_load_invalid_schema(self): + """Test loading configuration with invalid schema.""" + with patch("instill.config.CONFIG_DIR", self.config_dir): + # Create a config file with invalid schema + config_data = { + "hosts": { + "default": { + "url": "https://test.com", + # Missing required fields + } + } + } + + with open(self.config_file, "w", encoding="utf-8") as f: + yaml.dump(config_data, f) + + with pytest.raises(BaseException, match="Invalid configuration file"): + Configuration() + + def test_configuration_save(self): + """Test saving configuration to file.""" + with patch("instill.config.CONFIG_DIR", self.config_dir): + config = Configuration() + + # Add a custom host + config.set_default("https://saved.com", "saved-token", False) + + # Save configuration + config.save() + + # Verify file was created + assert self.config_file.exists() + + # Load the saved configuration + with open(self.config_file, "r", encoding="utf-8") as f: + saved_data = yaml.load(f, Loader=yaml.FullLoader) + + assert "hosts" in saved_data + assert "default" in saved_data["hosts"] + assert saved_data["hosts"]["default"]["url"] == "https://saved.com" + assert saved_data["hosts"]["default"]["secure"] is False + assert saved_data["hosts"]["default"]["token"] == "saved-token" + + def test_configuration_save_creates_directory(self): + """Test that save creates the config directory if it doesn't exist.""" + # Remove the config directory + + if self.config_dir.exists(): + shutil.rmtree(self.config_dir) + + with patch("instill.config.CONFIG_DIR", self.config_dir): + config = Configuration() + config.save() + + # Verify directory and file were created + assert self.config_dir.exists() + assert self.config_file.exists() + + def test_configuration_set_default(self): + """Test setting the default host configuration.""" + with patch("instill.config.CONFIG_DIR", self.config_dir): + config = Configuration() + + # Set default host + config.set_default("https://new-default.com", "new-token", True) + + assert "default" in config.hosts + assert config.hosts["default"].url == "https://new-default.com" + assert config.hosts["default"].token == "new-token" + assert config.hosts["default"].secure is True + + def test_configuration_set_default_overwrites_existing(self): + """Test that set_default overwrites existing default host.""" + with patch("instill.config.CONFIG_DIR", self.config_dir): + config = Configuration() + + # Set default host initially + config.set_default("https://initial.com", "initial-token", False) + + # Overwrite with new values + config.set_default("https://overwritten.com", "overwritten-token", True) + + assert config.hosts["default"].url == "https://overwritten.com" + assert config.hosts["default"].token == "overwritten-token" + assert config.hosts["default"].secure is True + + def test_configuration_hosts_property(self): + """Test the hosts property.""" + with patch("instill.config.CONFIG_DIR", self.config_dir): + config = Configuration() + + # Add some hosts + config._config.hosts["test1"] = _InstillHost( + url="https://test1.com", secure=True, token="token1" + ) + config._config.hosts["test2"] = _InstillHost( + url="https://test2.com", secure=False, token="token2" + ) + + hosts = config.hosts + + assert len(hosts) == 3 # Including default + assert "default" in hosts + assert "test1" in hosts + assert "test2" in hosts + assert hosts["test1"].url == "https://test1.com" + assert hosts["test2"].url == "https://test2.com" + + def test_configuration_with_environment_variable(self): + """Test configuration with custom config path from environment variable.""" + custom_config_dir = Path(self.temp_dir.name) / "custom" / "config" + custom_config_dir.mkdir(parents=True, exist_ok=True) + + with patch.dict( + os.environ, {"INSTILL_SYSTEM_CONFIG_PATH": str(custom_config_dir)} + ): + # Re-import to get the updated CONFIG_DIR + importlib.reload(instill.config) + + # Create a config file in the custom directory + config_file = custom_config_dir / "config.yml" + config_data = { + "hosts": { + "custom": { + "url": "https://custom-env.com", + "secure": True, + "token": "env-token", + } + } + } + + with open(config_file, "w", encoding="utf-8") as f: + yaml.dump(config_data, f) + + config = instill.config.Configuration() + + # Should have both the loaded host and a default host + assert len(config.hosts) == 2 + assert "default" in config.hosts + assert config.hosts["default"].url == HOST_URL_PROD + assert "custom" in config.hosts + assert config.hosts["custom"].url == "https://custom-env.com" + assert config.hosts["custom"].token == "env-token" + + def test_configuration_save_excludes_none_values(self): + """Test that save excludes None values from the output.""" + with patch("instill.config.CONFIG_DIR", self.config_dir): + config = Configuration() + + # Save configuration + config.save() + + # Load the saved configuration + with open(self.config_file, "r", encoding="utf-8") as f: + saved_data = yaml.load(f, Loader=yaml.FullLoader) + + # Verify no None values in the saved data + def check_no_none_values(data): + if isinstance(data, dict): + for key, value in data.items(): + assert value is not None, f"Found None value for key: {key}" + check_no_none_values(value) + elif isinstance(data, list): + for item in data: + check_no_none_values(item) + + check_no_none_values(saved_data) + + def test_configuration_with_complex_yaml(self): + """Test configuration with complex YAML structure.""" + with patch("instill.config.CONFIG_DIR", self.config_dir): + # Ensure the config directory exists before writing the file + self.config_dir.mkdir(parents=True, exist_ok=True) + + # Create a complex config file + config_data = { + "hosts": { + "production": { + "url": "https://api.instill.tech", + "secure": True, + "token": "prod-token-123", + }, + "staging": { + "url": "https://staging.instill.tech", + "secure": True, + "token": "staging-token-456", + }, + "development": { + "url": "http://localhost:8080", + "secure": False, + "token": "", + }, + } + } + + with open(self.config_file, "w", encoding="utf-8") as f: + yaml.dump(config_data, f) + + config = Configuration() + + # Verify all hosts are loaded correctly (including default) + assert len(config.hosts) == 4 # Including default + assert config.hosts["production"].url == "https://api.instill.tech" + assert config.hosts["production"].token == "prod-token-123" + assert config.hosts["production"].secure is True + assert config.hosts["staging"].url == "https://staging.instill.tech" + assert config.hosts["staging"].token == "staging-token-456" + assert config.hosts["staging"].secure is True + assert config.hosts["development"].url == "http://localhost:8080" + assert config.hosts["development"].token == "" + assert config.hosts["development"].secure is False + # Default host should still be present + assert "default" in config.hosts + assert config.hosts["default"].url == HOST_URL_PROD + + +class TestGlobalConfig: + """Test cases for the global configuration instance.""" + + def test_global_config_singleton(self): + """Test that global_config is a singleton instance.""" + temp_dir = Path(tempfile.mkdtemp()) + + with patch("instill.config.CONFIG_DIR", temp_dir): + # Import the module to get the global instance + + importlib.reload(instill.config) + + # Get the global config instance + config1 = instill.config.global_config + config2 = instill.config.global_config + + assert config1 is config2 + # Test that it has the expected attributes instead of using isinstance + assert hasattr(config1, "hosts") + assert hasattr(config1, "save") + assert hasattr(config1, "set_default") + + def test_global_config_has_default_host(self): + """Test that global_config has a default host.""" + temp_dir = Path(tempfile.mkdtemp()) + + with patch("instill.config.CONFIG_DIR", temp_dir): + # Import the module to get the global instance + + importlib.reload(instill.config) + + config = instill.config.global_config + + assert "default" in config.hosts + assert config.hosts["default"].url == HOST_URL_PROD + assert config.hosts["default"].secure is True + assert config.hosts["default"].token == "" + + +class TestConfigurationIntegration: + """Integration tests for the Configuration class.""" + + def setup_method(self): + """Set up test fixtures.""" + self.temp_dir = tempfile.TemporaryDirectory() + self.config_dir = Path(self.temp_dir.name) / "instill" / "sdk" / "python" + self.config_file = self.config_dir / "config.yml" + + def teardown_method(self): + """Clean up test fixtures.""" + self.temp_dir.cleanup() + + def test_full_configuration_workflow(self): + """Test a complete configuration workflow: create, modify, save, reload.""" + with patch("instill.config.CONFIG_DIR", self.config_dir): + # Ensure the config directory exists before creating Configuration + self.config_dir.mkdir(parents=True, exist_ok=True) + + # Create initial configuration + config1 = Configuration() + + # Add multiple hosts + config1.set_default("https://default.com", "default-token", True) + config1._config.hosts["custom1"] = _InstillHost( + url="https://custom1.com", secure=False, token="token1" + ) + config1._config.hosts["custom2"] = _InstillHost( + url="https://custom2.com", secure=True, token="token2" + ) + + # Save configuration + config1.save() + + # Create new configuration instance (simulating reload) + config2 = Configuration() + + # Verify all hosts are preserved + assert len(config2.hosts) == 3 + assert config2.hosts["default"].url == "https://default.com" + assert config2.hosts["default"].token == "default-token" + assert config2.hosts["default"].secure is True + assert config2.hosts["custom1"].url == "https://custom1.com" + assert config2.hosts["custom1"].token == "token1" + assert config2.hosts["custom1"].secure is False + assert config2.hosts["custom2"].url == "https://custom2.com" + assert config2.hosts["custom2"].token == "token2" + assert config2.hosts["custom2"].secure is True + + def test_configuration_with_complex_yaml(self): + """Test configuration with complex YAML structure.""" + with patch("instill.config.CONFIG_DIR", self.config_dir): + # Ensure the config directory exists before writing the file + self.config_dir.mkdir(parents=True, exist_ok=True) + + # Create a complex config file + config_data = { + "hosts": { + "production": { + "url": "https://api.instill.tech", + "secure": True, + "token": "prod-token-123", + }, + "staging": { + "url": "https://staging.instill.tech", + "secure": True, + "token": "staging-token-456", + }, + "development": { + "url": "http://localhost:8080", + "secure": False, + "token": "", + }, + } + } + + with open(self.config_file, "w", encoding="utf-8") as f: + yaml.dump(config_data, f) + + config = Configuration() + + # Verify all hosts are loaded correctly (including default) + assert len(config.hosts) == 4 # Including default + assert config.hosts["production"].url == "https://api.instill.tech" + assert config.hosts["production"].token == "prod-token-123" + assert config.hosts["production"].secure is True + assert config.hosts["staging"].url == "https://staging.instill.tech" + assert config.hosts["staging"].token == "staging-token-456" + assert config.hosts["staging"].secure is True + assert config.hosts["development"].url == "http://localhost:8080" + assert config.hosts["development"].token == "" + assert config.hosts["development"].secure is False + # Default host should still be present + assert "default" in config.hosts + assert config.hosts["default"].url == HOST_URL_PROD diff --git a/tests/test_integration.py b/instill/tests/test_integration.py similarity index 100% rename from tests/test_integration.py rename to instill/tests/test_integration.py diff --git a/instill/tests/test_resources.py b/instill/tests/test_resources.py new file mode 100644 index 0000000..7389cb9 --- /dev/null +++ b/instill/tests/test_resources.py @@ -0,0 +1,728 @@ +"""Unit tests for resources.""" + +# pylint: disable=unused-argument,unused-variable,no-name-in-module,abstract-class-instantiated + +from unittest.mock import Mock, patch + +import grpc +import pytest +from google.longrunning.operations_pb2 import Operation +from google.protobuf.field_mask_pb2 import FieldMask +from google.protobuf.struct_pb2 import Struct + +from instill.clients import InstillClient +from instill.resources.model import Model +from instill.resources.pipeline import Pipeline +from instill.resources.resource import Resource + + +def describe_resource(): + """Test the base Resource abstract class.""" + + def test_resource_is_abstract(): + """Test that Resource is an abstract base class.""" + with pytest.raises(TypeError): + Resource() # type: ignore + + def test_resource_abstract_methods(): + """Test that Resource has required abstract methods.""" + + # Create a concrete implementation for testing + class ConcreteResource(Resource): + def __init__(self): + self._client = None + self._resource = None + + @property + def client(self): + return self._client + + @client.setter + def client(self, value): + self._client = value + + @property + def resource(self): + return self._resource + + @resource.setter + def resource(self, value): + self._resource = value + + # Should not raise an error + resource = ConcreteResource() + assert resource is not None + + +def describe_model(): + """Test the Model resource class.""" + + @pytest.fixture + def mock_client(): + """Create a mock InstillClient.""" + client = Mock(spec=InstillClient) + client.model = Mock() + return client + + @pytest.fixture + def mock_model_response(): + """Create a mock model response.""" + model = Mock() + model.id = "test-model-id" + model.model_definition = Mock() + return model + + def test_model_initialization_existing_model(mock_client, mock_model_response): + """Test Model initialization when model already exists.""" + # Setup + mock_client.model.get_model.return_value = Mock(model=mock_model_response) + + # Execute + model = Model( + client=mock_client, + name="test-model", + definition="test-definition", + configuration={"key": "value"}, + ) + + # Assert + assert model.client == mock_client + assert model.resource == mock_model_response + mock_client.model.get_model.assert_called_once_with( + model_name="test-model", silent=True + ) + mock_client.model.create_model.assert_not_called() + + def test_model_initialization_new_model(mock_client, mock_model_response): + """Test Model initialization when creating new model.""" + # Setup + mock_client.model.get_model.return_value = None + mock_client.model.create_model.return_value = Mock(model=mock_model_response) + + # Execute + model = Model( + client=mock_client, + name="test-model", + definition="test-definition", + configuration={"key": "value"}, + ) + + # Assert + assert model.client == mock_client + assert model.resource == mock_model_response + mock_client.model.get_model.assert_called_once_with( + model_name="test-model", silent=True + ) + mock_client.model.create_model.assert_called_once_with( + name="test-model", + definition="test-definition", + configuration={"key": "value"}, + ) + + def test_model_initialization_creation_failed(mock_client): + """Test Model initialization when model creation fails.""" + # Setup + mock_client.model.get_model.return_value = None + mock_client.model.create_model.return_value = Mock(model=None) + + # Execute & Assert + with pytest.raises(BaseException, match="model creation failed"): + Model( + client=mock_client, + name="test-model", + definition="test-definition", + configuration={"key": "value"}, + ) + + def test_model_call_success(mock_client, mock_model_response): + """Test Model __call__ method with successful response.""" + # Setup + mock_client.model.get_model.return_value = Mock(model=mock_model_response) + mock_response = Mock() + mock_response.task_outputs = ["output1", "output2"] + mock_client.model.trigger.return_value = mock_response + + model = Model( + client=mock_client, + name="test-model", + definition="test-definition", + configuration={"key": "value"}, + ) + + # Execute + result = model(task_inputs=["input1"], silent=False) + + # Assert + assert result == ["output1", "output2"] + mock_client.model.trigger.assert_called_once_with( + "test-model-id", ["input1"], silent=False + ) + + def test_model_call_no_response(mock_client, mock_model_response): + """Test Model __call__ method with no response.""" + # Setup + mock_client.model.get_model.return_value = Mock(model=mock_model_response) + mock_client.model.trigger.return_value = None + + model = Model( + client=mock_client, + name="test-model", + definition="test-definition", + configuration={"key": "value"}, + ) + + # Execute + result = model(task_inputs=["input1"], silent=True) + + # Assert + assert result is None + mock_client.model.trigger.assert_called_once_with( + "test-model-id", ["input1"], silent=True + ) + + def test_model_properties(mock_client, mock_model_response): + """Test Model property getters and setters.""" + # Setup + mock_client.model.get_model.return_value = Mock(model=mock_model_response) + + model = Model( + client=mock_client, + name="test-model", + definition="test-definition", + configuration={"key": "value"}, + ) + + # Test client property + new_client = Mock(spec=InstillClient) + model.client = new_client + assert model.client == new_client + + # Test resource property + new_resource = Mock() + model.resource = new_resource + assert model.resource == new_resource + + def test_model_update(mock_client, mock_model_response): + """Test Model _update method.""" + # Setup + mock_client.model.get_model.return_value = Mock(model=mock_model_response) + updated_model = Mock() + updated_model.id = "updated-model-id" + mock_client.model.get_model.side_effect = [ + Mock(model=mock_model_response), # First call for initialization + Mock(model=updated_model), # Second call for _update + ] + + model = Model( + client=mock_client, + name="test-model", + definition="test-definition", + configuration={"key": "value"}, + ) + + # Execute + model._update() + + # Assert + assert model.resource == updated_model + assert mock_client.model.get_model.call_count == 2 + + def test_model_get_definition(mock_client, mock_model_response): + """Test Model get_definition method.""" + # Setup + mock_definition = Mock() + mock_model_response.model_definition = mock_definition + mock_client.model.get_model.return_value = Mock(model=mock_model_response) + + model = Model( + client=mock_client, + name="test-model", + definition="test-definition", + configuration={"key": "value"}, + ) + + # Execute + result = model.get_definition() + + # Assert + assert result == mock_definition + + def test_model_delete(mock_client, mock_model_response): + """Test Model delete method.""" + # Setup + mock_client.model.get_model.return_value = Mock(model=mock_model_response) + + model = Model( + client=mock_client, + name="test-model", + definition="test-definition", + configuration={"key": "value"}, + ) + + # Execute + model.delete(silent=True) + + # Assert + mock_client.model.delete_model.assert_called_once_with( + "test-model-id", silent=True + ) + + def test_model_delete_no_resource(mock_client, mock_model_response): + """Test Model delete method when resource is None.""" + # Setup + mock_client.model.get_model.return_value = Mock(model=mock_model_response) + + model = Model( + client=mock_client, + name="test-model", + definition="test-definition", + configuration={"key": "value"}, + ) + model.resource = None + + # Execute (should not raise exception) + model.delete(silent=True) + + # Assert + mock_client.model.delete_model.assert_not_called() + + +def describe_pipeline(): + """Test the Pipeline resource class.""" + + @pytest.fixture + def mock_client(): + """Create a mock InstillClient.""" + client = Mock(spec=InstillClient) + client.pipeline = Mock() + return client + + @pytest.fixture + def mock_pipeline_response(): + """Create a mock pipeline response.""" + pipeline = Mock() + pipeline.id = "test-pipeline-id" + pipeline.recipe = Struct() + return pipeline + + def test_pipeline_initialization_existing_pipeline( + mock_client, mock_pipeline_response + ): + """Test Pipeline initialization when pipeline already exists.""" + # Setup + mock_client.pipeline.get_pipeline.return_value = Mock( + pipeline=mock_pipeline_response + ) + + # Execute + pipeline = Pipeline( + client=mock_client, + namespace_id="test-namespace", + pipeline_id="test-pipeline", + ) + + # Assert + assert pipeline.client == mock_client + assert pipeline.resource == mock_pipeline_response + mock_client.pipeline.get_pipeline.assert_called_once_with( + namespace_id="test-namespace", pipeline_id="test-pipeline" + ) + mock_client.pipeline.create_pipeline.assert_not_called() + + def test_pipeline_initialization_new_pipeline(mock_client, mock_pipeline_response): + """Test Pipeline initialization when creating new pipeline.""" + # Setup + mock_client.pipeline.get_pipeline.return_value = None + mock_client.pipeline.create_pipeline.return_value = Mock( + pipeline=mock_pipeline_response + ) + + # Execute + pipeline = Pipeline( + client=mock_client, + namespace_id="test-namespace", + pipeline_id="test-pipeline", + ) + + # Assert + assert pipeline.client == mock_client + assert pipeline.resource == mock_pipeline_response + mock_client.pipeline.get_pipeline.assert_called_once_with( + namespace_id="test-namespace", pipeline_id="test-pipeline" + ) + mock_client.pipeline.create_pipeline.assert_called_once_with( + namespace_id="test-namespace", recipe=None + ) + + def test_pipeline_initialization_with_recipe(mock_client, mock_pipeline_response): + """Test Pipeline initialization with recipe.""" + # Setup + mock_client.pipeline.get_pipeline.return_value = None + mock_client.pipeline.create_pipeline.return_value = Mock( + pipeline=mock_pipeline_response + ) + recipe = Struct() + + # Execute + pipeline = Pipeline( + client=mock_client, + namespace_id="test-namespace", + pipeline_id="test-pipeline", + recipe=recipe, + ) + + # Assert + mock_client.pipeline.create_pipeline.assert_called_once_with( + namespace_id="test-namespace", recipe=recipe + ) + + def test_pipeline_initialization_creation_failed(mock_client): + """Test Pipeline initialization when pipeline creation fails.""" + # Setup + mock_client.pipeline.get_pipeline.return_value = None + mock_client.pipeline.create_pipeline.return_value = Mock(pipeline=None) + + # Execute & Assert + with pytest.raises(BaseException, match="pipeline creation failed"): + Pipeline( + client=mock_client, + namespace_id="test-namespace", + pipeline_id="test-pipeline", + ) + + def test_pipeline_call_success(mock_client, mock_pipeline_response): + """Test Pipeline __call__ method with successful response.""" + # Setup + mock_client.pipeline.get_pipeline.return_value = Mock( + pipeline=mock_pipeline_response + ) + mock_response = Mock() + mock_response.outputs = ["output1", "output2"] + mock_response.metadata = Mock() + mock_client.pipeline.trigger.return_value = mock_response + + pipeline = Pipeline( + client=mock_client, + namespace_id="test-namespace", + pipeline_id="test-pipeline", + ) + + # Execute + result = pipeline(task_inputs=["input1"], silent=False) + + # Assert + assert result == (["output1", "output2"], mock_response.metadata) + mock_client.pipeline.trigger.assert_called_once_with( + "test-pipeline-id", ["input1"], silent=False + ) + + def test_pipeline_call_no_response(mock_client, mock_pipeline_response): + """Test Pipeline __call__ method with no response.""" + # Setup + mock_client.pipeline.get_pipeline.return_value = Mock( + pipeline=mock_pipeline_response + ) + mock_client.pipeline.trigger.return_value = None + + pipeline = Pipeline( + client=mock_client, + namespace_id="test-namespace", + pipeline_id="test-pipeline", + ) + + # Execute + result = pipeline(task_inputs=["input1"], silent=True) + + # Assert + assert result is None + mock_client.pipeline.trigger.assert_called_once_with( + "test-pipeline-id", ["input1"], silent=True + ) + + def test_pipeline_properties(mock_client, mock_pipeline_response): + """Test Pipeline property getters and setters.""" + # Setup + mock_client.pipeline.get_pipeline.return_value = Mock( + pipeline=mock_pipeline_response + ) + + pipeline = Pipeline( + client=mock_client, + namespace_id="test-namespace", + pipeline_id="test-pipeline", + ) + + # Test client property + new_client = Mock(spec=InstillClient) + pipeline.client = new_client + assert pipeline.client == new_client + + # Test resource property + new_resource = Mock() + pipeline.resource = new_resource + assert pipeline.resource == new_resource + + def test_pipeline_update(mock_client, mock_pipeline_response): + """Test Pipeline _update method.""" + # Setup + updated_pipeline = Mock() + updated_pipeline.id = "updated-pipeline-id" + + # Create a new mock client to avoid conflicts + fresh_mock_client = Mock(spec=InstillClient) + fresh_mock_client.pipeline = Mock() + + # Configure the mock to handle different call signatures + def mock_get_pipeline(*args, **kwargs): + if "namespace_id" in kwargs and "pipeline_id" in kwargs: + # Initialization call + return Mock(pipeline=mock_pipeline_response) + if "name" in kwargs: + # Update call - return the updated pipeline directly + return updated_pipeline + return None + + fresh_mock_client.pipeline.get_pipeline.side_effect = mock_get_pipeline + + pipeline = Pipeline( + client=fresh_mock_client, + namespace_id="test-namespace", + pipeline_id="test-pipeline", + ) + + # Execute + pipeline._update() + + # Assert + assert pipeline.resource == updated_pipeline + assert fresh_mock_client.pipeline.get_pipeline.call_count == 2 + + def test_pipeline_get_operation_success(mock_client, mock_pipeline_response): + """Test Pipeline get_operation method with successful response.""" + # Setup + mock_client.pipeline.get_pipeline.return_value = Mock( + pipeline=mock_pipeline_response + ) + mock_operation = Operation() + mock_response = Mock() + mock_response.operation = mock_operation + mock_client.pipeline.get_operation.return_value = mock_response + + pipeline = Pipeline( + client=mock_client, + namespace_id="test-namespace", + pipeline_id="test-pipeline", + ) + + # Execute + result = pipeline.get_operation(mock_operation, silent=True) + + # Assert + assert result == mock_operation + mock_client.pipeline.get_operation.assert_called_once_with( + mock_operation.name, silent=True + ) + + def test_pipeline_get_operation_no_response(mock_client, mock_pipeline_response): + """Test Pipeline get_operation method with no response.""" + # Setup + mock_client.pipeline.get_pipeline.return_value = Mock( + pipeline=mock_pipeline_response + ) + mock_operation = Operation() + mock_client.pipeline.get_operation.return_value = None + + pipeline = Pipeline( + client=mock_client, + namespace_id="test-namespace", + pipeline_id="test-pipeline", + ) + + # Execute + result = pipeline.get_operation(mock_operation, silent=False) + + # Assert + assert result is None + mock_client.pipeline.get_operation.assert_called_once_with( + mock_operation.name, silent=False + ) + + def test_pipeline_trigger_async_success(mock_client, mock_pipeline_response): + """Test Pipeline trigger_async method with successful response.""" + # Setup + mock_client.pipeline.get_pipeline.return_value = Mock( + pipeline=mock_pipeline_response + ) + mock_operation = Operation() + mock_response = Mock() + mock_response.operation = mock_operation + mock_client.pipeline.trigger_async.return_value = mock_response + + pipeline = Pipeline( + client=mock_client, + namespace_id="test-namespace", + pipeline_id="test-pipeline", + ) + + # Execute + result = pipeline.trigger_async(task_inputs=["input1"], silent=True) + + # Assert + assert result == mock_operation + mock_client.pipeline.trigger_async.assert_called_once_with( + "test-pipeline-id", ["input1"], silent=True + ) + + def test_pipeline_trigger_async_no_response(mock_client, mock_pipeline_response): + """Test Pipeline trigger_async method with no response.""" + # Setup + mock_client.pipeline.get_pipeline.return_value = Mock( + pipeline=mock_pipeline_response + ) + mock_client.pipeline.trigger_async.return_value = None + + pipeline = Pipeline( + client=mock_client, + namespace_id="test-namespace", + pipeline_id="test-pipeline", + ) + + # Execute + result = pipeline.trigger_async(task_inputs=["input1"], silent=False) + + # Assert + assert result is None + mock_client.pipeline.trigger_async.assert_called_once_with( + "test-pipeline-id", ["input1"], silent=False + ) + + @patch("instill.resources.pipeline.json_format.MessageToDict") + def test_pipeline_get_recipe(mock_json_format, mock_client, mock_pipeline_response): + """Test Pipeline get_recipe method.""" + # Setup + mock_client.pipeline.get_pipeline.return_value = Mock( + pipeline=mock_pipeline_response + ) + mock_json_format.return_value = {"recipe": "data"} + + pipeline = Pipeline( + client=mock_client, + namespace_id="test-namespace", + pipeline_id="test-pipeline", + ) + + # Execute + result = pipeline.get_recipe() + + # Assert + assert result == {"recipe": "data"} + mock_json_format.assert_called_once_with(mock_pipeline_response.recipe) + + def test_pipeline_update_recipe(mock_client, mock_pipeline_response): + """Test Pipeline update_recipe method.""" + # Setup + mock_client.pipeline.get_pipeline.return_value = Mock( + pipeline=mock_pipeline_response + ) + updated_pipeline = Mock() + mock_client.pipeline.get_pipeline.side_effect = [ + Mock(pipeline=mock_pipeline_response), # First call for initialization + Mock(pipeline=updated_pipeline), # Second call for _update + ] + + pipeline = Pipeline( + client=mock_client, + namespace_id="test-namespace", + pipeline_id="test-pipeline", + ) + + recipe = Struct() + + # Execute + pipeline.update_recipe(recipe, silent=True) + + # Assert + mock_client.pipeline.update_pipeline.assert_called_once() + call_args = mock_client.pipeline.update_pipeline.call_args + assert call_args[0][0] == mock_pipeline_response + assert isinstance(call_args[0][1], FieldMask) + assert call_args[0][1].paths == ["recipe"] + assert call_args[1]["silent"] is True + + @patch("instill.resources.pipeline.Logger") + def test_pipeline_validate_pipeline_failure( + mock_logger, mock_client, mock_pipeline_response + ): + """Test Pipeline validate_pipeline method with validation failure.""" + # Setup + mock_client.pipeline.get_pipeline.return_value = Mock( + pipeline=mock_pipeline_response + ) + + # Create a proper RpcError exception + class MockRpcError(grpc.RpcError): + def code(self): + return grpc.StatusCode.INVALID_ARGUMENT + + def details(self): + return "Validation failed" + + # Set the side_effect to raise the exception + mock_client.pipeline.validate_pipeline.side_effect = MockRpcError() + + pipeline = Pipeline( + client=mock_client, + namespace_id="test-namespace", + pipeline_id="test-pipeline", + ) + + # Execute + result = pipeline.validate_pipeline(silent=False) + + # Verify + assert result is False + mock_client.pipeline.validate_pipeline.assert_called_once_with( + name="test-pipeline-id", silent=False + ) + mock_logger.w.assert_called() + + def test_pipeline_delete(mock_client, mock_pipeline_response): + """Test Pipeline delete method.""" + # Setup + mock_client.pipeline.get_pipeline.return_value = Mock( + pipeline=mock_pipeline_response + ) + + pipeline = Pipeline( + client=mock_client, + namespace_id="test-namespace", + pipeline_id="test-pipeline", + ) + + # Execute + pipeline.delete(silent=True) + + # Assert + mock_client.pipeline.delete_pipeline.assert_called_once_with( + "test-pipeline-id", silent=True + ) + + def test_pipeline_delete_no_resource(mock_client, mock_pipeline_response): + """Test Pipeline delete method when resource is None.""" + # Setup + mock_client.pipeline.get_pipeline.return_value = Mock( + pipeline=mock_pipeline_response + ) + + pipeline = Pipeline( + client=mock_client, + namespace_id="test-namespace", + pipeline_id="test-pipeline", + ) + pipeline.resource = None + + # Execute (should not raise exception) + pipeline.delete(silent=True) + + # Assert + mock_client.pipeline.delete_pipeline.assert_not_called() diff --git a/instill/tests/test_utils.py b/instill/tests/test_utils.py new file mode 100644 index 0000000..86e755e --- /dev/null +++ b/instill/tests/test_utils.py @@ -0,0 +1,515 @@ +"""Unit tests for the instill.utils module.""" + +# pylint: disable=unused-argument,unused-variable +import logging +import os +import tempfile +from unittest.mock import MagicMock, patch + +import grpc +import pytest + +from instill.utils.error_handler import ( + NamespaceException, + NotServingException, + grpc_handler, +) +from instill.utils.logger import Logger +from instill.utils.process_file import get_file_type, process_file + + +class TestLogger: + """Test cases for the Logger class.""" + + def setup_method(self): + """Set up test fixtures.""" + # Reset logger state before each test + Logger.Initialized = False + + def test_logger_initialization(self): + """Test logger initialization.""" + # Test that logger is not initialized initially + assert Logger.Initialized is False + + # Initialize logger + Logger.initialize() + + # Test that logger is now initialized + assert Logger.Initialized is True + + def test_logger_singleton_pattern(self): + """Test that logger follows singleton pattern.""" + # Initialize logger twice + Logger.initialize() + initial_state = Logger.Initialized + + Logger.initialize() + + # Should remain initialized + assert Logger.Initialized is True + + def test_logger_handler_setup(self): + """Test that logger has correct handlers.""" + Logger.initialize() + + # Get the root logger + logger = logging.getLogger() + + # Check that logger has handlers + assert len(logger.handlers) >= 3 # runlog, errorlog, and console handlers + + # Check for specific handler types + file_handlers = [ + h for h in logger.handlers if isinstance(h, logging.FileHandler) + ] + stream_handlers = [ + h for h in logger.handlers if isinstance(h, logging.StreamHandler) + ] + + assert len(file_handlers) >= 2 # runlog and errorlog handlers + assert len(stream_handlers) >= 1 # console handler + + def test_logger_level(self): + """Test logger level.""" + Logger.initialize() + logger = logging.getLogger() + + # Default level should be INFO + assert logger.level == logging.INFO + + def test_logger_format(self): + """Test logger format.""" + Logger.initialize() + logger = logging.getLogger() + + # Check that formatter is set + for handler in logger.handlers: + if handler.formatter: + format_string = handler.formatter._fmt + if format_string is not None: + assert "%(asctime)s" in format_string + assert ( + "%(levelname)" in format_string + ) # Check for levelname with any modifiers + assert "%(message)s" in format_string + + def test_logger_logging_methods(self): + """Test logger logging methods.""" + Logger.initialize() + + # Test all logging methods + Logger.d("debug message") + Logger.i("info message") + Logger.w("warning message") + Logger.e("error message") + + # These should not raise exceptions + assert True + + def test_logger_exception_method(self): + """Test logger exception method.""" + Logger.initialize() + + # Test exception logging + try: + raise ValueError("Test exception") + except ValueError: + Logger.exception("Exception occurred") + + # Should not raise exception + assert True + + def test_logger_uninitialized_error(self): + """Test that logger methods fail when not initialized.""" + # Reset to uninitialized state + Logger.Initialized = False + + # All methods should raise AssertionError when not initialized + with pytest.raises(AssertionError, match="Logger has not been initialized"): + Logger.d("test") + + with pytest.raises(AssertionError, match="Logger has not been initialized"): + Logger.i("test") + + with pytest.raises(AssertionError, match="Logger has not been initialized"): + Logger.w("test") + + with pytest.raises(AssertionError, match="Logger has not been initialized"): + Logger.e("test") + + with pytest.raises(AssertionError, match="Logger has not been initialized"): + Logger.exception("test") + + def test_logger_custom_paths(self): + """Test logger initialization with custom paths.""" + with tempfile.TemporaryDirectory() as temp_dir: + runlog_path = os.path.join(temp_dir, "run.log") + errorlog_path = os.path.join(temp_dir, "error.log") + + Logger.initialize(runlog_path=runlog_path, errorlog_path=errorlog_path) + + # Check that files were created + assert os.path.exists(runlog_path) + assert os.path.exists(errorlog_path) + + # Test logging + Logger.i("test message") + + # Check that message was written to run log + with open(runlog_path, "r", encoding="utf-8") as f: + content = f.read() + assert "test message" in content + + +class TestErrorHandler: + """Test cases for error handling utilities.""" + + def test_not_serving_exception(self): + """Test NotServingException.""" + exception = NotServingException() + assert str(exception) == "target host is not serving" + + custom_message = "Custom error message" + exception = NotServingException(custom_message) + assert str(exception) == custom_message + + def test_namespace_exception(self): + """Test NamespaceException.""" + exception = NamespaceException() + assert str(exception) == "namespace ID not available" + + custom_message = "Custom namespace error" + exception = NamespaceException(custom_message) + assert str(exception) == custom_message + + @patch("instill.utils.error_handler.Logger") + @patch("os._exit") + def test_grpc_handler_not_serving(self, mock_exit, mock_logger): + """Test grpc_handler when target is not serving.""" + # Create a mock object that returns False for is_serving() + mock_obj = MagicMock() + mock_obj.is_serving.return_value = False + + # Create a test function + def test_func(obj, *args, **kwargs): + return "success" + + # Apply the decorator + decorated_func = grpc_handler(test_func) + + # Call the decorated function with the mock object as first argument + decorated_func(mock_obj) + + # Verify is_serving was called + mock_obj.is_serving.assert_called_once() + + # Verify that Logger.exception was called (because NotServingException was caught) + mock_logger.exception.assert_called_once() + + # Verify that os._exit was called with exit code 1 + mock_exit.assert_called_once_with(1) + + @patch("instill.utils.error_handler.Logger") + @patch("os._exit") + def test_grpc_handler_serving_success(self, mock_exit, mock_logger): + """Test grpc_handler when target is serving and function succeeds.""" + # Create a mock object that returns True for is_serving() + mock_obj = MagicMock() + mock_obj.is_serving.return_value = True + + # Create a test function + def test_func(obj, *args, **kwargs): + return "success" + + # Apply the decorator + decorated_func = grpc_handler(test_func) + + # Call the decorated function + result = decorated_func(mock_obj) + + # Verify result + assert result == "success" + mock_obj.is_serving.assert_called_once() + + @patch("instill.utils.error_handler.Logger") + @patch("os._exit") + def test_grpc_handler_grpc_error(self, mock_exit, mock_logger): + """Test grpc_handler when GRPC error occurs.""" + # Create a mock object that returns True for is_serving() + mock_obj = MagicMock() + mock_obj.is_serving.return_value = True + + # Create a proper GRPC error that inherits from grpc.RpcError + class MockRpcError(grpc.RpcError): + def code(self): + return grpc.StatusCode.UNAVAILABLE + + def details(self): + return "Service unavailable" + + # Create a test function that raises the mock GRPC error + def test_func(obj, *args, **kwargs): + raise MockRpcError() + + # Apply the decorator + decorated_func = grpc_handler(test_func) + + # Call the decorated function + decorated_func(mock_obj) + + # Verify that Logger.w was called twice (once for code, once for details) + assert mock_logger.w.call_count == 2 + mock_logger.w.assert_any_call(grpc.StatusCode.UNAVAILABLE) + mock_logger.w.assert_any_call("Service unavailable") + + # Verify that os._exit was called with exit code 1 + mock_exit.assert_called_once_with(1) + + @patch("instill.utils.error_handler.Logger") + @patch("os._exit") + def test_grpc_handler_grpc_error_silent(self, mock_exit, mock_logger): + """Test grpc_handler when GRPC error occurs with silent=True.""" + # Create a mock object that returns True for is_serving() + mock_obj = MagicMock() + mock_obj.is_serving.return_value = True + + # Create a proper GRPC error class that inherits from grpc.RpcError + class MockRpcError(grpc.RpcError): + def code(self): + return grpc.StatusCode.UNAVAILABLE + + def details(self): + return "Service unavailable" + + # Create a test function that raises the proper GRPC error + def test_func(obj, *args, **kwargs): + raise MockRpcError() + + # Apply the decorator + decorated_func = grpc_handler(test_func) + + # Call the decorated function with silent=True + decorated_func(mock_obj, silent=True) + + # Verify that Logger.w was NOT called (because silent=True) + mock_logger.w.assert_not_called() + + # Verify that os._exit was NOT called (because silent=True) + mock_exit.assert_not_called() + + @patch("instill.utils.error_handler.Logger") + @patch("os._exit") + def test_grpc_handler_general_exception(self, mock_exit, mock_logger): + """Test grpc_handler when general exception occurs.""" + # Create a mock object that returns True for is_serving() + mock_obj = MagicMock() + mock_obj.is_serving.return_value = True + + # Create a test function that raises general exception + def test_func(obj, *args, **kwargs): + raise ValueError("Test error") + + # Apply the decorator + decorated_func = grpc_handler(test_func) + + # Call the decorated function + decorated_func(mock_obj) + + # Verify that Logger.exception was called and os._exit was called + mock_logger.exception.assert_called_once() + mock_exit.assert_called_once_with(1) + + +class TestProcessFile: + """Test cases for file processing utilities.""" + + def test_get_file_type_text(self): + """Test get_file_type for text files.""" + # Test various text file extensions that are actually supported + text_extensions = [".txt", ".log", ".ini", ".csv"] + + for ext in text_extensions: + with tempfile.NamedTemporaryFile(suffix=ext, delete=False) as temp_file: + temp_file.write(b"Some text content") + temp_file.flush() + + file_type = get_file_type(temp_file.name, ext) + assert file_type == "FILE_TYPE_TEXT" + + # Clean up + os.unlink(temp_file.name) + + def test_get_file_type_markdown(self): + """Test get_file_type for markdown files.""" + with tempfile.NamedTemporaryFile(suffix=".md", delete=False) as temp_file: + temp_file.write(b"# Markdown content") + temp_file.flush() + + file_type = get_file_type(temp_file.name, ".md") + assert file_type == "FILE_TYPE_MARKDOWN" + + # Clean up + os.unlink(temp_file.name) + + def test_get_file_type_html(self): + """Test get_file_type for HTML files.""" + with tempfile.NamedTemporaryFile(suffix=".html", delete=False) as temp_file: + temp_file.write(b"Content") + temp_file.flush() + + file_type = get_file_type(temp_file.name, ".html") + assert file_type == "FILE_TYPE_HTML" + + # Clean up + os.unlink(temp_file.name) + + def test_get_file_type_pdf(self): + """Test get_file_type for PDF files.""" + with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as temp_file: + temp_file.write(b"%PDF-1.4\nfake pdf content") + temp_file.flush() + + file_type = get_file_type(temp_file.name, ".pdf") + assert file_type == "FILE_TYPE_PDF" + + # Clean up + os.unlink(temp_file.name) + + def test_get_file_type_office_documents(self): + """Test get_file_type for Office documents.""" + office_extensions = [".doc", ".docx", ".ppt", ".pptx", ".xls", ".xlsx"] + + for ext in office_extensions: + with tempfile.NamedTemporaryFile(suffix=ext, delete=False) as temp_file: + temp_file.write(b"fake office document content") + temp_file.flush() + + file_type = get_file_type(temp_file.name, ext) + expected_type = ext.upper().replace(".", "FILE_TYPE_") + assert file_type == expected_type + + # Clean up + os.unlink(temp_file.name) + + def test_get_file_type_unknown_extension(self): + """Test get_file_type for unknown file extensions.""" + with tempfile.NamedTemporaryFile(suffix=".unknown", delete=False) as temp_file: + temp_file.write(b"some data") + temp_file.flush() + + # Should raise ValueError for unknown extension + with pytest.raises(ValueError, match="Unsupported file type"): + get_file_type(temp_file.name, ".unknown") + + # Clean up + os.unlink(temp_file.name) + + def test_get_file_type_content_based_detection(self): + """Test get_file_type content-based detection for unknown extensions.""" + # Test PDF detection by content + with tempfile.NamedTemporaryFile(suffix=".unknown", delete=False) as temp_file: + temp_file.write(b"%PDF-1.4\nfake pdf content") + temp_file.flush() + + file_type = get_file_type(temp_file.name, ".unknown") + assert file_type == "FILE_TYPE_PDF" + + # Clean up + os.unlink(temp_file.name) + + # Test HTML detection by content + with tempfile.NamedTemporaryFile(suffix=".unknown", delete=False) as temp_file: + temp_file.write(b"Content") + temp_file.flush() + + file_type = get_file_type(temp_file.name, ".unknown") + assert file_type == "FILE_TYPE_HTML" + + # Clean up + os.unlink(temp_file.name) + + def test_get_file_type_office_xml_detection(self): + """Test get_file_type for Office XML formats detection.""" + # Test DOCX detection + with tempfile.NamedTemporaryFile(suffix=".unknown", delete=False) as temp_file: + temp_file.write(b"PK\x03\x04[Content_Types].xmlword/document.xml") + temp_file.flush() + + file_type = get_file_type(temp_file.name, ".unknown") + assert file_type == "FILE_TYPE_DOCX" + + # Clean up + os.unlink(temp_file.name) + + def test_process_file_text(self): + """Test process_file for text files.""" + # Create a temporary text file + with tempfile.NamedTemporaryFile( + suffix=".txt", mode="w", delete=False + ) as temp_file: + temp_file.write("Test content") + temp_file.flush() + + # Process the file + result = process_file(temp_file.name) + + # Verify result properties + assert result.name == os.path.basename(temp_file.name) + assert result.type == 1 # FILE_TYPE_TEXT enum value + assert result.content is not None + assert len(result.content) > 0 + + # Clean up + os.unlink(temp_file.name) + + def test_process_file_markdown(self): + """Test process_file for markdown files.""" + # Create a temporary markdown file + with tempfile.NamedTemporaryFile( + suffix=".md", mode="w", delete=False + ) as temp_file: + temp_file.write("# Test markdown") + temp_file.flush() + + # Process the file + result = process_file(temp_file.name) + + # Verify result properties + assert result.name == os.path.basename(temp_file.name) + assert result.type == 3 # FILE_TYPE_MARKDOWN enum value + assert result.content is not None + assert len(result.content) > 0 + + # Clean up + os.unlink(temp_file.name) + + def test_process_file_nonexistent(self): + """Test process_file with nonexistent file.""" + with pytest.raises(FileNotFoundError): + process_file("nonexistent_file.txt") + + def test_process_file_directory(self): + """Test process_file with directory path.""" + with tempfile.TemporaryDirectory() as temp_dir: + with pytest.raises(IsADirectoryError): + process_file(temp_dir) + + def test_get_file_type_unsupported_extensions(self): + """Test get_file_type for unsupported file extensions.""" + unsupported_extensions = [".js", ".css", ".json", ".xml", ".py", ".java"] + + for ext in unsupported_extensions: + with tempfile.NamedTemporaryFile(suffix=ext, delete=False) as temp_file: + temp_file.write(b"Some content") + temp_file.flush() + + # Should raise ValueError for unsupported extensions + with pytest.raises(ValueError, match="Unsupported file type"): + get_file_type(temp_file.name, ext) + + # Clean up + os.unlink(temp_file.name) + + +if __name__ == "__main__": + pytest.main([__file__]) diff --git a/notebooks/asyncio_example.py b/notebooks/asyncio_example.py index 2b7fad1..16c3685 100644 --- a/notebooks/asyncio_example.py +++ b/notebooks/asyncio_example.py @@ -6,7 +6,7 @@ from google.protobuf.struct_pb2 import Struct from instill.clients import InstillClient -from instill.configuration import global_config +from instill.config import global_config global_config.set_default( url="localhost:8080", diff --git a/notebooks/create_pipeline.ipynb b/notebooks/create_pipeline.ipynb index 986ba1d..b561ae3 100644 --- a/notebooks/create_pipeline.ipynb +++ b/notebooks/create_pipeline.ipynb @@ -36,7 +36,7 @@ "metadata": {}, "outputs": [], "source": [ - "from instill.configuration import global_config\n", + "from instill.config import global_config\n", "\n", "global_config.set_default(\n", " url=\"api.instill-ai.com\",\n", diff --git a/notebooks/serve_github_model.ipynb b/notebooks/serve_github_model.ipynb index 4861d76..5b16095 100644 --- a/notebooks/serve_github_model.ipynb +++ b/notebooks/serve_github_model.ipynb @@ -23,7 +23,7 @@ } ], "source": [ - "from instill.configuration import global_config\n", + "from instill.config import global_config\n", "# setup target instance\n", "global_config.set_default(\n", " url=\"localhost:8080\",\n", diff --git a/scent.py b/scent.py index 950cd74..d943e5a 100644 --- a/scent.py +++ b/scent.py @@ -31,8 +31,7 @@ class Options: rerun_args = (None, None, None) targets = [ - (("make", "test-unit", "DISABLE_COVERAGE=true"), "Unit Tests", True), - (("make", "test-all"), "Integration Tests", False), + (("make", "test"), "Unit Tests", True), (("make", "check"), "Static Analysis", True), (("make", "docs", "CI=true"), None, True), ] diff --git a/tests/__init__.py b/tests/__init__.py deleted file mode 100644 index a916f18..0000000 --- a/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Integration tests for the package.""" diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 83c3c90..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Integration tests configuration file.""" - -# pylint: disable=unused-import,no-name-in-module - -import pytest - -from instill.clients import MgmtClient, ModelClient, PipelineClient -from instill.tests.conftest import pytest_configure - - -@pytest.fixture(scope="session") -def clients(): - pass - # mgmt_client = MgmtClient() - # model_client = ModelClient(user=mgmt_client.get_user()) - # pipeline_client = PipelineClient(user=mgmt_client.get_user()) - - # return [mgmt_client, model_client, pipeline_client] diff --git a/tests/test_client.py b/tests/test_client.py deleted file mode 100644 index 36d51e9..0000000 --- a/tests/test_client.py +++ /dev/null @@ -1,90 +0,0 @@ -# pylint: disable=redefined-outer-name,unused-variable,expression-not-assigned,no-name-in-module - -import instill.protogen.core.mgmt.v1beta.mgmt_public_service_pb2_grpc as mgmt_service -import instill.protogen.model.model.v1alpha.model_public_service_pb2_grpc as model_service -import instill.protogen.pipeline.pipeline.v1beta.pipeline_public_service_pb2_grpc as pipeline_service -from instill.clients import MgmtClient, ModelClient, PipelineClient -from instill.clients.instance import InstillInstance - - -def mock(_: str): - return "" - - -def describe_client(): - def describe_host(): - def when_not_set(expect): - mgmt_client = MgmtClient("") - expect(mgmt_client.host) is not None - model_client = ModelClient("", mock) - expect(model_client.host) is not None - pipeline_client = PipelineClient("", mock) - expect(pipeline_client.host) is not None - - def when_set_correct_type_url(expect): - mgmt_instance = InstillInstance( - mgmt_service.MgmtPublicServiceStub, - "test_url", - "token", - False, - False, - ) - mgmt_client = MgmtClient("") - mgmt_client.host = mgmt_instance - expect(mgmt_client.host.url) == "test_url" - - model_instance = InstillInstance( - model_service.ModelPublicServiceStub, - "test_url", - "token", - False, - False, - ) - model_client = ModelClient("", mock) - model_client.host = model_instance - expect(model_client.host.url) == "test_url" - - pipeline_instance = InstillInstance( - pipeline_service.PipelinePublicServiceStub, - "test_url", - "token", - False, - False, - ) - pipeline_client = PipelineClient("", mock) - pipeline_client.host = pipeline_instance - expect(pipeline_client.host.url) == "test_url" - - def when_set_correct_type_token(expect): - mgmt_instance = InstillInstance( - mgmt_service.MgmtPublicServiceStub, - "test_url", - "token", - False, - False, - ) - mgmt_client = MgmtClient("") - mgmt_client.host = mgmt_instance - expect(mgmt_client.host.token) == "token" - - model_instance = InstillInstance( - model_service.ModelPublicServiceStub, - "test_url", - "token", - False, - False, - ) - model_client = ModelClient("", mock) - model_client.host = model_instance - expect(model_client.host.token) == "token" - - pipeline_instance = InstillInstance( - pipeline_service.PipelinePublicServiceStub, - "test_url", - "token", - False, - False, - ) - pipeline_client = PipelineClient("", mock) - pipeline_client.host = pipeline_instance - expect(pipeline_client.host.token) == "token"