spaceforge 0.0.2__py3-none-any.whl → 0.0.3__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 +2 -2
- spaceforge/cls.py +8 -4
- spaceforge/conftest.py +89 -0
- spaceforge/generator.py +92 -40
- spaceforge/plugin.py +105 -12
- spaceforge/runner.py +0 -12
- spaceforge/schema.json +23 -5
- spaceforge/{generator_test.py → test_generator.py} +121 -10
- spaceforge/test_generator_binaries.py +167 -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} +2 -65
- spaceforge/test_runner_cli.py +69 -0
- spaceforge/test_runner_core.py +124 -0
- spaceforge/test_runner_execution.py +169 -0
- spaceforge-0.0.3.dist-info/METADATA +595 -0
- spaceforge-0.0.3.dist-info/RECORD +31 -0
- spaceforge/plugin_test.py +0 -621
- spaceforge-0.0.2.dist-info/METADATA +0 -163
- spaceforge-0.0.2.dist-info/RECORD +0 -20
- /spaceforge/{cls_test.py → test_cls.py} +0 -0
- {spaceforge-0.0.2.dist-info → spaceforge-0.0.3.dist-info}/WHEEL +0 -0
- {spaceforge-0.0.2.dist-info → spaceforge-0.0.3.dist-info}/entry_points.txt +0 -0
- {spaceforge-0.0.2.dist-info → spaceforge-0.0.3.dist-info}/licenses/LICENSE +0 -0
- {spaceforge-0.0.2.dist-info → spaceforge-0.0.3.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import tempfile
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Dict, List
|
|
4
4
|
from unittest.mock import Mock, mock_open, patch
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
@@ -318,8 +318,15 @@ class NotAPlugin:
|
|
|
318
318
|
"""Test binary installation command generation."""
|
|
319
319
|
generator = PluginGenerator()
|
|
320
320
|
generator.plugin_class = PluginExample
|
|
321
|
+
generator.plugin_working_directory = "/mnt/workspace/plugins/test_plugin"
|
|
322
|
+
generator.config = {
|
|
323
|
+
"setup_virtual_env": "cd /mnt/workspace/plugins/test_plugin && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
|
|
324
|
+
"plugin_mounted_path": "/mnt/workspace/plugins/test_plugin/plugin.py",
|
|
325
|
+
}
|
|
321
326
|
|
|
322
|
-
|
|
327
|
+
hooks: Dict[str, List[str]] = {"before_init": []}
|
|
328
|
+
generator._generate_binary_install_command(hooks)
|
|
329
|
+
command = hooks["before_init"][-1]
|
|
323
330
|
|
|
324
331
|
assert "mkdir -p /mnt/workspace/plugins/plugin_binaries" in command
|
|
325
332
|
assert "curl https://example.com/test-cli-amd64" in command
|
|
@@ -335,9 +342,16 @@ class NotAPlugin:
|
|
|
335
342
|
|
|
336
343
|
generator = PluginGenerator()
|
|
337
344
|
generator.plugin_class = NoBinariesPlugin
|
|
345
|
+
generator.plugin_working_directory = "/mnt/workspace/plugins/nobinaries"
|
|
346
|
+
generator.config = {
|
|
347
|
+
"setup_virtual_env": "cd /mnt/workspace/plugins/nobinaries && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
|
|
348
|
+
"plugin_mounted_path": "/mnt/workspace/plugins/nobinaries/plugin.py",
|
|
349
|
+
}
|
|
338
350
|
|
|
339
|
-
|
|
340
|
-
|
|
351
|
+
hooks: Dict[str, List[str]] = {"before_init": []}
|
|
352
|
+
generator._generate_binary_install_command(hooks)
|
|
353
|
+
# No binaries should mean no new commands added
|
|
354
|
+
assert len(hooks["before_init"]) == 0
|
|
341
355
|
|
|
342
356
|
def test_generate_binary_install_command_missing_urls(self) -> None:
|
|
343
357
|
"""Test binary command generation with missing URLs."""
|
|
@@ -347,9 +361,15 @@ class NotAPlugin:
|
|
|
347
361
|
|
|
348
362
|
generator = PluginGenerator()
|
|
349
363
|
generator.plugin_class = InvalidBinaryPlugin
|
|
364
|
+
generator.plugin_working_directory = "/mnt/workspace/plugins/invalidbinary"
|
|
365
|
+
generator.config = {
|
|
366
|
+
"setup_virtual_env": "cd /mnt/workspace/plugins/invalidbinary && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
|
|
367
|
+
"plugin_mounted_path": "/mnt/workspace/plugins/invalidbinary/plugin.py",
|
|
368
|
+
}
|
|
350
369
|
|
|
370
|
+
hooks: Dict[str, List[str]] = {"before_init": []}
|
|
351
371
|
with pytest.raises(ValueError, match="must have at least one download URL"):
|
|
352
|
-
generator.
|
|
372
|
+
generator._generate_binary_install_command(hooks)
|
|
353
373
|
|
|
354
374
|
def test_get_plugin_policies(self) -> None:
|
|
355
375
|
"""Test policy extraction."""
|
|
@@ -389,6 +409,10 @@ class NotAPlugin:
|
|
|
389
409
|
generator = PluginGenerator(self.test_plugin_path)
|
|
390
410
|
generator.plugin_class = PluginExample
|
|
391
411
|
generator.plugin_working_directory = "/mnt/workspace/plugins/test_plugin"
|
|
412
|
+
generator.config = {
|
|
413
|
+
"setup_virtual_env": "cd /mnt/workspace/plugins/test_plugin && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
|
|
414
|
+
"plugin_mounted_path": "/mnt/workspace/plugins/test_plugin/plugin.py",
|
|
415
|
+
}
|
|
392
416
|
|
|
393
417
|
contexts = generator.get_plugin_contexts()
|
|
394
418
|
|
|
@@ -426,6 +450,10 @@ class NotAPlugin:
|
|
|
426
450
|
generator = PluginGenerator(self.test_plugin_path)
|
|
427
451
|
generator.plugin_class = PluginExample
|
|
428
452
|
generator.plugin_working_directory = "/mnt/workspace/plugins/test_plugin"
|
|
453
|
+
generator.config = {
|
|
454
|
+
"setup_virtual_env": "cd /mnt/workspace/plugins/test_plugin && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
|
|
455
|
+
"plugin_mounted_path": "/mnt/workspace/plugins/test_plugin/plugin.py",
|
|
456
|
+
}
|
|
429
457
|
|
|
430
458
|
with patch.object(
|
|
431
459
|
generator, "get_available_hooks", return_value=["after_plan"]
|
|
@@ -447,16 +475,21 @@ class NotAPlugin:
|
|
|
447
475
|
# Should have spacepy runner hooks
|
|
448
476
|
assert context.hooks is not None
|
|
449
477
|
assert "after_plan" in context.hooks
|
|
450
|
-
runner_command = context.hooks["after_plan"][
|
|
478
|
+
runner_command = context.hooks["after_plan"][1]
|
|
451
479
|
assert "python -m spaceforge runner" in runner_command
|
|
452
480
|
assert "after_plan" in runner_command
|
|
453
481
|
|
|
454
482
|
def test_generate_manifest(self) -> None:
|
|
455
483
|
"""Test complete manifest generation."""
|
|
456
484
|
generator = PluginGenerator(self.test_plugin_path)
|
|
485
|
+
generator.plugin_class = PluginExample
|
|
486
|
+
generator.plugin_working_directory = "/mnt/workspace/plugins/test_plugin"
|
|
487
|
+
generator.config = {
|
|
488
|
+
"setup_virtual_env": "cd /mnt/workspace/plugins/test_plugin && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
|
|
489
|
+
"plugin_mounted_path": "/mnt/workspace/plugins/test_plugin/plugin.py",
|
|
490
|
+
}
|
|
457
491
|
|
|
458
492
|
with patch.object(generator, "load_plugin"):
|
|
459
|
-
generator.plugin_class = PluginExample
|
|
460
493
|
manifest = generator.generate_manifest()
|
|
461
494
|
|
|
462
495
|
assert isinstance(manifest, PluginManifest)
|
|
@@ -474,7 +507,7 @@ class NotAPlugin:
|
|
|
474
507
|
"""Test YAML writing functionality."""
|
|
475
508
|
generator = PluginGenerator(output_path=self.test_output_path)
|
|
476
509
|
manifest = PluginManifest(
|
|
477
|
-
|
|
510
|
+
name="test",
|
|
478
511
|
version="1.0.0",
|
|
479
512
|
description="Test",
|
|
480
513
|
author="Test Author",
|
|
@@ -500,7 +533,7 @@ class NotAPlugin:
|
|
|
500
533
|
"""Test complete generate method."""
|
|
501
534
|
generator = PluginGenerator()
|
|
502
535
|
mock_manifest = PluginManifest(
|
|
503
|
-
|
|
536
|
+
name="test", version="1.0.0", description="Test", author="Test"
|
|
504
537
|
)
|
|
505
538
|
mock_generate_manifest.return_value = mock_manifest
|
|
506
539
|
|
|
@@ -606,8 +639,15 @@ class TestPluginGeneratorEdgeCases:
|
|
|
606
639
|
|
|
607
640
|
generator = PluginGenerator()
|
|
608
641
|
generator.plugin_class = SingleArchPlugin
|
|
642
|
+
generator.plugin_working_directory = "/mnt/workspace/plugins/singlearch"
|
|
643
|
+
generator.config = {
|
|
644
|
+
"setup_virtual_env": "cd /mnt/workspace/plugins/singlearch && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
|
|
645
|
+
"plugin_mounted_path": "/mnt/workspace/plugins/singlearch/plugin.py",
|
|
646
|
+
}
|
|
609
647
|
|
|
610
|
-
|
|
648
|
+
hooks: Dict[str, List[str]] = {"before_init": []}
|
|
649
|
+
generator._generate_binary_install_command(hooks)
|
|
650
|
+
command = hooks["before_init"][-1]
|
|
611
651
|
|
|
612
652
|
assert "https://example.com/binary-amd64" in command
|
|
613
653
|
assert "arm64 binary not available" in command
|
|
@@ -638,6 +678,10 @@ class TestPluginGeneratorEdgeCases:
|
|
|
638
678
|
generator = PluginGenerator("/fake/path")
|
|
639
679
|
generator.plugin_class = ExistingHooksPlugin
|
|
640
680
|
generator.plugin_working_directory = "/mnt/workspace/plugins/existing_hooks"
|
|
681
|
+
generator.config = {
|
|
682
|
+
"setup_virtual_env": "cd /mnt/workspace/plugins/existing_hooks && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
|
|
683
|
+
"plugin_mounted_path": "/mnt/workspace/plugins/existing_hooks/plugin.py",
|
|
684
|
+
}
|
|
641
685
|
|
|
642
686
|
with patch("os.path.exists") as mock_exists:
|
|
643
687
|
# Only plugin file exists, not requirements.txt
|
|
@@ -669,3 +713,70 @@ class TestPluginGeneratorEdgeCases:
|
|
|
669
713
|
assert context.env is not None
|
|
670
714
|
existing_vars = [var for var in context.env if var.key == "EXISTING"]
|
|
671
715
|
assert len(existing_vars) == 1
|
|
716
|
+
|
|
717
|
+
def test_parameters_and_variables_id_generation(self) -> None:
|
|
718
|
+
class ParametersAndVariablesPlugin(SpaceforgePlugin):
|
|
719
|
+
__plugin_name__ = "parameters_and_variables"
|
|
720
|
+
|
|
721
|
+
__parameters__ = [
|
|
722
|
+
Parameter(
|
|
723
|
+
name="api key",
|
|
724
|
+
description="API key for authentication",
|
|
725
|
+
required=True,
|
|
726
|
+
sensitive=True,
|
|
727
|
+
),
|
|
728
|
+
Parameter(
|
|
729
|
+
name="endpoint",
|
|
730
|
+
description="API endpoint URL",
|
|
731
|
+
required=False,
|
|
732
|
+
default="https://api.example.com",
|
|
733
|
+
),
|
|
734
|
+
]
|
|
735
|
+
|
|
736
|
+
__contexts__ = [
|
|
737
|
+
Context(
|
|
738
|
+
name_prefix="existing",
|
|
739
|
+
description="Existing context",
|
|
740
|
+
hooks={"before_init": ["echo 'existing hook'"]},
|
|
741
|
+
mounted_files=[
|
|
742
|
+
MountedFile(
|
|
743
|
+
path="/existing", content="existing", sensitive=False
|
|
744
|
+
)
|
|
745
|
+
],
|
|
746
|
+
env=[
|
|
747
|
+
Variable(
|
|
748
|
+
key="API_KEY",
|
|
749
|
+
value_from_parameter="api key",
|
|
750
|
+
sensitive=True,
|
|
751
|
+
),
|
|
752
|
+
Variable(key="ENDPOINT", value_from_parameter="endpoint"),
|
|
753
|
+
],
|
|
754
|
+
)
|
|
755
|
+
]
|
|
756
|
+
|
|
757
|
+
def after_plan(self) -> None:
|
|
758
|
+
pass
|
|
759
|
+
|
|
760
|
+
generator = PluginGenerator("/fake/path")
|
|
761
|
+
generator.plugin_class = ParametersAndVariablesPlugin
|
|
762
|
+
generator.plugin_working_directory = (
|
|
763
|
+
"/mnt/workspace/plugins/parametersandvariables"
|
|
764
|
+
)
|
|
765
|
+
generator.config = {
|
|
766
|
+
"setup_virtual_env": "cd /mnt/workspace/plugins/parametersandvariables && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
|
|
767
|
+
"plugin_mounted_path": "/mnt/workspace/plugins/parametersandvariables/plugin.py",
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
with patch("os.path.exists") as mock_exists:
|
|
771
|
+
# Only plugin file exists, not requirements.txt
|
|
772
|
+
mock_exists.side_effect = lambda path: path == "/fake/path"
|
|
773
|
+
with patch("builtins.open", mock_open(read_data="fake content")):
|
|
774
|
+
contexts = generator.get_plugin_contexts()
|
|
775
|
+
parameters = generator.get_plugin_parameters()
|
|
776
|
+
|
|
777
|
+
assert contexts[0].env is not None
|
|
778
|
+
assert parameters is not None
|
|
779
|
+
assert parameters[0].id is not None
|
|
780
|
+
assert parameters[1].id is not None
|
|
781
|
+
assert parameters[0].id == contexts[0].env[0].value_from_parameter
|
|
782
|
+
assert parameters[1].id == contexts[0].env[1].value_from_parameter
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Tests for PluginGenerator binary handling."""
|
|
2
|
+
|
|
3
|
+
from typing import Dict, List
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from spaceforge.cls import Binary
|
|
8
|
+
from spaceforge.generator import PluginGenerator
|
|
9
|
+
from spaceforge.plugin import SpaceforgePlugin
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestPluginGeneratorBinaries:
|
|
13
|
+
"""Test binary extraction and installation command generation."""
|
|
14
|
+
|
|
15
|
+
def test_should_extract_binaries_when_defined(self) -> None:
|
|
16
|
+
"""Should extract and return binary list when plugin defines them."""
|
|
17
|
+
|
|
18
|
+
# Arrange
|
|
19
|
+
class BinaryPlugin(SpaceforgePlugin):
|
|
20
|
+
__binaries__ = [
|
|
21
|
+
Binary(
|
|
22
|
+
name="test-cli",
|
|
23
|
+
download_urls={
|
|
24
|
+
"amd64": "https://example.com/test-cli-amd64",
|
|
25
|
+
"arm64": "https://example.com/test-cli-arm64",
|
|
26
|
+
},
|
|
27
|
+
)
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
generator = PluginGenerator()
|
|
31
|
+
generator.plugin_class = BinaryPlugin
|
|
32
|
+
|
|
33
|
+
# Act
|
|
34
|
+
binaries = generator.get_plugin_binaries()
|
|
35
|
+
|
|
36
|
+
# Assert
|
|
37
|
+
assert binaries is not None
|
|
38
|
+
assert len(binaries) == 1
|
|
39
|
+
assert binaries[0].name == "test-cli"
|
|
40
|
+
assert "amd64" in binaries[0].download_urls
|
|
41
|
+
assert "arm64" in binaries[0].download_urls
|
|
42
|
+
|
|
43
|
+
def test_should_return_none_when_no_binaries_defined(self) -> None:
|
|
44
|
+
"""Should return None when plugin has no binaries."""
|
|
45
|
+
|
|
46
|
+
# Arrange
|
|
47
|
+
class NoBinariesPlugin(SpaceforgePlugin):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
generator = PluginGenerator()
|
|
51
|
+
generator.plugin_class = NoBinariesPlugin
|
|
52
|
+
|
|
53
|
+
# Act
|
|
54
|
+
binaries = generator.get_plugin_binaries()
|
|
55
|
+
|
|
56
|
+
# Assert
|
|
57
|
+
assert binaries is None
|
|
58
|
+
|
|
59
|
+
def test_should_generate_binary_install_command_for_multi_arch(self) -> None:
|
|
60
|
+
"""Should generate installation commands for multiple architectures."""
|
|
61
|
+
|
|
62
|
+
# Arrange
|
|
63
|
+
class MultiArchPlugin(SpaceforgePlugin):
|
|
64
|
+
__binaries__ = [
|
|
65
|
+
Binary(
|
|
66
|
+
name="multi-cli",
|
|
67
|
+
download_urls={
|
|
68
|
+
"amd64": "https://example.com/multi-cli-amd64",
|
|
69
|
+
"arm64": "https://example.com/multi-cli-arm64",
|
|
70
|
+
},
|
|
71
|
+
)
|
|
72
|
+
]
|
|
73
|
+
|
|
74
|
+
generator = PluginGenerator()
|
|
75
|
+
generator.plugin_class = MultiArchPlugin
|
|
76
|
+
generator.plugin_working_directory = "/mnt/workspace/plugins/multiarch"
|
|
77
|
+
generator.config = {
|
|
78
|
+
"setup_virtual_env": "cd /mnt/workspace/plugins/multiarch && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
|
|
79
|
+
"plugin_mounted_path": "/mnt/workspace/plugins/multiarch/plugin.py",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
hooks: Dict[str, List[str]] = {"before_init": []}
|
|
83
|
+
|
|
84
|
+
# Act
|
|
85
|
+
generator._generate_binary_install_command(hooks)
|
|
86
|
+
command = hooks["before_init"][-1]
|
|
87
|
+
|
|
88
|
+
# Assert
|
|
89
|
+
assert "mkdir -p /mnt/workspace/plugins/plugin_binaries" in command
|
|
90
|
+
assert "curl https://example.com/multi-cli-amd64" in command
|
|
91
|
+
assert "curl https://example.com/multi-cli-arm64" in command
|
|
92
|
+
assert "arch" in command
|
|
93
|
+
assert "x86_64" in command
|
|
94
|
+
|
|
95
|
+
def test_should_not_add_commands_when_no_binaries(self) -> None:
|
|
96
|
+
"""Should not add installation commands when plugin has no binaries."""
|
|
97
|
+
|
|
98
|
+
# Arrange
|
|
99
|
+
class NoBinariesPlugin(SpaceforgePlugin):
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
generator = PluginGenerator()
|
|
103
|
+
generator.plugin_class = NoBinariesPlugin
|
|
104
|
+
generator.plugin_working_directory = "/mnt/workspace/plugins/nobinaries"
|
|
105
|
+
generator.config = {
|
|
106
|
+
"setup_virtual_env": "cd /mnt/workspace/plugins/nobinaries && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
|
|
107
|
+
"plugin_mounted_path": "/mnt/workspace/plugins/nobinaries/plugin.py",
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
hooks: Dict[str, List[str]] = {"before_init": []}
|
|
111
|
+
|
|
112
|
+
# Act
|
|
113
|
+
generator._generate_binary_install_command(hooks)
|
|
114
|
+
|
|
115
|
+
# Assert
|
|
116
|
+
assert len(hooks["before_init"]) == 0
|
|
117
|
+
|
|
118
|
+
def test_should_raise_error_when_binary_has_no_download_urls(self) -> None:
|
|
119
|
+
"""Should raise ValueError when binary has empty download URLs."""
|
|
120
|
+
|
|
121
|
+
# Arrange
|
|
122
|
+
class InvalidBinaryPlugin(SpaceforgePlugin):
|
|
123
|
+
__binaries__ = [Binary(name="invalid", download_urls={})]
|
|
124
|
+
|
|
125
|
+
generator = PluginGenerator()
|
|
126
|
+
generator.plugin_class = InvalidBinaryPlugin
|
|
127
|
+
generator.plugin_working_directory = "/mnt/workspace/plugins/invalidbinary"
|
|
128
|
+
generator.config = {
|
|
129
|
+
"setup_virtual_env": "cd /mnt/workspace/plugins/invalidbinary && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
|
|
130
|
+
"plugin_mounted_path": "/mnt/workspace/plugins/invalidbinary/plugin.py",
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
hooks: Dict[str, List[str]] = {"before_init": []}
|
|
134
|
+
|
|
135
|
+
# Act & Assert
|
|
136
|
+
with pytest.raises(ValueError, match="must have at least one download URL"):
|
|
137
|
+
generator._generate_binary_install_command(hooks)
|
|
138
|
+
|
|
139
|
+
def test_should_handle_single_architecture_binary(self) -> None:
|
|
140
|
+
"""Should generate appropriate commands for single architecture binary."""
|
|
141
|
+
|
|
142
|
+
# Arrange
|
|
143
|
+
class SingleArchPlugin(SpaceforgePlugin):
|
|
144
|
+
__binaries__ = [
|
|
145
|
+
Binary(
|
|
146
|
+
name="single-arch",
|
|
147
|
+
download_urls={"amd64": "https://example.com/binary-amd64"},
|
|
148
|
+
)
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
generator = PluginGenerator()
|
|
152
|
+
generator.plugin_class = SingleArchPlugin
|
|
153
|
+
generator.plugin_working_directory = "/mnt/workspace/plugins/singlearch"
|
|
154
|
+
generator.config = {
|
|
155
|
+
"setup_virtual_env": "cd /mnt/workspace/plugins/singlearch && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
|
|
156
|
+
"plugin_mounted_path": "/mnt/workspace/plugins/singlearch/plugin.py",
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
hooks: Dict[str, List[str]] = {"before_init": []}
|
|
160
|
+
|
|
161
|
+
# Act
|
|
162
|
+
generator._generate_binary_install_command(hooks)
|
|
163
|
+
command = hooks["before_init"][-1]
|
|
164
|
+
|
|
165
|
+
# Assert
|
|
166
|
+
assert "https://example.com/binary-amd64" in command
|
|
167
|
+
assert "arm64 binary not available" in command
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
"""Tests for PluginGenerator core functionality."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from unittest.mock import Mock, patch
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from spaceforge.generator import PluginGenerator
|
|
9
|
+
from spaceforge.plugin import SpaceforgePlugin
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TestPluginGeneratorInitialization:
|
|
13
|
+
"""Test PluginGenerator initialization and configuration."""
|
|
14
|
+
|
|
15
|
+
def test_should_initialize_with_custom_paths(self) -> None:
|
|
16
|
+
"""Should accept and store custom plugin and output paths."""
|
|
17
|
+
# Arrange & Act
|
|
18
|
+
generator = PluginGenerator("custom_plugin.py", "custom_output.yaml")
|
|
19
|
+
|
|
20
|
+
# Assert
|
|
21
|
+
assert generator.plugin_path == "custom_plugin.py"
|
|
22
|
+
assert generator.output_path == "custom_output.yaml"
|
|
23
|
+
assert generator.plugin_class is None
|
|
24
|
+
assert generator.plugin_instance is None
|
|
25
|
+
assert generator.plugin_working_directory is None
|
|
26
|
+
|
|
27
|
+
def test_should_use_defaults_when_no_paths_provided(self) -> None:
|
|
28
|
+
"""Should use default paths when none specified."""
|
|
29
|
+
# Arrange & Act
|
|
30
|
+
generator = PluginGenerator()
|
|
31
|
+
|
|
32
|
+
# Assert
|
|
33
|
+
assert generator.plugin_path == "plugin.py"
|
|
34
|
+
assert generator.output_path == "plugin.yaml"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class TestPluginGeneratorLoading:
|
|
38
|
+
"""Test plugin file loading functionality."""
|
|
39
|
+
|
|
40
|
+
def test_should_raise_file_not_found_when_plugin_file_missing(self) -> None:
|
|
41
|
+
"""Should raise FileNotFoundError when plugin file doesn't exist."""
|
|
42
|
+
# Arrange
|
|
43
|
+
generator = PluginGenerator("nonexistent.py")
|
|
44
|
+
|
|
45
|
+
# Act & Assert
|
|
46
|
+
with pytest.raises(FileNotFoundError, match="Plugin file not found"):
|
|
47
|
+
generator.load_plugin()
|
|
48
|
+
|
|
49
|
+
def test_should_raise_exception_when_plugin_file_has_syntax_errors(
|
|
50
|
+
self, temp_dir: str
|
|
51
|
+
) -> None:
|
|
52
|
+
"""Should raise exception when plugin file has invalid Python syntax."""
|
|
53
|
+
# Arrange
|
|
54
|
+
invalid_path = os.path.join(temp_dir, "invalid.py")
|
|
55
|
+
with open(invalid_path, "w") as f:
|
|
56
|
+
f.write("invalid python syntax }")
|
|
57
|
+
|
|
58
|
+
generator = PluginGenerator(invalid_path)
|
|
59
|
+
|
|
60
|
+
# Act & Assert
|
|
61
|
+
with pytest.raises(Exception): # Could be syntax error or import error
|
|
62
|
+
generator.load_plugin()
|
|
63
|
+
|
|
64
|
+
def test_should_raise_value_error_when_no_spaceforge_plugin_found(
|
|
65
|
+
self, temp_dir: str
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Should raise ValueError when file has no SpaceforgePlugin subclass."""
|
|
68
|
+
# Arrange
|
|
69
|
+
no_plugin_path = os.path.join(temp_dir, "no_plugin.py")
|
|
70
|
+
with open(no_plugin_path, "w") as f:
|
|
71
|
+
f.write(
|
|
72
|
+
"""
|
|
73
|
+
class NotAPlugin:
|
|
74
|
+
pass
|
|
75
|
+
"""
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
generator = PluginGenerator(no_plugin_path)
|
|
79
|
+
|
|
80
|
+
# Act & Assert
|
|
81
|
+
with pytest.raises(ValueError, match="No SpaceforgePlugin subclass found"):
|
|
82
|
+
generator.load_plugin()
|
|
83
|
+
|
|
84
|
+
def test_should_load_plugin_successfully_when_valid_file_provided(
|
|
85
|
+
self, test_plugin_file: str
|
|
86
|
+
) -> None:
|
|
87
|
+
"""Should successfully load plugin from valid file."""
|
|
88
|
+
# Arrange
|
|
89
|
+
generator = PluginGenerator(test_plugin_file)
|
|
90
|
+
|
|
91
|
+
# Act
|
|
92
|
+
generator.load_plugin()
|
|
93
|
+
|
|
94
|
+
# Assert
|
|
95
|
+
assert generator.plugin_class is not None
|
|
96
|
+
assert generator.plugin_instance is not None
|
|
97
|
+
assert generator.plugin_class.__name__ == "TestPlugin"
|
|
98
|
+
assert generator.plugin_working_directory == "/mnt/workspace/plugins/test"
|
|
99
|
+
|
|
100
|
+
@patch("spaceforge.generator.importlib.util.spec_from_file_location")
|
|
101
|
+
def test_should_raise_import_error_when_spec_is_none(
|
|
102
|
+
self, mock_spec: Mock, test_plugin_file: str
|
|
103
|
+
) -> None:
|
|
104
|
+
"""Should raise ImportError when importlib spec creation fails."""
|
|
105
|
+
# Arrange
|
|
106
|
+
mock_spec.return_value = None
|
|
107
|
+
generator = PluginGenerator(test_plugin_file)
|
|
108
|
+
|
|
109
|
+
# Act & Assert
|
|
110
|
+
with pytest.raises(ImportError, match="Could not load plugin"):
|
|
111
|
+
generator.load_plugin()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class TestPluginGeneratorMetadata:
|
|
115
|
+
"""Test metadata extraction functionality."""
|
|
116
|
+
|
|
117
|
+
def test_should_extract_complete_metadata_when_all_attributes_present(self) -> None:
|
|
118
|
+
"""Should extract all metadata when plugin has complete attributes."""
|
|
119
|
+
|
|
120
|
+
# Arrange
|
|
121
|
+
class CompletePlugin(SpaceforgePlugin):
|
|
122
|
+
"""Complete test plugin."""
|
|
123
|
+
|
|
124
|
+
__plugin_name__ = "complete_test"
|
|
125
|
+
__version__ = "2.0.0"
|
|
126
|
+
__author__ = "Test Author"
|
|
127
|
+
|
|
128
|
+
generator = PluginGenerator()
|
|
129
|
+
generator.plugin_class = CompletePlugin
|
|
130
|
+
|
|
131
|
+
# Act
|
|
132
|
+
metadata = generator.get_plugin_metadata()
|
|
133
|
+
|
|
134
|
+
# Assert
|
|
135
|
+
assert metadata["name_prefix"] == "complete_test"
|
|
136
|
+
assert metadata["version"] == "2.0.0"
|
|
137
|
+
assert metadata["author"] == "Test Author"
|
|
138
|
+
assert metadata["description"] == "Complete test plugin."
|
|
139
|
+
|
|
140
|
+
def test_should_use_defaults_when_metadata_attributes_missing(self) -> None:
|
|
141
|
+
"""Should use default values when plugin metadata attributes are missing."""
|
|
142
|
+
|
|
143
|
+
# Arrange
|
|
144
|
+
class MinimalPlugin(SpaceforgePlugin):
|
|
145
|
+
pass
|
|
146
|
+
|
|
147
|
+
generator = PluginGenerator()
|
|
148
|
+
generator.plugin_class = MinimalPlugin
|
|
149
|
+
|
|
150
|
+
# Act
|
|
151
|
+
metadata = generator.get_plugin_metadata()
|
|
152
|
+
|
|
153
|
+
# Assert
|
|
154
|
+
assert metadata["name_prefix"] == "SpaceforgePlugin" # inherited
|
|
155
|
+
assert metadata["version"] == "1.0.0" # inherited from base
|
|
156
|
+
assert metadata["author"] == "Spacelift Team" # inherited from base
|
|
157
|
+
assert "MinimalPlugin" in metadata["description"]
|
|
158
|
+
|
|
159
|
+
def test_should_generate_name_from_class_name_when_plugin_name_missing(
|
|
160
|
+
self,
|
|
161
|
+
) -> None:
|
|
162
|
+
"""Should derive name from class name when __plugin_name__ not set."""
|
|
163
|
+
|
|
164
|
+
# Arrange
|
|
165
|
+
class MinimalPlugin: # Don't inherit from SpaceforgePlugin
|
|
166
|
+
__name__ = "MinimalPlugin"
|
|
167
|
+
|
|
168
|
+
generator = PluginGenerator()
|
|
169
|
+
generator.plugin_class = MinimalPlugin # type: ignore[assignment]
|
|
170
|
+
|
|
171
|
+
# Act
|
|
172
|
+
metadata = generator.get_plugin_metadata()
|
|
173
|
+
|
|
174
|
+
# Assert
|
|
175
|
+
assert (
|
|
176
|
+
metadata["name_prefix"] == "minimal"
|
|
177
|
+
) # class name lowercased with 'plugin' removed
|
|
178
|
+
assert metadata["version"] == "1.0.0" # default
|
|
179
|
+
assert metadata["author"] == "Unknown" # default
|
|
180
|
+
assert "MinimalPlugin" in metadata["description"]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""Tests for PluginGenerator hook detection."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from pytest import MonkeyPatch
|
|
6
|
+
|
|
7
|
+
from spaceforge.generator import PluginGenerator
|
|
8
|
+
from spaceforge.plugin import SpaceforgePlugin
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TestPluginGeneratorHooks:
|
|
12
|
+
"""Test hook method detection functionality."""
|
|
13
|
+
|
|
14
|
+
def test_should_detect_overridden_hook_methods(self) -> None:
|
|
15
|
+
"""Should identify hook methods that have been overridden in plugin."""
|
|
16
|
+
|
|
17
|
+
# Arrange
|
|
18
|
+
class HookedPlugin(SpaceforgePlugin):
|
|
19
|
+
def after_plan(self) -> None:
|
|
20
|
+
pass
|
|
21
|
+
|
|
22
|
+
def before_apply(self) -> None:
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
generator = PluginGenerator()
|
|
26
|
+
generator.plugin_class = HookedPlugin
|
|
27
|
+
|
|
28
|
+
# Act
|
|
29
|
+
hooks = generator.get_available_hooks()
|
|
30
|
+
|
|
31
|
+
# Assert
|
|
32
|
+
assert "after_plan" in hooks
|
|
33
|
+
assert "before_apply" in hooks
|
|
34
|
+
assert len(hooks) == 2
|
|
35
|
+
|
|
36
|
+
def test_should_return_empty_list_when_no_hooks_overridden(self) -> None:
|
|
37
|
+
"""Should return empty list when plugin has no overridden hook methods."""
|
|
38
|
+
|
|
39
|
+
# Arrange
|
|
40
|
+
class NoHooksPlugin(SpaceforgePlugin):
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
generator = PluginGenerator()
|
|
44
|
+
generator.plugin_class = NoHooksPlugin
|
|
45
|
+
|
|
46
|
+
# Act
|
|
47
|
+
hooks = generator.get_available_hooks()
|
|
48
|
+
|
|
49
|
+
# Assert
|
|
50
|
+
assert hooks == []
|
|
51
|
+
|
|
52
|
+
def test_contexts_should_not_have_duplicates_pip_install_in_hooks(
|
|
53
|
+
self, temp_dir: str, monkeypatch: MonkeyPatch
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Should not have duplicate hook methods in the list."""
|
|
56
|
+
|
|
57
|
+
# Arrange
|
|
58
|
+
plugin_content = """
|
|
59
|
+
from spaceforge import SpaceforgePlugin
|
|
60
|
+
class DuplicateHooksPlugin(SpaceforgePlugin):
|
|
61
|
+
def before_init(self) -> None:
|
|
62
|
+
pass
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
requirements_content = """
|
|
66
|
+
requests
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
plugin_path = os.path.join(temp_dir, "plugin.py")
|
|
70
|
+
with open(plugin_path, "w") as f:
|
|
71
|
+
f.write(plugin_content)
|
|
72
|
+
with open(os.path.join(temp_dir, "requirements.txt"), "w") as f:
|
|
73
|
+
f.write(requirements_content)
|
|
74
|
+
|
|
75
|
+
# Change to temporary directory (automatically restored after test)
|
|
76
|
+
monkeypatch.chdir(temp_dir)
|
|
77
|
+
|
|
78
|
+
generator = PluginGenerator(plugin_path)
|
|
79
|
+
generator.load_plugin()
|
|
80
|
+
|
|
81
|
+
# Act
|
|
82
|
+
contexts = generator.get_plugin_contexts()
|
|
83
|
+
hooks = contexts[0].hooks
|
|
84
|
+
assert hooks is not None
|
|
85
|
+
|
|
86
|
+
# Assert
|
|
87
|
+
processed_hooks = []
|
|
88
|
+
for hook in hooks["before_init"]:
|
|
89
|
+
assert hook not in processed_hooks
|
|
90
|
+
processed_hooks.append(hook)
|