-
Notifications
You must be signed in to change notification settings - Fork 42
Update version to 0.0.7b10 and enhance secret retrieval logic in Runtime class #277
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
NiveditJain
merged 3 commits into
exospherehost:main
from
NiveditJain:improved-secret-check
Aug 24, 2025
+337
−14
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1 +1 @@ | ||
| version = "0.0.7b9" | ||
| version = "0.0.7b10" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| import pytest | ||
| from pydantic import BaseModel | ||
| from exospherehost.node.BaseNode import BaseNode | ||
|
|
||
|
|
||
| class TestBaseNodeAbstract: | ||
| """Test the abstract BaseNode class and its NotImplementedError.""" | ||
|
|
||
| def test_base_node_abstract_execute(self): | ||
| """Test that BaseNode.execute raises NotImplementedError.""" | ||
| # Create a concrete subclass that implements execute but raises NotImplementedError | ||
| class ConcreteNode(BaseNode): | ||
| class Inputs(BaseModel): | ||
| name: str | ||
|
|
||
| class Outputs(BaseModel): | ||
| message: str | ||
|
|
||
| class Secrets(BaseModel): | ||
| pass | ||
|
|
||
| async def execute(self): | ||
| raise NotImplementedError("execute method must be implemented by all concrete node classes") | ||
|
|
||
| node = ConcreteNode() | ||
|
|
||
| with pytest.raises(NotImplementedError, match="execute method must be implemented by all concrete node classes"): | ||
| # This should raise NotImplementedError | ||
| import asyncio | ||
| asyncio.run(node.execute()) | ||
|
|
||
| def test_base_node_abstract_execute_with_inputs(self): | ||
| """Test that BaseNode._execute raises NotImplementedError when execute is not implemented.""" | ||
| # Create a concrete subclass that implements execute but raises NotImplementedError | ||
| class ConcreteNode(BaseNode): | ||
| class Inputs(BaseModel): | ||
| name: str | ||
|
|
||
| class Outputs(BaseModel): | ||
| message: str | ||
|
|
||
| class Secrets(BaseModel): | ||
| pass | ||
|
|
||
| async def execute(self): | ||
| raise NotImplementedError("execute method must be implemented by all concrete node classes") | ||
|
|
||
| node = ConcreteNode() | ||
|
|
||
| with pytest.raises(NotImplementedError, match="execute method must be implemented by all concrete node classes"): | ||
| # This should raise NotImplementedError | ||
| import asyncio | ||
| asyncio.run(node._execute(node.Inputs(name="test"), node.Secrets())) # type: ignore | ||
|
|
||
| def test_base_node_initialization(self): | ||
| """Test that BaseNode initializes correctly.""" | ||
| # Create a concrete subclass | ||
| class ConcreteNode(BaseNode): | ||
| class Inputs(BaseModel): | ||
| name: str | ||
|
|
||
| class Outputs(BaseModel): | ||
| message: str | ||
|
|
||
| class Secrets(BaseModel): | ||
| pass | ||
|
|
||
| async def execute(self): | ||
| return self.Outputs(message="test") | ||
|
|
||
| node = ConcreteNode() | ||
| assert node.inputs is None | ||
|
|
||
| def test_base_node_inputs_class(self): | ||
| """Test that BaseNode has Inputs class.""" | ||
| assert hasattr(BaseNode, 'Inputs') | ||
| assert issubclass(BaseNode.Inputs, BaseModel) | ||
|
|
||
| def test_base_node_outputs_class(self): | ||
| """Test that BaseNode has Outputs class.""" | ||
| assert hasattr(BaseNode, 'Outputs') | ||
| assert issubclass(BaseNode.Outputs, BaseModel) | ||
|
|
||
| def test_base_node_secrets_class(self): | ||
| """Test that BaseNode has Secrets class.""" | ||
| assert hasattr(BaseNode, 'Secrets') | ||
| assert issubclass(BaseNode.Secrets, BaseModel) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,205 @@ | ||
| import pytest | ||
| import asyncio | ||
| import warnings | ||
| from unittest.mock import AsyncMock, patch, MagicMock | ||
| from pydantic import BaseModel | ||
| from exospherehost.runtime import Runtime, _setup_default_logging | ||
| from exospherehost.node.BaseNode import BaseNode | ||
|
|
||
|
|
||
| class MockTestNode(BaseNode): | ||
| class Inputs(BaseModel): | ||
| name: str | ||
|
|
||
| class Outputs(BaseModel): | ||
| message: str | ||
|
|
||
| class Secrets(BaseModel): | ||
| api_key: str | ||
|
|
||
| async def execute(self): | ||
| return self.Outputs(message=f"Hello {self.inputs.name}") # type: ignore | ||
|
|
||
|
|
||
| class MockTestNodeWithNonStringFields(BaseNode): | ||
| class Inputs(BaseModel): | ||
| name: str | ||
| count: int # This should cause validation error | ||
|
|
||
| class Outputs(BaseModel): | ||
| message: str | ||
|
|
||
| class Secrets(BaseModel): | ||
| api_key: str | ||
|
|
||
| async def execute(self): | ||
| return self.Outputs(message=f"Hello {self.inputs.name}") # type: ignore | ||
|
|
||
|
|
||
| class MockTestNodeWithoutSecrets(BaseNode): | ||
| class Inputs(BaseModel): | ||
| name: str | ||
|
|
||
| class Outputs(BaseModel): | ||
| message: str | ||
|
|
||
| class Secrets(BaseModel): | ||
| pass # Empty secrets | ||
|
|
||
| async def execute(self): | ||
| return self.Outputs(message=f"Hello {self.inputs.name}") # type: ignore | ||
|
|
||
|
|
||
| class MockTestNodeWithError(BaseNode): | ||
| class Inputs(BaseModel): | ||
| should_fail: str | ||
|
|
||
| class Outputs(BaseModel): | ||
| result: str | ||
|
|
||
| class Secrets(BaseModel): | ||
| api_key: str | ||
|
|
||
| async def execute(self): | ||
| if self.inputs.should_fail == "true": # type: ignore | ||
| raise ValueError("Test error") | ||
| return self.Outputs(result="success") | ||
|
|
||
|
|
||
| class TestRuntimeEdgeCases: | ||
| """Test edge cases and error handling scenarios in the Runtime class.""" | ||
|
|
||
| def test_setup_default_logging_disabled(self, monkeypatch): | ||
| """Test that _setup_default_logging returns early when disabled.""" | ||
| monkeypatch.setenv('EXOSPHERE_DISABLE_DEFAULT_LOGGING', 'true') | ||
|
|
||
| # This should not raise any exceptions and should return early | ||
| _setup_default_logging() | ||
|
|
||
| def test_setup_default_logging_invalid_level(self, monkeypatch): | ||
| """Test _setup_default_logging with invalid log level.""" | ||
| monkeypatch.setenv('EXOSPHERE_LOG_LEVEL', 'INVALID_LEVEL') | ||
|
|
||
| # Should fall back to INFO level | ||
| _setup_default_logging() | ||
|
|
||
| def test_runtime_validation_non_string_fields(self): | ||
| """Test that Runtime validates node fields are strings.""" | ||
| with pytest.raises(ValueError, match="must be of type str"): | ||
| Runtime( | ||
| namespace="test", | ||
| name="test", | ||
| nodes=[MockTestNodeWithNonStringFields], | ||
| state_manager_uri="http://localhost:8080", | ||
| key="test_key" | ||
| ) | ||
|
|
||
| def test_runtime_validation_duplicate_node_names(self): | ||
| """Test that Runtime validates no duplicate node names.""" | ||
| # Create two classes with the same name | ||
| class TestNode1(MockTestNode): | ||
| pass | ||
|
|
||
| class TestNode2(MockTestNode): | ||
| pass | ||
|
|
||
| # Rename the second class to have the same name as the first | ||
| TestNode2.__name__ = "TestNode1" | ||
|
|
||
| # Suppress the RuntimeWarning about unawaited coroutines | ||
| with warnings.catch_warnings(): | ||
| warnings.filterwarnings("ignore", message=".*coroutine.*was never awaited.*", category=RuntimeWarning) | ||
| with pytest.raises(ValueError, match="Duplicate node class names found"): | ||
| Runtime( | ||
| namespace="test", | ||
| name="test", | ||
| nodes=[TestNode1, TestNode2], | ||
| state_manager_uri="http://localhost:8080", | ||
| key="test_key" | ||
| ) | ||
|
|
||
| def test_need_secrets_empty_secrets(self): | ||
| """Test _need_secrets with empty secrets class.""" | ||
| runtime = Runtime( | ||
| namespace="test", | ||
| name="test", | ||
| nodes=[MockTestNodeWithoutSecrets], | ||
| state_manager_uri="http://localhost:8080", | ||
| key="test_key" | ||
| ) | ||
|
|
||
| # Should return False for empty secrets | ||
| assert not runtime._need_secrets(MockTestNodeWithoutSecrets) | ||
|
|
||
| def test_need_secrets_with_secrets(self): | ||
| """Test _need_secrets with secrets class that has fields.""" | ||
| runtime = Runtime( | ||
| namespace="test", | ||
| name="test", | ||
| nodes=[MockTestNode], | ||
| state_manager_uri="http://localhost:8080", | ||
| key="test_key" | ||
| ) | ||
|
|
||
| # Should return True for secrets with fields | ||
| assert runtime._need_secrets(MockTestNode) | ||
|
|
||
| @pytest.mark.asyncio | ||
| async def test_enqueue_error_handling(self): | ||
| """Test error handling in _enqueue method.""" | ||
| runtime = Runtime( | ||
| namespace="test", | ||
| name="test", | ||
| nodes=[MockTestNode], | ||
| state_manager_uri="http://localhost:8080", | ||
| key="test_key" | ||
| ) | ||
|
|
||
| # Mock _enqueue_call to raise an exception | ||
| with patch.object(runtime, '_enqueue_call', side_effect=Exception("Test error")): | ||
| # This should not raise an exception but log an error | ||
| # We'll test this by checking that the method doesn't crash | ||
| task = asyncio.create_task(runtime._enqueue()) | ||
| await asyncio.sleep(0.1) # Let it run briefly | ||
| task.cancel() | ||
| try: | ||
| await task | ||
| except asyncio.CancelledError: | ||
| pass | ||
|
|
||
| def test_start_without_running_loop(self): | ||
| """Test start method when no event loop is running.""" | ||
| runtime = Runtime( | ||
| namespace="test", | ||
| name="test", | ||
| nodes=[MockTestNode], | ||
| state_manager_uri="http://localhost:8080", | ||
| key="test_key" | ||
| ) | ||
|
|
||
| # Mock _start to avoid actual execution | ||
| with patch.object(runtime, '_start', new_callable=AsyncMock): | ||
| # This should not raise an exception | ||
| result = runtime.start() | ||
| assert result is None | ||
|
|
||
| def test_start_with_running_loop(self): | ||
| """Test start method when an event loop is already running.""" | ||
| runtime = Runtime( | ||
| namespace="test", | ||
| name="test", | ||
| nodes=[MockTestNode], | ||
| state_manager_uri="http://localhost:8080", | ||
| key="test_key" | ||
| ) | ||
|
|
||
| # Mock _start to avoid actual execution | ||
| with patch.object(runtime, '_start', new_callable=AsyncMock): | ||
| # Create a mock loop | ||
| mock_loop = MagicMock() | ||
| mock_task = MagicMock() | ||
| mock_loop.create_task.return_value = mock_task | ||
|
|
||
| with patch('asyncio.get_running_loop', return_value=mock_loop): | ||
| result = runtime.start() | ||
| assert result == mock_task |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.