diff --git a/.commitlintrc.yml b/.commitlintrc.yml new file mode 100644 index 0000000..a7f3ad9 --- /dev/null +++ b/.commitlintrc.yml @@ -0,0 +1,85 @@ +# Copyright 2025 Vantage Compute Corporation +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# .commitlintrc.yml +# Enforces: [()]: [] (#) +# - scope is optional +# - [task-id] is optional +# - "(#)" is optional +# - No "type!:"; use BREAKING CHANGE footer instead. + +extends: + - "@commitlint/config-conventional" + +parserPreset: + # Inline custom parser (no external package needed) + parserOpts: + headerPattern: "^([a-z]+)(?:\\(([^)]+)\\))?:\\s(?:\\[(.+?)\\]\\s)?(.+?)(?:\\s\\(#\\d+\\))?$" + headerCorrespondence: + - type + - scope + - task + - subject + +rules: + # Allowed types + type-enum: + - 2 + - always + - [fix, feat, cherry-pick, chore, security, ci, docs, test, refactor, perf, release] + + type-case: + - 2 + - always + - lower-case + + # Scope is optional; if present, keep it lowercase + scope-case: + - 2 + - always + - lower-case + + # Require concise subject (your "summary") + subject-empty: + - 2 + - never + subject-max-length: + - 2 + - always + - 80 + subject-full-stop: + - 2 + - never + + # Enforce blank lines before body/footer when present + body-leading-blank: + - 2 + - always + footer-leading-blank: + - 2 + - always + + # Soft cap on full header length (subject already capped to 80) + header-max-length: + - 1 + - always + - 120 + + type-empty: + - 2 + - never + + # Disallow "!" in the subject; "type!:" is already blocked by headerPattern + subject-exclamation-mark: + - 2 + - never diff --git a/go/shim/main.go b/go/shim/main.go index 763f43e..e8cf4a5 100644 --- a/go/shim/main.go +++ b/go/shim/main.go @@ -545,7 +545,7 @@ func helm_sdkpy_history(handle C.helm_sdkpy_handle, release_name *C.char, result // Pull action //export helm_sdkpy_pull -func helm_sdkpy_pull(handle C.helm_sdkpy_handle, chart_ref *C.char, dest_dir *C.char) C.int { +func helm_sdkpy_pull(handle C.helm_sdkpy_handle, chart_ref *C.char, dest_dir *C.char, version *C.char) C.int { state, err := getConfig(handle) if err != nil { return setError(err) @@ -556,6 +556,7 @@ func helm_sdkpy_pull(handle C.helm_sdkpy_handle, chart_ref *C.char, dest_dir *C. chartRef := C.GoString(chart_ref) destDir := C.GoString(dest_dir) + chartVersion := C.GoString(version) // Create pull action client := action.NewPull() @@ -563,6 +564,10 @@ func helm_sdkpy_pull(handle C.helm_sdkpy_handle, chart_ref *C.char, dest_dir *C. if destDir != "" { client.DestDir = destDir } + // Set version if provided + if chartVersion != "" { + client.Version = chartVersion + } // Run the pull _, err = client.Run(chartRef) diff --git a/helm_sdkpy/__init__.py b/helm_sdkpy/__init__.py index aeaf1e0..9603e43 100644 --- a/helm_sdkpy/__init__.py +++ b/helm_sdkpy/__init__.py @@ -14,6 +14,8 @@ """Python bindings for Helm - The Kubernetes Package Manager.""" +from importlib.metadata import version as _get_version + from ._ffi import get_version from .actions import ( Configuration, @@ -28,7 +30,7 @@ Uninstall, Upgrade, ) -from .chart import Lint, Package, Pull, Push, Show, Test +from .chart import Lint, Package, Pull, Push, ReleaseTest, Show from .exceptions import ( ChartError, ConfigurationError, @@ -44,7 +46,10 @@ ) from .repo import RepoAdd, RepoList, RepoRemove, RepoUpdate -__version__ = "0.0.1" +# Backwards compatibility alias - ReleaseTest renamed to avoid pytest collection +Test = ReleaseTest + +__version__ = _get_version("helm-sdkpy") __all__ = [ # Core classes @@ -63,7 +68,8 @@ # Chart classes "Pull", "Show", - "Test", + "Test", # Alias for ReleaseTest + "ReleaseTest", "Lint", "Package", "Push", diff --git a/helm_sdkpy/_ffi.py b/helm_sdkpy/_ffi.py index 82d004f..f50541e 100644 --- a/helm_sdkpy/_ffi.py +++ b/helm_sdkpy/_ffi.py @@ -58,7 +58,7 @@ int helm_sdkpy_history(helm_sdkpy_handle handle, const char *release_name, char **result_json); // Pull action - int helm_sdkpy_pull(helm_sdkpy_handle handle, const char *chart_ref, const char *dest_dir); + int helm_sdkpy_pull(helm_sdkpy_handle handle, const char *chart_ref, const char *dest_dir, const char *version); // Show chart action int helm_sdkpy_show_chart(helm_sdkpy_handle handle, const char *chart_path, char **result_json); @@ -84,7 +84,7 @@ // Registry management actions int helm_sdkpy_registry_login(helm_sdkpy_handle handle, const char *hostname, const char *username, const char *password, const char *options_json); int helm_sdkpy_registry_logout(helm_sdkpy_handle handle, const char *hostname); - + // Push action (for OCI registries) int helm_sdkpy_push(helm_sdkpy_handle handle, const char *chart_ref, const char *remote, const char *options_json); diff --git a/helm_sdkpy/actions.py b/helm_sdkpy/actions.py index f418280..f681f3d 100644 --- a/helm_sdkpy/actions.py +++ b/helm_sdkpy/actions.py @@ -613,7 +613,6 @@ async def run( Raises: RegistryError: If login fails """ - from .exceptions import RegistryError def _registry_login(): hostname_cstr = ffi.new("char[]", hostname.encode("utf-8")) @@ -678,7 +677,6 @@ async def run(self, hostname: str) -> None: Raises: RegistryError: If logout fails """ - from .exceptions import RegistryError def _registry_logout(): hostname_cstr = ffi.new("char[]", hostname.encode("utf-8")) diff --git a/helm_sdkpy/chart.py b/helm_sdkpy/chart.py index 0ff42ca..94ccacb 100644 --- a/helm_sdkpy/chart.py +++ b/helm_sdkpy/chart.py @@ -43,12 +43,15 @@ def __init__(self, config): self.config = config self._lib = get_library() - async def run(self, chart_ref: str, dest_dir: str | None = None) -> None: + async def run( + self, chart_ref: str, dest_dir: str | None = None, version: str | None = None + ) -> None: """Pull a chart asynchronously. Args: chart_ref: Chart reference (e.g., "repo/chart" or "oci://...") dest_dir: Destination directory (default: current directory) + version: Chart version to pull (e.g., "1.2.3"). If not specified, uses latest Raises: ChartError: If pull fails @@ -57,8 +60,12 @@ async def run(self, chart_ref: str, dest_dir: str | None = None) -> None: def _pull(): ref_cstr = ffi.new("char[]", chart_ref.encode("utf-8")) dest_cstr = ffi.new("char[]", dest_dir.encode("utf-8")) if dest_dir else ffi.NULL + version_str = version or "" + version_cstr = ffi.new("char[]", version_str.encode("utf-8")) - result = self._lib.helm_sdkpy_pull(self.config._handle_value, ref_cstr, dest_cstr) + result = self._lib.helm_sdkpy_pull( + self.config._handle_value, ref_cstr, dest_cstr, version_cstr + ) if result != 0: check_error(result) @@ -149,7 +156,7 @@ def _values(): return await asyncio.to_thread(_values) -class Test: +class ReleaseTest: """Helm test action. Runs tests for a release. @@ -160,7 +167,7 @@ class Test: Example: >>> import asyncio >>> config = Configuration() - >>> test = Test(config) + >>> test = ReleaseTest(config) >>> result = asyncio.run(test.run("my-release")) """ diff --git a/pyproject.toml b/pyproject.toml index 04b5b89..0e8a3ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ dev = [ # Testing "coverage[toml] ~= 7.8", "pytest ~= 8.3", + "pytest-asyncio ~= 0.24", "pytest-mock ~= 3.14", "pytest-order ~= 1.3", "python-dotenv ~= 1.0", @@ -38,6 +39,12 @@ dev = [ [tool.pytest.ini_options] addopts = "-ra" testpaths = ["tests"] +asyncio_mode = "auto" +asyncio_default_fixture_loop_scope = "function" +filterwarnings = [ + "ignore:'asyncio.get_event_loop_policy':DeprecationWarning", + "ignore:cannot collect test class 'ReleaseTest':pytest.PytestCollectionWarning", +] [build-system] requires = ["hatchling"] @@ -47,6 +54,7 @@ build-backend = "hatchling.build" dev = [ "codespell>=2.4.1", "pyright>=1.1.406", + "pytest-asyncio>=0.24", "pytest-cov>=7.0.0", "ruff>=0.14.1", ] diff --git a/tests/test_actions.py b/tests/test_actions.py new file mode 100644 index 0000000..0ad8127 --- /dev/null +++ b/tests/test_actions.py @@ -0,0 +1,331 @@ +# Copyright 2025 Vantage Compute +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for Helm action classes.""" + +import inspect + +import pytest + +from helm_sdkpy import ( + Configuration, + GetValues, + History, + Install, + List, + Rollback, + Status, + Uninstall, + Upgrade, +) + + +class TestConfiguration: + """Test Configuration class.""" + + def test_configuration_import(self): + """Test that Configuration can be imported.""" + assert Configuration is not None + + def test_configuration_default_instantiation(self): + """Test Configuration with default parameters.""" + config = Configuration() + assert config is not None + assert hasattr(config, "_handle_value") + + def test_configuration_with_namespace(self): + """Test Configuration with custom namespace.""" + config = Configuration(namespace="test-namespace") + assert config is not None + + def test_configuration_with_kubeconfig(self): + """Test Configuration with kubeconfig path.""" + # Using a non-existent path is fine for instantiation test + config = Configuration(kubeconfig="/tmp/nonexistent-kubeconfig") + assert config is not None + + def test_configuration_with_kubecontext(self): + """Test Configuration with kubecontext.""" + config = Configuration(kubecontext="my-context") + assert config is not None + + def test_configuration_with_all_params(self): + """Test Configuration with all parameters.""" + config = Configuration( + namespace="test-ns", + kubeconfig="/tmp/kubeconfig", + kubecontext="test-context", + ) + assert config is not None + + def test_configuration_context_manager(self): + """Test Configuration as context manager.""" + with Configuration() as config: + assert config is not None + assert hasattr(config, "_handle_value") + + def test_configuration_has_handle(self): + """Test that Configuration creates a valid handle.""" + config = Configuration() + assert hasattr(config, "_handle_value") + assert config._handle_value is not None + + +class TestInstall: + """Test Install class.""" + + def test_install_import(self): + """Test that Install can be imported.""" + assert Install is not None + + def test_install_instantiation(self): + """Test Install instantiation.""" + config = Configuration() + install = Install(config) + assert install is not None + assert install.config == config + + def test_install_has_run_method(self): + """Test that Install has an async run method.""" + config = Configuration() + install = Install(config) + assert hasattr(install, "run") + assert inspect.iscoroutinefunction(install.run) + + def test_install_run_signature(self): + """Test Install.run() method signature.""" + sig = inspect.signature(Install.run) + params = list(sig.parameters.keys()) + assert "self" in params + assert "release_name" in params + assert "chart_path" in params + assert "values" in params + assert "version" in params + assert "create_namespace" in params + assert "wait" in params + assert "timeout" in params + + +class TestUpgrade: + """Test Upgrade class.""" + + def test_upgrade_import(self): + """Test that Upgrade can be imported.""" + assert Upgrade is not None + + def test_upgrade_instantiation(self): + """Test Upgrade instantiation.""" + config = Configuration() + upgrade = Upgrade(config) + assert upgrade is not None + assert upgrade.config == config + + def test_upgrade_has_run_method(self): + """Test that Upgrade has an async run method.""" + config = Configuration() + upgrade = Upgrade(config) + assert hasattr(upgrade, "run") + assert inspect.iscoroutinefunction(upgrade.run) + + def test_upgrade_run_signature(self): + """Test Upgrade.run() method signature.""" + sig = inspect.signature(Upgrade.run) + params = list(sig.parameters.keys()) + assert "self" in params + assert "release_name" in params + assert "chart_path" in params + assert "values" in params + assert "version" in params + + +class TestUninstall: + """Test Uninstall class.""" + + def test_uninstall_import(self): + """Test that Uninstall can be imported.""" + assert Uninstall is not None + + def test_uninstall_instantiation(self): + """Test Uninstall instantiation.""" + config = Configuration() + uninstall = Uninstall(config) + assert uninstall is not None + assert uninstall.config == config + + def test_uninstall_has_run_method(self): + """Test that Uninstall has an async run method.""" + config = Configuration() + uninstall = Uninstall(config) + assert hasattr(uninstall, "run") + assert inspect.iscoroutinefunction(uninstall.run) + + def test_uninstall_run_signature(self): + """Test Uninstall.run() method signature.""" + sig = inspect.signature(Uninstall.run) + params = list(sig.parameters.keys()) + assert "self" in params + assert "release_name" in params + assert "wait" in params + assert "timeout" in params + + +class TestList: + """Test List class.""" + + def test_list_import(self): + """Test that List can be imported.""" + assert List is not None + + def test_list_instantiation(self): + """Test List instantiation.""" + config = Configuration() + list_action = List(config) + assert list_action is not None + assert list_action.config == config + + def test_list_has_run_method(self): + """Test that List has an async run method.""" + config = Configuration() + list_action = List(config) + assert hasattr(list_action, "run") + assert inspect.iscoroutinefunction(list_action.run) + + def test_list_run_signature(self): + """Test List.run() method signature.""" + sig = inspect.signature(List.run) + params = list(sig.parameters.keys()) + assert "self" in params + assert "all" in params + + +class TestStatus: + """Test Status class.""" + + def test_status_import(self): + """Test that Status can be imported.""" + assert Status is not None + + def test_status_instantiation(self): + """Test Status instantiation.""" + config = Configuration() + status = Status(config) + assert status is not None + assert status.config == config + + def test_status_has_run_method(self): + """Test that Status has an async run method.""" + config = Configuration() + status = Status(config) + assert hasattr(status, "run") + assert inspect.iscoroutinefunction(status.run) + + def test_status_run_signature(self): + """Test Status.run() method signature.""" + sig = inspect.signature(Status.run) + params = list(sig.parameters.keys()) + assert "self" in params + assert "release_name" in params + + +class TestRollback: + """Test Rollback class.""" + + def test_rollback_import(self): + """Test that Rollback can be imported.""" + assert Rollback is not None + + def test_rollback_instantiation(self): + """Test Rollback instantiation.""" + config = Configuration() + rollback = Rollback(config) + assert rollback is not None + assert rollback.config == config + + def test_rollback_has_run_method(self): + """Test that Rollback has an async run method.""" + config = Configuration() + rollback = Rollback(config) + assert hasattr(rollback, "run") + assert inspect.iscoroutinefunction(rollback.run) + + def test_rollback_run_signature(self): + """Test Rollback.run() method signature.""" + sig = inspect.signature(Rollback.run) + params = list(sig.parameters.keys()) + assert "self" in params + assert "release_name" in params + assert "revision" in params + + +class TestGetValues: + """Test GetValues class.""" + + def test_getvalues_import(self): + """Test that GetValues can be imported.""" + assert GetValues is not None + + def test_getvalues_instantiation(self): + """Test GetValues instantiation.""" + config = Configuration() + get_values = GetValues(config) + assert get_values is not None + assert get_values.config == config + + def test_getvalues_has_run_method(self): + """Test that GetValues has an async run method.""" + config = Configuration() + get_values = GetValues(config) + assert hasattr(get_values, "run") + assert inspect.iscoroutinefunction(get_values.run) + + def test_getvalues_run_signature(self): + """Test GetValues.run() method signature.""" + sig = inspect.signature(GetValues.run) + params = list(sig.parameters.keys()) + assert "self" in params + assert "release_name" in params + assert "all" in params + + +class TestHistory: + """Test History class.""" + + def test_history_import(self): + """Test that History can be imported.""" + assert History is not None + + def test_history_instantiation(self): + """Test History instantiation.""" + config = Configuration() + history = History(config) + assert history is not None + assert history.config == config + + def test_history_has_run_method(self): + """Test that History has an async run method.""" + config = Configuration() + history = History(config) + assert hasattr(history, "run") + assert inspect.iscoroutinefunction(history.run) + + def test_history_run_signature(self): + """Test History.run() method signature.""" + sig = inspect.signature(History.run) + params = list(sig.parameters.keys()) + assert "self" in params + assert "release_name" in params + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/tests/test_chart.py b/tests/test_chart.py new file mode 100644 index 0000000..0e12b67 --- /dev/null +++ b/tests/test_chart.py @@ -0,0 +1,199 @@ +# Copyright 2025 Vantage Compute +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for Helm chart classes.""" + +import inspect + +import pytest + +from helm_sdkpy import Configuration, Lint, Package, Pull, ReleaseTest, Show, Test + + +class TestPull: + """Test Pull class.""" + + def test_pull_import(self): + """Test that Pull can be imported.""" + assert Pull is not None + + def test_pull_instantiation(self): + """Test Pull instantiation.""" + config = Configuration() + pull = Pull(config) + assert pull is not None + assert pull.config == config + + def test_pull_has_run_method(self): + """Test that Pull has an async run method.""" + config = Configuration() + pull = Pull(config) + assert hasattr(pull, "run") + assert inspect.iscoroutinefunction(pull.run) + + def test_pull_run_signature(self): + """Test Pull.run() method signature includes version parameter.""" + sig = inspect.signature(Pull.run) + params = list(sig.parameters.keys()) + assert "self" in params + assert "chart_ref" in params + assert "dest_dir" in params + assert "version" in params, "Pull.run() must have version parameter" + + def test_pull_run_version_default(self): + """Test that Pull.run() version parameter has None as default.""" + sig = inspect.signature(Pull.run) + version_param = sig.parameters.get("version") + assert version_param is not None + assert version_param.default is None + + +class TestShow: + """Test Show class.""" + + def test_show_import(self): + """Test that Show can be imported.""" + assert Show is not None + + def test_show_instantiation(self): + """Test Show instantiation.""" + config = Configuration() + show = Show(config) + assert show is not None + assert show.config == config + + def test_show_has_chart_method(self): + """Test that Show has an async chart method.""" + config = Configuration() + show = Show(config) + assert hasattr(show, "chart") + assert inspect.iscoroutinefunction(show.chart) + + def test_show_has_values_method(self): + """Test that Show has an async values method.""" + config = Configuration() + show = Show(config) + assert hasattr(show, "values") + assert inspect.iscoroutinefunction(show.values) + + def test_show_chart_signature(self): + """Test Show.chart() method signature.""" + sig = inspect.signature(Show.chart) + params = list(sig.parameters.keys()) + assert "self" in params + assert "chart_path" in params + + def test_show_values_signature(self): + """Test Show.values() method signature.""" + sig = inspect.signature(Show.values) + params = list(sig.parameters.keys()) + assert "self" in params + assert "chart_path" in params + + +class TestReleaseTest: + """Test ReleaseTest class (Helm test action).""" + + def test_releasetest_import(self): + """Test that ReleaseTest can be imported.""" + assert ReleaseTest is not None + + def test_test_alias_import(self): + """Test that Test alias works for backwards compatibility.""" + assert Test is not None + assert Test is ReleaseTest + + def test_releasetest_instantiation(self): + """Test ReleaseTest instantiation.""" + config = Configuration() + test = ReleaseTest(config) + assert test is not None + assert test.config == config + + def test_releasetest_has_run_method(self): + """Test that ReleaseTest has an async run method.""" + config = Configuration() + test = ReleaseTest(config) + assert hasattr(test, "run") + assert inspect.iscoroutinefunction(test.run) + + def test_releasetest_run_signature(self): + """Test ReleaseTest.run() method signature.""" + sig = inspect.signature(ReleaseTest.run) + params = list(sig.parameters.keys()) + assert "self" in params + assert "release_name" in params + + +class TestLint: + """Test Lint class.""" + + def test_lint_import(self): + """Test that Lint can be imported.""" + assert Lint is not None + + def test_lint_instantiation(self): + """Test Lint instantiation.""" + config = Configuration() + lint = Lint(config) + assert lint is not None + assert lint.config == config + + def test_lint_has_run_method(self): + """Test that Lint has an async run method.""" + config = Configuration() + lint = Lint(config) + assert hasattr(lint, "run") + assert inspect.iscoroutinefunction(lint.run) + + def test_lint_run_signature(self): + """Test Lint.run() method signature.""" + sig = inspect.signature(Lint.run) + params = list(sig.parameters.keys()) + assert "self" in params + assert "chart_path" in params + + +class TestPackage: + """Test Package class.""" + + def test_package_import(self): + """Test that Package can be imported.""" + assert Package is not None + + def test_package_instantiation(self): + """Test Package instantiation.""" + config = Configuration() + package = Package(config) + assert package is not None + assert package.config == config + + def test_package_has_run_method(self): + """Test that Package has an async run method.""" + config = Configuration() + package = Package(config) + assert hasattr(package, "run") + assert inspect.iscoroutinefunction(package.run) + + def test_package_run_signature(self): + """Test Package.run() method signature.""" + sig = inspect.signature(Package.run) + params = list(sig.parameters.keys()) + assert "self" in params + assert "chart_path" in params + assert "dest_dir" in params + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/tests/test_ffi.py b/tests/test_ffi.py new file mode 100644 index 0000000..7605099 --- /dev/null +++ b/tests/test_ffi.py @@ -0,0 +1,184 @@ +# Copyright 2025 Vantage Compute +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for FFI utilities.""" + +import pytest + +from helm_sdkpy._ffi import ( + _reset_for_tests, + check_error, + configure, + ffi, + get_library, + get_version, + string_from_c, +) +from helm_sdkpy.exceptions import HelmError + + +class TestGetLibrary: + """Test get_library function.""" + + def test_get_library_returns_library(self): + """Test that get_library returns a library object.""" + lib = get_library() + assert lib is not None + + def test_get_library_cached(self): + """Test that get_library returns same instance on multiple calls.""" + lib1 = get_library() + lib2 = get_library() + assert lib1 is lib2 + + def test_get_library_has_expected_functions(self): + """Test that library has expected C functions.""" + lib = get_library() + assert hasattr(lib, "helm_sdkpy_config_create") + assert hasattr(lib, "helm_sdkpy_config_destroy") + assert hasattr(lib, "helm_sdkpy_install") + assert hasattr(lib, "helm_sdkpy_upgrade") + assert hasattr(lib, "helm_sdkpy_uninstall") + assert hasattr(lib, "helm_sdkpy_list") + assert hasattr(lib, "helm_sdkpy_status") + assert hasattr(lib, "helm_sdkpy_rollback") + assert hasattr(lib, "helm_sdkpy_get_values") + assert hasattr(lib, "helm_sdkpy_history") + assert hasattr(lib, "helm_sdkpy_pull") + assert hasattr(lib, "helm_sdkpy_show_chart") + assert hasattr(lib, "helm_sdkpy_show_values") + assert hasattr(lib, "helm_sdkpy_test") + assert hasattr(lib, "helm_sdkpy_lint") + assert hasattr(lib, "helm_sdkpy_package") + assert hasattr(lib, "helm_sdkpy_repo_add") + assert hasattr(lib, "helm_sdkpy_repo_remove") + assert hasattr(lib, "helm_sdkpy_repo_list") + assert hasattr(lib, "helm_sdkpy_repo_update") + assert hasattr(lib, "helm_sdkpy_registry_login") + assert hasattr(lib, "helm_sdkpy_registry_logout") + assert hasattr(lib, "helm_sdkpy_push") + assert hasattr(lib, "helm_sdkpy_last_error") + assert hasattr(lib, "helm_sdkpy_free") + assert hasattr(lib, "helm_sdkpy_version_number") + assert hasattr(lib, "helm_sdkpy_version_string") + + +class TestGetVersion: + """Test get_version function.""" + + def test_get_version_returns_string(self): + """Test that get_version returns a string.""" + version = get_version() + assert isinstance(version, str) + + def test_get_version_not_empty(self): + """Test that version is not empty.""" + version = get_version() + assert len(version) > 0 + + def test_get_version_format(self): + """Test that version has expected format.""" + version = get_version() + # Version should contain 'helm-sdkpy' or version number + assert "helm" in version.lower() or any(c.isdigit() for c in version) + + +class TestConfigure: + """Test configure function.""" + + def test_configure_accepts_none(self): + """Test that configure accepts None to reset.""" + # This should not raise + configure(None) + + def test_configure_accepts_string(self): + """Test that configure accepts a path string.""" + # This should not raise, even if path doesn't exist + # (it only sets the path, doesn't validate it) + configure("/some/path/to/lib.so") + # Reset for other tests + configure(None) + + +class TestCheckError: + """Test check_error function.""" + + def test_check_error_zero_no_exception(self): + """Test that check_error with 0 does not raise.""" + # Should not raise + check_error(0) + + def test_check_error_nonzero_raises(self): + """Test that check_error with non-zero raises HelmError.""" + # First, we need to trigger an actual error in the library + # to set the error message. For this test, we'll just verify + # that when called with non-zero, it raises HelmError. + with pytest.raises(HelmError): + check_error(-1) + + +class TestStringFromC: + """Test string_from_c function.""" + + def test_string_from_c_null_returns_empty(self): + """Test that string_from_c with NULL returns empty string.""" + result = string_from_c(ffi.NULL) + assert result == "" + + def test_string_from_c_converts_string(self): + """Test that string_from_c converts C string to Python string.""" + # Create a C string using the library's allocator + lib = get_library() + # We can use version_string which returns a valid C string + version_ptr = lib.helm_sdkpy_version_string() + # Note: version_string returns a static pointer, so we shouldn't free it + # Just test that we can read it + if version_ptr != ffi.NULL: + result = ffi.string(version_ptr).decode("utf-8") + assert isinstance(result, str) + assert len(result) > 0 + + +class TestFfi: + """Test ffi object.""" + + def test_ffi_exists(self): + """Test that ffi object exists.""" + assert ffi is not None + + def test_ffi_can_create_char_array(self): + """Test that ffi can create char arrays.""" + test_str = "hello world" + c_str = ffi.new("char[]", test_str.encode("utf-8")) + assert c_str is not None + + def test_ffi_null_constant(self): + """Test that ffi.NULL exists.""" + assert ffi.NULL is not None + + +class TestResetForTests: + """Test _reset_for_tests function.""" + + def test_reset_for_tests_runs(self): + """Test that _reset_for_tests can be called.""" + # Just verify it doesn't raise + _reset_for_tests() + # Library should be reloadable after reset + lib = get_library() + assert lib is not None + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/tests/test_registry.py b/tests/test_registry.py index dd2d5a3..cb47082 100644 --- a/tests/test_registry.py +++ b/tests/test_registry.py @@ -1,10 +1,10 @@ """Tests for registry operations.""" -import asyncio +import inspect + import pytest -from helm_sdkpy import Configuration, RegistryLogin, RegistryLogout, Push -from helm_sdkpy.exceptions import HelmError +from helm_sdkpy import Configuration, Push, RegistryLogin, RegistryLogout class TestRegistryOperations: @@ -43,54 +43,60 @@ def test_push_instantiation(self): assert push is not None assert push.config == config - @pytest.mark.asyncio - async def test_registry_login_invalid_credentials(self): - """Test registry login with invalid credentials fails gracefully.""" - config = Configuration() - registry_login = RegistryLogin(config) - - # This should fail because credentials are invalid - with pytest.raises((HelmError, Exception)): - await registry_login.run( - hostname="ghcr.io", - username="invalid_user_12345", - password="invalid_password_12345" - ) - - @pytest.mark.asyncio - async def test_registry_logout_nonexistent(self): - """Test registry logout for a registry we're not logged into.""" - config = Configuration() - registry_logout = RegistryLogout(config) - - # Logout from a registry we were never logged into - # This should succeed (no-op) or fail gracefully - try: - await registry_logout.run(hostname="nonexistent-registry.example.com") - except Exception: - # It's okay if this fails - registry might not exist in credentials - pass - def test_registry_login_has_run_method(self): """Test that RegistryLogin has an async run method.""" config = Configuration() registry_login = RegistryLogin(config) assert hasattr(registry_login, "run") - assert asyncio.iscoroutinefunction(registry_login.run) + assert inspect.iscoroutinefunction(registry_login.run) def test_registry_logout_has_run_method(self): """Test that RegistryLogout has an async run method.""" config = Configuration() registry_logout = RegistryLogout(config) assert hasattr(registry_logout, "run") - assert asyncio.iscoroutinefunction(registry_logout.run) + assert inspect.iscoroutinefunction(registry_logout.run) def test_push_has_run_method(self): """Test that Push has an async run method.""" config = Configuration() push = Push(config) assert hasattr(push, "run") - assert asyncio.iscoroutinefunction(push.run) + assert inspect.iscoroutinefunction(push.run) + + def test_registry_login_run_signature(self): + """Test RegistryLogin.run() method signature.""" + sig = inspect.signature(RegistryLogin.run) + params = list(sig.parameters.keys()) + assert "self" in params + assert "hostname" in params + assert "username" in params + assert "password" in params + assert "cert_file" in params + assert "key_file" in params + assert "ca_file" in params + assert "insecure" in params + assert "plain_http" in params + + def test_registry_logout_run_signature(self): + """Test RegistryLogout.run() method signature.""" + sig = inspect.signature(RegistryLogout.run) + params = list(sig.parameters.keys()) + assert "self" in params + assert "hostname" in params + + def test_push_run_signature(self): + """Test Push.run() method signature.""" + sig = inspect.signature(Push.run) + params = list(sig.parameters.keys()) + assert "self" in params + assert "chart_path" in params + assert "remote" in params + assert "cert_file" in params + assert "key_file" in params + assert "ca_file" in params + assert "insecure_skip_tls_verify" in params + assert "plain_http" in params if __name__ == "__main__": diff --git a/tests/test_repo.py b/tests/test_repo.py new file mode 100644 index 0000000..364a712 --- /dev/null +++ b/tests/test_repo.py @@ -0,0 +1,149 @@ +# Copyright 2025 Vantage Compute +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Tests for Helm repository classes.""" + +import inspect + +import pytest + +from helm_sdkpy import Configuration, RepoAdd, RepoList, RepoRemove, RepoUpdate + + +class TestRepoAdd: + """Test RepoAdd class.""" + + def test_repoadd_import(self): + """Test that RepoAdd can be imported.""" + assert RepoAdd is not None + + def test_repoadd_instantiation(self): + """Test RepoAdd instantiation.""" + config = Configuration() + repo_add = RepoAdd(config) + assert repo_add is not None + assert repo_add.config == config + + def test_repoadd_has_run_method(self): + """Test that RepoAdd has an async run method.""" + config = Configuration() + repo_add = RepoAdd(config) + assert hasattr(repo_add, "run") + assert inspect.iscoroutinefunction(repo_add.run) + + def test_repoadd_run_signature(self): + """Test RepoAdd.run() method signature.""" + sig = inspect.signature(RepoAdd.run) + params = list(sig.parameters.keys()) + assert "self" in params + assert "name" in params + assert "url" in params + assert "username" in params + assert "password" in params + assert "insecure_skip_tls_verify" in params + assert "pass_credentials_all" in params + assert "cert_file" in params + assert "key_file" in params + assert "ca_file" in params + + +class TestRepoRemove: + """Test RepoRemove class.""" + + def test_reporemove_import(self): + """Test that RepoRemove can be imported.""" + assert RepoRemove is not None + + def test_reporemove_instantiation(self): + """Test RepoRemove instantiation.""" + config = Configuration() + repo_remove = RepoRemove(config) + assert repo_remove is not None + assert repo_remove.config == config + + def test_reporemove_has_run_method(self): + """Test that RepoRemove has an async run method.""" + config = Configuration() + repo_remove = RepoRemove(config) + assert hasattr(repo_remove, "run") + assert inspect.iscoroutinefunction(repo_remove.run) + + def test_reporemove_run_signature(self): + """Test RepoRemove.run() method signature.""" + sig = inspect.signature(RepoRemove.run) + params = list(sig.parameters.keys()) + assert "self" in params + assert "name" in params + + +class TestRepoList: + """Test RepoList class.""" + + def test_repolist_import(self): + """Test that RepoList can be imported.""" + assert RepoList is not None + + def test_repolist_instantiation(self): + """Test RepoList instantiation.""" + config = Configuration() + repo_list = RepoList(config) + assert repo_list is not None + assert repo_list.config == config + + def test_repolist_has_run_method(self): + """Test that RepoList has an async run method.""" + config = Configuration() + repo_list = RepoList(config) + assert hasattr(repo_list, "run") + assert inspect.iscoroutinefunction(repo_list.run) + + def test_repolist_run_signature(self): + """Test RepoList.run() method signature.""" + sig = inspect.signature(RepoList.run) + params = list(sig.parameters.keys()) + assert "self" in params + # RepoList.run() takes no arguments besides self + + +class TestRepoUpdate: + """Test RepoUpdate class.""" + + def test_repoupdate_import(self): + """Test that RepoUpdate can be imported.""" + assert RepoUpdate is not None + + def test_repoupdate_instantiation(self): + """Test RepoUpdate instantiation.""" + config = Configuration() + repo_update = RepoUpdate(config) + assert repo_update is not None + assert repo_update.config == config + + def test_repoupdate_has_run_method(self): + """Test that RepoUpdate has an async run method.""" + config = Configuration() + repo_update = RepoUpdate(config) + assert hasattr(repo_update, "run") + assert inspect.iscoroutinefunction(repo_update.run) + + def test_repoupdate_run_signature(self): + """Test RepoUpdate.run() method signature.""" + sig = inspect.signature(RepoUpdate.run) + params = list(sig.parameters.keys()) + assert "self" in params + assert "name" in params + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/uv.lock b/uv.lock index 4a653ed..1f961f3 100644 --- a/uv.lock +++ b/uv.lock @@ -165,6 +165,7 @@ dev = [ { name = "coverage" }, { name = "pyright" }, { name = "pytest" }, + { name = "pytest-asyncio" }, { name = "pytest-mock" }, { name = "pytest-order" }, { name = "python-dotenv" }, @@ -175,6 +176,7 @@ dev = [ dev = [ { name = "codespell" }, { name = "pyright" }, + { name = "pytest-asyncio" }, { name = "pytest-cov" }, { name = "ruff" }, ] @@ -186,6 +188,7 @@ requires-dist = [ { name = "coverage", extras = ["toml"], marker = "extra == 'dev'", specifier = "~=7.8" }, { name = "pyright", marker = "extra == 'dev'" }, { name = "pytest", marker = "extra == 'dev'", specifier = "~=8.3" }, + { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = "~=0.24" }, { name = "pytest-mock", marker = "extra == 'dev'", specifier = "~=3.14" }, { name = "pytest-order", marker = "extra == 'dev'", specifier = "~=1.3" }, { name = "python-dotenv", marker = "extra == 'dev'", specifier = "~=1.0" }, @@ -197,6 +200,7 @@ provides-extras = ["dev"] dev = [ { name = "codespell", specifier = ">=2.4.1" }, { name = "pyright", specifier = ">=1.1.406" }, + { name = "pytest-asyncio", specifier = ">=0.24" }, { name = "pytest-cov", specifier = ">=7.0.0" }, { name = "ruff", specifier = ">=0.14.1" }, ] @@ -284,6 +288,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a8/a4/20da314d277121d6534b3a980b29035dcd51e6744bd79075a6ce8fa4eb8d/pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79", size = 365750, upload-time = "2025-09-04T14:34:20.226Z" }, ] +[[package]] +name = "pytest-asyncio" +version = "0.26.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/c4/453c52c659521066969523e87d85d54139bbd17b78f09532fb8eb8cdb58e/pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f", size = 54156, upload-time = "2025-03-25T06:22:28.883Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/7f/338843f449ace853647ace35870874f69a764d251872ed1b4de9f234822c/pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0", size = 19694, upload-time = "2025-03-25T06:22:27.807Z" }, +] + [[package]] name = "pytest-cov" version = "7.0.0"