spaceforge 0.1.0.dev0__py3-none-any.whl → 1.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- spaceforge/__init__.py +12 -4
- spaceforge/__main__.py +3 -3
- spaceforge/_version.py +0 -1
- spaceforge/_version_scm.py +34 -0
- spaceforge/cls.py +24 -14
- spaceforge/conftest.py +89 -0
- spaceforge/generator.py +129 -56
- spaceforge/plugin.py +199 -22
- spaceforge/runner.py +3 -15
- spaceforge/schema.json +45 -22
- spaceforge/templates/binary_install.sh.j2 +24 -0
- spaceforge/templates/ensure_spaceforge_and_run.sh.j2 +24 -0
- spaceforge/{generator_test.py → test_generator.py} +265 -53
- spaceforge/test_generator_binaries.py +194 -0
- spaceforge/test_generator_core.py +180 -0
- spaceforge/test_generator_hooks.py +90 -0
- spaceforge/test_generator_parameters.py +59 -0
- spaceforge/test_plugin.py +357 -0
- spaceforge/test_plugin_file_operations.py +118 -0
- spaceforge/test_plugin_hooks.py +100 -0
- spaceforge/test_plugin_inheritance.py +102 -0
- spaceforge/{runner_test.py → test_runner.py} +5 -68
- spaceforge/test_runner_cli.py +69 -0
- spaceforge/test_runner_core.py +124 -0
- spaceforge/test_runner_execution.py +169 -0
- spaceforge-1.0.1.dist-info/METADATA +606 -0
- spaceforge-1.0.1.dist-info/RECORD +33 -0
- spaceforge/plugin_test.py +0 -621
- spaceforge-0.1.0.dev0.dist-info/METADATA +0 -163
- spaceforge-0.1.0.dev0.dist-info/RECORD +0 -19
- /spaceforge/{cls_test.py → test_cls.py} +0 -0
- {spaceforge-0.1.0.dev0.dist-info → spaceforge-1.0.1.dist-info}/WHEEL +0 -0
- {spaceforge-0.1.0.dev0.dist-info → spaceforge-1.0.1.dist-info}/entry_points.txt +0 -0
- {spaceforge-0.1.0.dev0.dist-info → spaceforge-1.0.1.dist-info}/licenses/LICENSE +0 -0
- {spaceforge-0.1.0.dev0.dist-info → spaceforge-1.0.1.dist-info}/top_level.txt +0 -0
spaceforge/plugin_test.py
DELETED
|
@@ -1,621 +0,0 @@
|
|
|
1
|
-
import json
|
|
2
|
-
import logging
|
|
3
|
-
import os
|
|
4
|
-
import subprocess
|
|
5
|
-
import tempfile
|
|
6
|
-
from typing import Any, Dict
|
|
7
|
-
from unittest.mock import Mock, patch
|
|
8
|
-
|
|
9
|
-
import pytest
|
|
10
|
-
|
|
11
|
-
from spaceforge.plugin import SpaceforgePlugin
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
class TestSpaceforgePlugin:
|
|
15
|
-
"""Test the base SpaceforgePlugin class."""
|
|
16
|
-
|
|
17
|
-
def setup_method(self) -> None:
|
|
18
|
-
"""Setup test fixtures."""
|
|
19
|
-
self.temp_dir = tempfile.mkdtemp()
|
|
20
|
-
|
|
21
|
-
def teardown_method(self) -> None:
|
|
22
|
-
"""Cleanup test fixtures."""
|
|
23
|
-
import shutil
|
|
24
|
-
|
|
25
|
-
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
|
26
|
-
|
|
27
|
-
def test_spacepy_plugin_init_defaults(self) -> None:
|
|
28
|
-
"""Test SpaceforgePlugin initialization with default environment."""
|
|
29
|
-
with patch.dict(os.environ, {}, clear=True):
|
|
30
|
-
plugin = SpaceforgePlugin()
|
|
31
|
-
|
|
32
|
-
assert plugin._api_token is False
|
|
33
|
-
assert plugin._spacelift_domain is False
|
|
34
|
-
assert plugin._api_enabled is False
|
|
35
|
-
assert plugin._workspace_root == os.getcwd()
|
|
36
|
-
assert isinstance(plugin.logger, logging.Logger)
|
|
37
|
-
|
|
38
|
-
def test_spacepy_plugin_init_with_api_credentials(self) -> None:
|
|
39
|
-
"""Test SpaceforgePlugin initialization with API credentials."""
|
|
40
|
-
test_env = {
|
|
41
|
-
"SPACELIFT_API_TOKEN": "test_token",
|
|
42
|
-
"TF_VAR_spacelift_graphql_endpoint": "https://test.spacelift.io",
|
|
43
|
-
"WORKSPACE_ROOT": "/test/workspace",
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
with patch.dict(os.environ, test_env, clear=True):
|
|
47
|
-
plugin = SpaceforgePlugin()
|
|
48
|
-
|
|
49
|
-
assert plugin._api_token == "test_token"
|
|
50
|
-
assert plugin._spacelift_domain == "https://test.spacelift.io"
|
|
51
|
-
assert plugin._api_enabled is True
|
|
52
|
-
assert plugin._workspace_root == "/test/workspace"
|
|
53
|
-
|
|
54
|
-
def test_spacepy_plugin_init_domain_trailing_slash(self) -> None:
|
|
55
|
-
"""Test domain with trailing slash gets normalized."""
|
|
56
|
-
test_env = {
|
|
57
|
-
"SPACELIFT_API_TOKEN": "test_token",
|
|
58
|
-
"TF_VAR_spacelift_graphql_endpoint": "https://test.spacelift.io/",
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
with patch.dict(os.environ, test_env, clear=True):
|
|
62
|
-
plugin = SpaceforgePlugin()
|
|
63
|
-
|
|
64
|
-
assert plugin._spacelift_domain == "https://test.spacelift.io"
|
|
65
|
-
assert plugin._api_enabled is True
|
|
66
|
-
|
|
67
|
-
def test_spacepy_plugin_init_domain_no_https(self) -> None:
|
|
68
|
-
"""Test domain without https:// prefix disables API."""
|
|
69
|
-
test_env = {
|
|
70
|
-
"SPACELIFT_API_TOKEN": "test_token",
|
|
71
|
-
"TF_VAR_spacelift_graphql_endpoint": "test.spacelift.io",
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
with patch.dict(os.environ, test_env, clear=True):
|
|
75
|
-
plugin = SpaceforgePlugin()
|
|
76
|
-
|
|
77
|
-
assert plugin._spacelift_domain == "test.spacelift.io"
|
|
78
|
-
assert plugin._api_enabled is False
|
|
79
|
-
|
|
80
|
-
def test_spacepy_plugin_init_partial_credentials(self) -> None:
|
|
81
|
-
"""Test initialization with only token or only domain."""
|
|
82
|
-
# Only token, no domain
|
|
83
|
-
with patch.dict(os.environ, {"SPACELIFT_API_TOKEN": "test_token"}, clear=True):
|
|
84
|
-
plugin = SpaceforgePlugin()
|
|
85
|
-
assert plugin._api_enabled is False
|
|
86
|
-
|
|
87
|
-
# Only domain, no token
|
|
88
|
-
with patch.dict(
|
|
89
|
-
os.environ,
|
|
90
|
-
{"TF_VAR_spacelift_graphql_endpoint": "https://test.spacelift.io"},
|
|
91
|
-
clear=True,
|
|
92
|
-
):
|
|
93
|
-
plugin = SpaceforgePlugin()
|
|
94
|
-
assert plugin._api_enabled is False
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
class TestSpaceforgePluginLogging:
|
|
98
|
-
"""Test the logging functionality."""
|
|
99
|
-
|
|
100
|
-
def test_logger_setup_basic(self) -> None:
|
|
101
|
-
"""Test basic logger setup."""
|
|
102
|
-
with patch.dict(os.environ, {}, clear=True):
|
|
103
|
-
plugin = SpaceforgePlugin()
|
|
104
|
-
|
|
105
|
-
assert plugin.logger.name == "spaceforge.SpaceforgePlugin"
|
|
106
|
-
assert len(plugin.logger.handlers) >= 1
|
|
107
|
-
# Logger level might be DEBUG from previous tests, check effective level
|
|
108
|
-
assert plugin.logger.getEffectiveLevel() <= logging.INFO
|
|
109
|
-
|
|
110
|
-
def test_logger_setup_debug_mode(self) -> None:
|
|
111
|
-
"""Test logger setup with debug mode enabled."""
|
|
112
|
-
test_env = {"SPACELIFT_DEBUG": "true"}
|
|
113
|
-
|
|
114
|
-
with patch.dict(os.environ, test_env, clear=True):
|
|
115
|
-
plugin = SpaceforgePlugin()
|
|
116
|
-
|
|
117
|
-
assert plugin.logger.level == logging.DEBUG
|
|
118
|
-
|
|
119
|
-
def test_logger_setup_with_run_id(self) -> None:
|
|
120
|
-
"""Test logger setup with run ID in environment."""
|
|
121
|
-
test_env = {"TF_VAR_spacelift_run_id": "run-123"}
|
|
122
|
-
|
|
123
|
-
with patch.dict(os.environ, test_env, clear=True):
|
|
124
|
-
plugin = SpaceforgePlugin()
|
|
125
|
-
|
|
126
|
-
# Test that the formatter includes the run ID
|
|
127
|
-
formatter = plugin.logger.handlers[0].formatter
|
|
128
|
-
assert formatter is not None
|
|
129
|
-
record = logging.LogRecord(
|
|
130
|
-
name="test",
|
|
131
|
-
level=logging.INFO,
|
|
132
|
-
pathname="",
|
|
133
|
-
lineno=0,
|
|
134
|
-
msg="test message",
|
|
135
|
-
args=(),
|
|
136
|
-
exc_info=None,
|
|
137
|
-
)
|
|
138
|
-
record.levelname = "INFO" # Set levelname explicitly
|
|
139
|
-
formatted = formatter.format(record)
|
|
140
|
-
# The default run_id is "local" when TF_VAR_spacelift_run_id is not set
|
|
141
|
-
# But we set it above, so it should be there, but let's check for the actual format
|
|
142
|
-
assert "[run-123]" in formatted or "[local]" in formatted
|
|
143
|
-
|
|
144
|
-
def test_logger_color_formatting(self) -> None:
|
|
145
|
-
"""Test color formatting for different log levels."""
|
|
146
|
-
plugin = SpaceforgePlugin()
|
|
147
|
-
formatter = plugin.logger.handlers[0].formatter
|
|
148
|
-
assert formatter is not None
|
|
149
|
-
|
|
150
|
-
# Test different log levels
|
|
151
|
-
levels_to_test = [
|
|
152
|
-
(logging.INFO, "INFO"),
|
|
153
|
-
(logging.DEBUG, "DEBUG"),
|
|
154
|
-
(logging.WARNING, "WARNING"),
|
|
155
|
-
(logging.ERROR, "ERROR"),
|
|
156
|
-
]
|
|
157
|
-
|
|
158
|
-
for level, level_name in levels_to_test:
|
|
159
|
-
record = logging.LogRecord(
|
|
160
|
-
name="test",
|
|
161
|
-
level=level,
|
|
162
|
-
pathname="",
|
|
163
|
-
lineno=0,
|
|
164
|
-
msg="test message",
|
|
165
|
-
args=(),
|
|
166
|
-
exc_info=None,
|
|
167
|
-
)
|
|
168
|
-
record.levelname = level_name
|
|
169
|
-
formatted = formatter.format(record)
|
|
170
|
-
|
|
171
|
-
# Should contain color codes and plugin name
|
|
172
|
-
assert "\033[" in formatted # ANSI color codes
|
|
173
|
-
assert "(SpaceforgePlugin)" in formatted
|
|
174
|
-
assert "test message" in formatted
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
class TestSpaceforgePluginHooks:
|
|
178
|
-
"""Test hook methods."""
|
|
179
|
-
|
|
180
|
-
def test_default_hook_methods_exist(self) -> None:
|
|
181
|
-
"""Test that all expected hook methods exist and are callable."""
|
|
182
|
-
plugin = SpaceforgePlugin()
|
|
183
|
-
|
|
184
|
-
expected_hooks = [
|
|
185
|
-
"before_init",
|
|
186
|
-
"after_init",
|
|
187
|
-
"before_plan",
|
|
188
|
-
"after_plan",
|
|
189
|
-
"before_apply",
|
|
190
|
-
"after_apply",
|
|
191
|
-
"before_perform",
|
|
192
|
-
"after_perform",
|
|
193
|
-
"before_destroy",
|
|
194
|
-
"after_destroy",
|
|
195
|
-
"after_run",
|
|
196
|
-
]
|
|
197
|
-
|
|
198
|
-
for hook_name in expected_hooks:
|
|
199
|
-
assert hasattr(plugin, hook_name)
|
|
200
|
-
hook_method = getattr(plugin, hook_name)
|
|
201
|
-
assert callable(hook_method)
|
|
202
|
-
|
|
203
|
-
# Should be able to call without error (default implementation is pass)
|
|
204
|
-
hook_method()
|
|
205
|
-
|
|
206
|
-
def test_get_available_hooks_base_class(self) -> None:
|
|
207
|
-
"""Test get_available_hooks on base class returns expected hooks."""
|
|
208
|
-
plugin = SpaceforgePlugin()
|
|
209
|
-
|
|
210
|
-
# Base class defines default hook methods
|
|
211
|
-
hooks = plugin.get_available_hooks()
|
|
212
|
-
expected_hooks = [
|
|
213
|
-
"before_init",
|
|
214
|
-
"after_init",
|
|
215
|
-
"before_plan",
|
|
216
|
-
"after_plan",
|
|
217
|
-
"before_apply",
|
|
218
|
-
"after_apply",
|
|
219
|
-
"before_perform",
|
|
220
|
-
"after_perform",
|
|
221
|
-
"before_destroy",
|
|
222
|
-
"after_destroy",
|
|
223
|
-
"after_run",
|
|
224
|
-
]
|
|
225
|
-
|
|
226
|
-
for expected_hook in expected_hooks:
|
|
227
|
-
assert expected_hook in hooks
|
|
228
|
-
|
|
229
|
-
def test_get_available_hooks_with_overrides(self) -> None:
|
|
230
|
-
"""Test get_available_hooks with overridden methods."""
|
|
231
|
-
|
|
232
|
-
class TestPluginWithHooks(SpaceforgePlugin):
|
|
233
|
-
def after_plan(self) -> None:
|
|
234
|
-
pass
|
|
235
|
-
|
|
236
|
-
def before_apply(self) -> None:
|
|
237
|
-
pass
|
|
238
|
-
|
|
239
|
-
def custom_method(self) -> None: # Not a hook
|
|
240
|
-
pass
|
|
241
|
-
|
|
242
|
-
plugin = TestPluginWithHooks()
|
|
243
|
-
hooks = plugin.get_available_hooks()
|
|
244
|
-
|
|
245
|
-
# Should include all base hooks plus any custom recognized hooks
|
|
246
|
-
assert "after_plan" in hooks
|
|
247
|
-
assert "before_apply" in hooks
|
|
248
|
-
assert "custom_method" not in hooks # Not a recognized hook
|
|
249
|
-
# Should have all the expected hooks from the base class
|
|
250
|
-
expected_hooks = [
|
|
251
|
-
"before_init",
|
|
252
|
-
"after_init",
|
|
253
|
-
"before_plan",
|
|
254
|
-
"after_plan",
|
|
255
|
-
"before_apply",
|
|
256
|
-
"after_apply",
|
|
257
|
-
"before_perform",
|
|
258
|
-
"after_perform",
|
|
259
|
-
"before_destroy",
|
|
260
|
-
"after_destroy",
|
|
261
|
-
"after_run",
|
|
262
|
-
]
|
|
263
|
-
|
|
264
|
-
for expected_hook in expected_hooks:
|
|
265
|
-
assert expected_hook in hooks
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
class TestSpaceforgePluginCLI:
|
|
269
|
-
"""Test CLI functionality."""
|
|
270
|
-
|
|
271
|
-
def test_run_cli_success(self) -> None:
|
|
272
|
-
"""Test successful CLI command execution."""
|
|
273
|
-
plugin = SpaceforgePlugin()
|
|
274
|
-
|
|
275
|
-
with patch("subprocess.Popen") as mock_popen:
|
|
276
|
-
mock_process = Mock()
|
|
277
|
-
mock_process.communicate.return_value = (b"success output\n", None)
|
|
278
|
-
mock_process.returncode = 0
|
|
279
|
-
mock_popen.return_value = mock_process
|
|
280
|
-
|
|
281
|
-
with patch.object(plugin.logger, "info") as mock_info:
|
|
282
|
-
plugin.run_cli("echo", "test")
|
|
283
|
-
|
|
284
|
-
mock_popen.assert_called_once_with(
|
|
285
|
-
("echo", "test"), stdout=subprocess.PIPE, stderr=subprocess.STDOUT
|
|
286
|
-
)
|
|
287
|
-
mock_info.assert_called_with("success output")
|
|
288
|
-
|
|
289
|
-
def test_run_cli_failure(self) -> None:
|
|
290
|
-
"""Test CLI command execution failure."""
|
|
291
|
-
plugin = SpaceforgePlugin()
|
|
292
|
-
|
|
293
|
-
with patch("subprocess.Popen") as mock_popen:
|
|
294
|
-
mock_process = Mock()
|
|
295
|
-
mock_process.communicate.return_value = (None, b"error output\n")
|
|
296
|
-
mock_process.returncode = 1
|
|
297
|
-
mock_popen.return_value = mock_process
|
|
298
|
-
|
|
299
|
-
with patch.object(plugin.logger, "error") as mock_error:
|
|
300
|
-
plugin.run_cli("false")
|
|
301
|
-
|
|
302
|
-
mock_error.assert_any_call("Command failed with return code 1")
|
|
303
|
-
mock_error.assert_any_call("error output")
|
|
304
|
-
|
|
305
|
-
def test_run_cli_with_multiple_args(self) -> None:
|
|
306
|
-
"""Test CLI command with multiple arguments."""
|
|
307
|
-
plugin = SpaceforgePlugin()
|
|
308
|
-
|
|
309
|
-
with patch("subprocess.Popen") as mock_popen:
|
|
310
|
-
mock_process = Mock()
|
|
311
|
-
mock_process.communicate.return_value = (b"", None)
|
|
312
|
-
mock_process.returncode = 0
|
|
313
|
-
mock_popen.return_value = mock_process
|
|
314
|
-
|
|
315
|
-
with patch.object(plugin.logger, "debug") as mock_debug:
|
|
316
|
-
plugin.run_cli("git", "status", "--porcelain")
|
|
317
|
-
|
|
318
|
-
mock_popen.assert_called_once_with(
|
|
319
|
-
("git", "status", "--porcelain"),
|
|
320
|
-
stdout=subprocess.PIPE,
|
|
321
|
-
stderr=subprocess.STDOUT,
|
|
322
|
-
)
|
|
323
|
-
mock_debug.assert_called_with("Running CLI command: git status --porcelain")
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
class TestSpaceforgePluginAPI:
|
|
327
|
-
"""Test Spacelift API functionality."""
|
|
328
|
-
|
|
329
|
-
def test_query_api_disabled(self) -> None:
|
|
330
|
-
"""Test API query when API is disabled."""
|
|
331
|
-
plugin = SpaceforgePlugin()
|
|
332
|
-
plugin._api_enabled = False
|
|
333
|
-
|
|
334
|
-
with patch.object(plugin.logger, "error") as mock_error:
|
|
335
|
-
with pytest.raises(SystemExit):
|
|
336
|
-
plugin.query_api("query { test }")
|
|
337
|
-
|
|
338
|
-
mock_error.assert_called_with(
|
|
339
|
-
'API is not enabled, please export "SPACELIFT_API_TOKEN" and "SPACELIFT_DOMAIN".'
|
|
340
|
-
)
|
|
341
|
-
|
|
342
|
-
def test_query_api_success(self) -> None:
|
|
343
|
-
"""Test successful API query."""
|
|
344
|
-
plugin = SpaceforgePlugin()
|
|
345
|
-
plugin._api_enabled = True
|
|
346
|
-
plugin._api_token = "test_token"
|
|
347
|
-
plugin._spacelift_domain = "https://test.spacelift.io"
|
|
348
|
-
|
|
349
|
-
mock_response_data = {"data": {"test": "result"}}
|
|
350
|
-
mock_response = Mock()
|
|
351
|
-
mock_response.read.return_value = json.dumps(mock_response_data).encode("utf-8")
|
|
352
|
-
|
|
353
|
-
with patch("urllib.request.urlopen") as mock_urlopen:
|
|
354
|
-
with patch("urllib.request.Request") as mock_request:
|
|
355
|
-
mock_urlopen.return_value.__enter__ = Mock(return_value=mock_response)
|
|
356
|
-
mock_urlopen.return_value.__exit__ = Mock(return_value=None)
|
|
357
|
-
|
|
358
|
-
result = plugin.query_api("query { test }")
|
|
359
|
-
|
|
360
|
-
# Verify request was made correctly
|
|
361
|
-
mock_request.assert_called_once()
|
|
362
|
-
call_args = mock_request.call_args[0]
|
|
363
|
-
assert call_args[0] == "https://test.spacelift.io/graphql"
|
|
364
|
-
|
|
365
|
-
# Verify request data
|
|
366
|
-
request_data = json.loads(call_args[1].decode("utf-8"))
|
|
367
|
-
assert request_data["query"] == "query { test }"
|
|
368
|
-
|
|
369
|
-
# Verify headers - they are passed as the third argument to Request
|
|
370
|
-
headers = mock_request.call_args[0][2]
|
|
371
|
-
assert headers["Content-Type"] == "application/json"
|
|
372
|
-
assert headers["Authorization"] == "Bearer test_token"
|
|
373
|
-
|
|
374
|
-
assert result == mock_response_data
|
|
375
|
-
|
|
376
|
-
def test_query_api_with_variables(self) -> None:
|
|
377
|
-
"""Test API query with variables."""
|
|
378
|
-
plugin = SpaceforgePlugin()
|
|
379
|
-
plugin._api_enabled = True
|
|
380
|
-
plugin._api_token = "test_token"
|
|
381
|
-
plugin._spacelift_domain = "https://test.spacelift.io"
|
|
382
|
-
|
|
383
|
-
mock_response_data = {"data": {"test": "result"}}
|
|
384
|
-
mock_response = Mock()
|
|
385
|
-
mock_response.read.return_value = json.dumps(mock_response_data).encode("utf-8")
|
|
386
|
-
|
|
387
|
-
variables = {"stackId": "test-stack"}
|
|
388
|
-
|
|
389
|
-
with patch("urllib.request.urlopen") as mock_urlopen:
|
|
390
|
-
with patch("urllib.request.Request") as mock_request:
|
|
391
|
-
mock_urlopen.return_value.__enter__ = Mock(return_value=mock_response)
|
|
392
|
-
mock_urlopen.return_value.__exit__ = Mock(return_value=None)
|
|
393
|
-
|
|
394
|
-
plugin.query_api(
|
|
395
|
-
"query ($stackId: ID!) { stack(id: $stackId) { name } }", variables
|
|
396
|
-
)
|
|
397
|
-
|
|
398
|
-
# Verify request data includes variables
|
|
399
|
-
request_data = json.loads(mock_request.call_args[0][1].decode("utf-8"))
|
|
400
|
-
assert request_data["variables"] == variables
|
|
401
|
-
|
|
402
|
-
def test_query_api_with_errors(self) -> None:
|
|
403
|
-
"""Test API query that returns errors."""
|
|
404
|
-
plugin = SpaceforgePlugin()
|
|
405
|
-
plugin._api_enabled = True
|
|
406
|
-
plugin._api_token = "test_token"
|
|
407
|
-
plugin._spacelift_domain = "https://test.spacelift.io"
|
|
408
|
-
|
|
409
|
-
mock_response_data = {"errors": [{"message": "Test error"}]}
|
|
410
|
-
mock_response = Mock()
|
|
411
|
-
mock_response.read.return_value = json.dumps(mock_response_data).encode("utf-8")
|
|
412
|
-
|
|
413
|
-
with patch("urllib.request.urlopen") as mock_urlopen:
|
|
414
|
-
with patch.object(plugin.logger, "error") as mock_error:
|
|
415
|
-
mock_urlopen.return_value.__enter__ = Mock(return_value=mock_response)
|
|
416
|
-
mock_urlopen.return_value.__exit__ = Mock(return_value=None)
|
|
417
|
-
|
|
418
|
-
result = plugin.query_api("query { test }")
|
|
419
|
-
|
|
420
|
-
mock_error.assert_called_with("Error: [{'message': 'Test error'}]")
|
|
421
|
-
assert result == mock_response_data
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
class TestSpaceforgePluginFileOperations:
|
|
425
|
-
"""Test file operation methods."""
|
|
426
|
-
|
|
427
|
-
def setup_method(self) -> None:
|
|
428
|
-
"""Setup test fixtures."""
|
|
429
|
-
self.temp_dir = tempfile.mkdtemp()
|
|
430
|
-
self.plugin = SpaceforgePlugin()
|
|
431
|
-
self.plugin._workspace_root = self.temp_dir
|
|
432
|
-
|
|
433
|
-
def teardown_method(self) -> None:
|
|
434
|
-
"""Cleanup test fixtures."""
|
|
435
|
-
import shutil
|
|
436
|
-
|
|
437
|
-
shutil.rmtree(self.temp_dir, ignore_errors=True)
|
|
438
|
-
|
|
439
|
-
def test_get_plan_json_success(self) -> None:
|
|
440
|
-
"""Test successful plan JSON retrieval."""
|
|
441
|
-
plan_data = {"resource_changes": [{"type": "create"}]}
|
|
442
|
-
plan_path = os.path.join(self.temp_dir, "spacelift.plan.json")
|
|
443
|
-
|
|
444
|
-
with open(plan_path, "w") as f:
|
|
445
|
-
json.dump(plan_data, f)
|
|
446
|
-
|
|
447
|
-
result = self.plugin.get_plan_json()
|
|
448
|
-
assert result == plan_data
|
|
449
|
-
|
|
450
|
-
def test_get_plan_json_not_found(self) -> None:
|
|
451
|
-
"""Test plan JSON retrieval when file doesn't exist."""
|
|
452
|
-
with patch.object(self.plugin.logger, "error") as mock_error:
|
|
453
|
-
result = self.plugin.get_plan_json()
|
|
454
|
-
|
|
455
|
-
assert result is None
|
|
456
|
-
mock_error.assert_called_with("spacelift.plan.json does not exist.")
|
|
457
|
-
|
|
458
|
-
def test_get_plan_json_invalid_json(self) -> None:
|
|
459
|
-
"""Test plan JSON retrieval with invalid JSON."""
|
|
460
|
-
plan_path = os.path.join(self.temp_dir, "spacelift.plan.json")
|
|
461
|
-
|
|
462
|
-
with open(plan_path, "w") as f:
|
|
463
|
-
f.write("invalid json {")
|
|
464
|
-
|
|
465
|
-
with pytest.raises(json.JSONDecodeError):
|
|
466
|
-
self.plugin.get_plan_json()
|
|
467
|
-
|
|
468
|
-
def test_get_state_before_json_success(self) -> None:
|
|
469
|
-
"""Test successful state before JSON retrieval."""
|
|
470
|
-
state_data: Dict[str, Any] = {"values": {"root_module": {}}}
|
|
471
|
-
state_path = os.path.join(self.temp_dir, "spacelift.state.before.json")
|
|
472
|
-
|
|
473
|
-
with open(state_path, "w") as f:
|
|
474
|
-
json.dump(state_data, f)
|
|
475
|
-
|
|
476
|
-
result = self.plugin.get_state_before_json()
|
|
477
|
-
assert result == state_data
|
|
478
|
-
|
|
479
|
-
def test_get_state_before_json_not_found(self) -> None:
|
|
480
|
-
"""Test state before JSON retrieval when file doesn't exist."""
|
|
481
|
-
with patch.object(self.plugin.logger, "error") as mock_error:
|
|
482
|
-
result = self.plugin.get_state_before_json()
|
|
483
|
-
|
|
484
|
-
assert result is None
|
|
485
|
-
mock_error.assert_called_with("spacelift.state.before.json does not exist.")
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
class TestSpaceforgePluginInheritance:
|
|
489
|
-
"""Test plugin inheritance and custom implementations."""
|
|
490
|
-
|
|
491
|
-
def test_custom_plugin_inheritance(self) -> None:
|
|
492
|
-
"""Test creating a custom plugin that inherits from SpaceforgePlugin."""
|
|
493
|
-
|
|
494
|
-
class CustomPlugin(SpaceforgePlugin):
|
|
495
|
-
__plugin_name__ = "custom"
|
|
496
|
-
__version__ = "2.0.0"
|
|
497
|
-
__author__ = "Custom Author"
|
|
498
|
-
|
|
499
|
-
def __init__(self) -> None:
|
|
500
|
-
super().__init__()
|
|
501
|
-
self.custom_state = "initialized"
|
|
502
|
-
|
|
503
|
-
def after_plan(self) -> None:
|
|
504
|
-
self.custom_state = "plan_executed"
|
|
505
|
-
|
|
506
|
-
def custom_method(self) -> str:
|
|
507
|
-
return "custom_result"
|
|
508
|
-
|
|
509
|
-
plugin = CustomPlugin()
|
|
510
|
-
|
|
511
|
-
# Test inheritance
|
|
512
|
-
assert plugin.__plugin_name__ == "custom"
|
|
513
|
-
assert plugin.__version__ == "2.0.0"
|
|
514
|
-
assert plugin.__author__ == "Custom Author"
|
|
515
|
-
assert plugin.custom_state == "initialized"
|
|
516
|
-
|
|
517
|
-
# Test custom method
|
|
518
|
-
assert plugin.custom_method() == "custom_result"
|
|
519
|
-
|
|
520
|
-
# Test hook override
|
|
521
|
-
plugin.after_plan()
|
|
522
|
-
assert plugin.custom_state == "plan_executed"
|
|
523
|
-
|
|
524
|
-
# Test inherited functionality
|
|
525
|
-
assert hasattr(plugin, "logger")
|
|
526
|
-
assert hasattr(plugin, "run_cli")
|
|
527
|
-
|
|
528
|
-
def test_plugin_with_complex_initialization(self) -> None:
|
|
529
|
-
"""Test plugin with complex initialization logic."""
|
|
530
|
-
|
|
531
|
-
class ComplexPlugin(SpaceforgePlugin):
|
|
532
|
-
def __init__(self) -> None:
|
|
533
|
-
super().__init__()
|
|
534
|
-
self.config = self._load_config()
|
|
535
|
-
self.initialized = True
|
|
536
|
-
|
|
537
|
-
def _load_config(self) -> Dict[str, str]:
|
|
538
|
-
return {"setting1": "value1", "setting2": "value2"}
|
|
539
|
-
|
|
540
|
-
def after_plan(self) -> None:
|
|
541
|
-
# Store the config info instead of returning it
|
|
542
|
-
self.config_info = f"Config loaded: {self.config}"
|
|
543
|
-
|
|
544
|
-
plugin = ComplexPlugin()
|
|
545
|
-
|
|
546
|
-
assert plugin.initialized is True
|
|
547
|
-
assert plugin.config == {"setting1": "value1", "setting2": "value2"}
|
|
548
|
-
plugin.after_plan() # Call the method
|
|
549
|
-
assert hasattr(plugin, "config_info")
|
|
550
|
-
assert "Config loaded:" in plugin.config_info
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
class TestSpaceforgePluginEdgeCases:
|
|
554
|
-
"""Test edge cases and error conditions."""
|
|
555
|
-
|
|
556
|
-
def test_plugin_with_environment_variable_access(self) -> None:
|
|
557
|
-
"""Test plugin accessing environment variables."""
|
|
558
|
-
|
|
559
|
-
class EnvPlugin(SpaceforgePlugin):
|
|
560
|
-
def get_custom_env(self) -> str:
|
|
561
|
-
return os.environ.get("CUSTOM_ENV", "default_value")
|
|
562
|
-
|
|
563
|
-
plugin = EnvPlugin()
|
|
564
|
-
|
|
565
|
-
# Test with no environment variable
|
|
566
|
-
assert plugin.get_custom_env() == "default_value"
|
|
567
|
-
|
|
568
|
-
# Test with environment variable set
|
|
569
|
-
with patch.dict(os.environ, {"CUSTOM_ENV": "custom_value"}):
|
|
570
|
-
assert plugin.get_custom_env() == "custom_value"
|
|
571
|
-
|
|
572
|
-
def test_plugin_logger_multiple_instances(self) -> None:
|
|
573
|
-
"""Test that multiple plugin instances share the same logger by name."""
|
|
574
|
-
plugin1 = SpaceforgePlugin()
|
|
575
|
-
plugin2 = SpaceforgePlugin()
|
|
576
|
-
|
|
577
|
-
# Python loggers are singletons by name, so they should be the same instance
|
|
578
|
-
assert plugin1.logger is plugin2.logger
|
|
579
|
-
assert plugin1.logger.name == "spaceforge.SpaceforgePlugin"
|
|
580
|
-
assert plugin2.logger.name == "spaceforge.SpaceforgePlugin"
|
|
581
|
-
|
|
582
|
-
def test_plugin_api_url_construction(self) -> None:
|
|
583
|
-
"""Test API URL construction with various domain formats."""
|
|
584
|
-
test_cases = [
|
|
585
|
-
("https://example.spacelift.io", "https://example.spacelift.io/graphql"),
|
|
586
|
-
("https://example.spacelift.io/", "https://example.spacelift.io/graphql"),
|
|
587
|
-
]
|
|
588
|
-
|
|
589
|
-
for domain, expected_url in test_cases:
|
|
590
|
-
plugin = SpaceforgePlugin()
|
|
591
|
-
plugin._api_enabled = True
|
|
592
|
-
plugin._api_token = "test_token"
|
|
593
|
-
plugin._spacelift_domain = domain.rstrip("/") # Plugin normalizes this
|
|
594
|
-
|
|
595
|
-
with patch("urllib.request.urlopen") as mock_urlopen:
|
|
596
|
-
with patch("urllib.request.Request") as mock_request:
|
|
597
|
-
mock_response = Mock()
|
|
598
|
-
mock_response.read.return_value = b'{"data": {}}'
|
|
599
|
-
mock_urlopen.return_value.__enter__ = Mock(
|
|
600
|
-
return_value=mock_response
|
|
601
|
-
)
|
|
602
|
-
mock_urlopen.return_value.__exit__ = Mock(return_value=None)
|
|
603
|
-
|
|
604
|
-
plugin.query_api("query { test }")
|
|
605
|
-
|
|
606
|
-
# Check that the correct URL was constructed
|
|
607
|
-
called_url = mock_request.call_args[0][0]
|
|
608
|
-
assert called_url == expected_url
|
|
609
|
-
|
|
610
|
-
def test_plugin_workspace_root_handling(self) -> None:
|
|
611
|
-
"""Test workspace root path handling."""
|
|
612
|
-
# Test default workspace root
|
|
613
|
-
with patch.dict(os.environ, {}, clear=True):
|
|
614
|
-
plugin = SpaceforgePlugin()
|
|
615
|
-
assert plugin._workspace_root == os.getcwd()
|
|
616
|
-
|
|
617
|
-
# Test custom workspace root
|
|
618
|
-
custom_root = "/custom/workspace"
|
|
619
|
-
with patch.dict(os.environ, {"WORKSPACE_ROOT": custom_root}, clear=True):
|
|
620
|
-
plugin = SpaceforgePlugin()
|
|
621
|
-
assert plugin._workspace_root == custom_root
|