spaceforge 0.1.0.dev0__py3-none-any.whl → 1.0.0__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 +0 -12
- 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} +2 -65
- spaceforge/test_runner_cli.py +69 -0
- spaceforge/test_runner_core.py +124 -0
- spaceforge/test_runner_execution.py +169 -0
- spaceforge-1.0.0.dist-info/METADATA +606 -0
- spaceforge-1.0.0.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.0.dist-info}/WHEEL +0 -0
- {spaceforge-0.1.0.dev0.dist-info → spaceforge-1.0.0.dist-info}/entry_points.txt +0 -0
- {spaceforge-0.1.0.dev0.dist-info → spaceforge-1.0.0.dist-info}/licenses/LICENSE +0 -0
- {spaceforge-0.1.0.dev0.dist-info → spaceforge-1.0.0.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 Any, Dict, List
|
|
4
4
|
from unittest.mock import Mock, mock_open, patch
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
@@ -55,7 +55,7 @@ class PluginExample(SpaceforgePlugin):
|
|
|
55
55
|
Context(
|
|
56
56
|
name_prefix="test_context",
|
|
57
57
|
description="Test context",
|
|
58
|
-
labels=
|
|
58
|
+
labels=["env:test"],
|
|
59
59
|
env=[Variable(key="TEST_VAR", value="test_value")],
|
|
60
60
|
)
|
|
61
61
|
]
|
|
@@ -64,16 +64,17 @@ class PluginExample(SpaceforgePlugin):
|
|
|
64
64
|
Webhook(
|
|
65
65
|
name_prefix="test_webhook",
|
|
66
66
|
endpoint="https://webhook.example.com",
|
|
67
|
-
|
|
67
|
+
secretFromParameter="api_key",
|
|
68
|
+
labels=["type:notification"],
|
|
68
69
|
)
|
|
69
70
|
]
|
|
70
71
|
|
|
71
72
|
__policies__ = [
|
|
72
73
|
Policy(
|
|
73
74
|
name_prefix="test_policy",
|
|
74
|
-
type="
|
|
75
|
+
type="NOTIFICATION",
|
|
75
76
|
body="package test",
|
|
76
|
-
labels=
|
|
77
|
+
labels=["type:security"],
|
|
77
78
|
)
|
|
78
79
|
]
|
|
79
80
|
|
|
@@ -318,14 +319,32 @@ class NotAPlugin:
|
|
|
318
319
|
"""Test binary installation command generation."""
|
|
319
320
|
generator = PluginGenerator()
|
|
320
321
|
generator.plugin_class = PluginExample
|
|
322
|
+
generator.plugin_working_directory = "/mnt/workspace/plugins/test_plugin"
|
|
323
|
+
generator.config = {
|
|
324
|
+
"setup_virtual_env": "cd /mnt/workspace/plugins/test_plugin && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
|
|
325
|
+
"plugin_mounted_path": "/mnt/workspace/plugins/test_plugin/plugin.py",
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
hooks: Dict[str, List[str]] = {"before_init": []}
|
|
329
|
+
mounted_files: List = []
|
|
330
|
+
generator._generate_binary_install_command(hooks, mounted_files)
|
|
331
|
+
|
|
332
|
+
# Should have added a script execution to hooks
|
|
333
|
+
assert len(hooks["before_init"]) == 1
|
|
334
|
+
command = hooks["before_init"][0]
|
|
321
335
|
|
|
322
|
-
|
|
336
|
+
# Should have added a mounted file with script content
|
|
337
|
+
assert len(mounted_files) == 1
|
|
338
|
+
script_content = mounted_files[0].content
|
|
323
339
|
|
|
324
|
-
|
|
325
|
-
assert "
|
|
326
|
-
assert "
|
|
327
|
-
|
|
328
|
-
|
|
340
|
+
# Check that hook command runs the script
|
|
341
|
+
assert "chmod +x" in command
|
|
342
|
+
assert "binary_install_test-cli.sh" in command
|
|
343
|
+
|
|
344
|
+
# Check script content contains binary installation logic
|
|
345
|
+
assert "test-cli" in script_content
|
|
346
|
+
assert "https://example.com/test-cli-amd64" in script_content
|
|
347
|
+
assert "https://example.com/test-cli-arm64" in script_content
|
|
329
348
|
|
|
330
349
|
def test_generate_binary_install_command_no_binaries(self) -> None:
|
|
331
350
|
"""Test binary command generation when no binaries."""
|
|
@@ -335,9 +354,18 @@ class NotAPlugin:
|
|
|
335
354
|
|
|
336
355
|
generator = PluginGenerator()
|
|
337
356
|
generator.plugin_class = NoBinariesPlugin
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
357
|
+
generator.plugin_working_directory = "/mnt/workspace/plugins/nobinaries"
|
|
358
|
+
generator.config = {
|
|
359
|
+
"setup_virtual_env": "cd /mnt/workspace/plugins/nobinaries && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
|
|
360
|
+
"plugin_mounted_path": "/mnt/workspace/plugins/nobinaries/plugin.py",
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
hooks: Dict[str, List[str]] = {"before_init": []}
|
|
364
|
+
mounted_files: List = []
|
|
365
|
+
generator._generate_binary_install_command(hooks, mounted_files)
|
|
366
|
+
# No binaries should mean no new commands or files added
|
|
367
|
+
assert len(hooks["before_init"]) == 0
|
|
368
|
+
assert len(mounted_files) == 0
|
|
341
369
|
|
|
342
370
|
def test_generate_binary_install_command_missing_urls(self) -> None:
|
|
343
371
|
"""Test binary command generation with missing URLs."""
|
|
@@ -347,9 +375,16 @@ class NotAPlugin:
|
|
|
347
375
|
|
|
348
376
|
generator = PluginGenerator()
|
|
349
377
|
generator.plugin_class = InvalidBinaryPlugin
|
|
350
|
-
|
|
378
|
+
generator.plugin_working_directory = "/mnt/workspace/plugins/invalidbinary"
|
|
379
|
+
generator.config = {
|
|
380
|
+
"setup_virtual_env": "cd /mnt/workspace/plugins/invalidbinary && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
|
|
381
|
+
"plugin_mounted_path": "/mnt/workspace/plugins/invalidbinary/plugin.py",
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
hooks: Dict[str, List[str]] = {"before_init": []}
|
|
385
|
+
mounted_files: List = []
|
|
351
386
|
with pytest.raises(ValueError, match="must have at least one download URL"):
|
|
352
|
-
generator.
|
|
387
|
+
generator._generate_binary_install_command(hooks, mounted_files)
|
|
353
388
|
|
|
354
389
|
def test_get_plugin_policies(self) -> None:
|
|
355
390
|
"""Test policy extraction."""
|
|
@@ -361,7 +396,7 @@ class NotAPlugin:
|
|
|
361
396
|
assert policies is not None
|
|
362
397
|
assert len(policies) == 1
|
|
363
398
|
assert policies[0].name_prefix == "test_policy"
|
|
364
|
-
assert policies[0].type == "
|
|
399
|
+
assert policies[0].type == "NOTIFICATION"
|
|
365
400
|
assert policies[0].body == "package test"
|
|
366
401
|
|
|
367
402
|
def test_get_plugin_webhooks(self) -> None:
|
|
@@ -377,33 +412,50 @@ class NotAPlugin:
|
|
|
377
412
|
assert webhooks[0].endpoint == "https://webhook.example.com"
|
|
378
413
|
|
|
379
414
|
@patch("os.path.exists")
|
|
380
|
-
|
|
381
|
-
def test_get_plugin_contexts_with_requirements(
|
|
382
|
-
self, mock_file: Mock, mock_exists: Mock
|
|
383
|
-
) -> None:
|
|
415
|
+
def test_get_plugin_contexts_with_requirements(self, mock_exists: Mock) -> None:
|
|
384
416
|
"""Test context generation with requirements.txt."""
|
|
385
417
|
mock_exists.side_effect = (
|
|
386
418
|
lambda path: path == "requirements.txt" or "plugin.py" in path
|
|
387
419
|
)
|
|
388
420
|
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
421
|
+
# Mock specific file contents with a custom open function
|
|
422
|
+
original_open = open
|
|
423
|
+
|
|
424
|
+
def mock_open_func(filename: str, *args: Any, **kwargs: Any) -> Any:
|
|
425
|
+
if filename == "requirements.txt":
|
|
426
|
+
from io import StringIO
|
|
392
427
|
|
|
393
|
-
|
|
428
|
+
return StringIO("requirements content")
|
|
429
|
+
elif "plugin.py" in filename:
|
|
430
|
+
from io import StringIO
|
|
431
|
+
|
|
432
|
+
return StringIO("plugin content")
|
|
433
|
+
else:
|
|
434
|
+
return original_open(filename, *args, **kwargs)
|
|
435
|
+
|
|
436
|
+
with patch("builtins.open", side_effect=mock_open_func):
|
|
437
|
+
generator = PluginGenerator(self.test_plugin_path)
|
|
438
|
+
generator.plugin_class = PluginExample
|
|
439
|
+
generator.plugin_working_directory = "/mnt/workspace/plugins/test_plugin"
|
|
440
|
+
generator.config = {
|
|
441
|
+
"setup_virtual_env": "cd /mnt/workspace/plugins/test_plugin && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
|
|
442
|
+
"plugin_mounted_path": "/mnt/workspace/plugins/test_plugin/plugin.py",
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
contexts = generator.get_plugin_contexts()
|
|
394
446
|
|
|
395
447
|
assert len(contexts) == 1
|
|
396
448
|
context = contexts[0]
|
|
397
449
|
|
|
398
|
-
# Should have before_init hooks
|
|
450
|
+
# Should have before_init hooks with mkdir command
|
|
399
451
|
assert context.hooks is not None
|
|
400
452
|
assert "before_init" in context.hooks
|
|
401
|
-
|
|
453
|
+
mkdir_command = None
|
|
402
454
|
for cmd in context.hooks["before_init"]:
|
|
403
|
-
if "
|
|
404
|
-
|
|
455
|
+
if "mkdir -p" in cmd:
|
|
456
|
+
mkdir_command = cmd
|
|
405
457
|
break
|
|
406
|
-
assert
|
|
458
|
+
assert mkdir_command is not None
|
|
407
459
|
|
|
408
460
|
# Should have requirements.txt as mounted file
|
|
409
461
|
assert context.mounted_files is not None
|
|
@@ -415,22 +467,44 @@ class NotAPlugin:
|
|
|
415
467
|
assert req_file is not None
|
|
416
468
|
assert req_file.content == "requirements content"
|
|
417
469
|
|
|
470
|
+
# Should have plugin.py as mounted file
|
|
471
|
+
plugin_file = None
|
|
472
|
+
for mf in context.mounted_files:
|
|
473
|
+
if "plugin.py" in mf.path:
|
|
474
|
+
plugin_file = mf
|
|
475
|
+
break
|
|
476
|
+
assert plugin_file is not None
|
|
477
|
+
assert plugin_file.content == "plugin content"
|
|
478
|
+
|
|
418
479
|
@patch("os.path.exists")
|
|
419
|
-
|
|
420
|
-
def test_get_plugin_contexts_basic(
|
|
421
|
-
self, mock_file: Mock, mock_exists: Mock
|
|
422
|
-
) -> None:
|
|
480
|
+
def test_get_plugin_contexts_basic(self, mock_exists: Mock) -> None:
|
|
423
481
|
"""Test basic context generation."""
|
|
424
482
|
mock_exists.side_effect = lambda path: "plugin.py" in path
|
|
425
483
|
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
generator.plugin_working_directory = "/mnt/workspace/plugins/test_plugin"
|
|
484
|
+
# Mock specific file contents with a custom open function
|
|
485
|
+
original_open = open
|
|
429
486
|
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
487
|
+
def mock_open_func(filename: str, *args: Any, **kwargs: Any) -> Any:
|
|
488
|
+
if "plugin.py" in filename:
|
|
489
|
+
from io import StringIO
|
|
490
|
+
|
|
491
|
+
return StringIO("plugin content")
|
|
492
|
+
else:
|
|
493
|
+
return original_open(filename, *args, **kwargs)
|
|
494
|
+
|
|
495
|
+
with patch("builtins.open", side_effect=mock_open_func):
|
|
496
|
+
generator = PluginGenerator(self.test_plugin_path)
|
|
497
|
+
generator.plugin_class = PluginExample
|
|
498
|
+
generator.plugin_working_directory = "/mnt/workspace/plugins/test_plugin"
|
|
499
|
+
generator.config = {
|
|
500
|
+
"setup_virtual_env": "cd /mnt/workspace/plugins/test_plugin && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
|
|
501
|
+
"plugin_mounted_path": "/mnt/workspace/plugins/test_plugin/plugin.py",
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
with patch.object(
|
|
505
|
+
generator, "get_available_hooks", return_value=["after_plan"]
|
|
506
|
+
):
|
|
507
|
+
contexts = generator.get_plugin_contexts()
|
|
434
508
|
|
|
435
509
|
assert len(contexts) == 1
|
|
436
510
|
context = contexts[0]
|
|
@@ -444,19 +518,36 @@ class NotAPlugin:
|
|
|
444
518
|
break
|
|
445
519
|
assert plugin_file is not None
|
|
446
520
|
|
|
447
|
-
# Should have
|
|
521
|
+
# Should have hooks for after_plan (the hook we mocked as available)
|
|
448
522
|
assert context.hooks is not None
|
|
449
523
|
assert "after_plan" in context.hooks
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
524
|
+
|
|
525
|
+
# Should have a script execution command
|
|
526
|
+
hook_command = context.hooks["after_plan"][0]
|
|
527
|
+
assert "chmod +x" in hook_command
|
|
528
|
+
assert "after_plan.sh" in hook_command
|
|
529
|
+
|
|
530
|
+
# Should have a mounted script file for after_plan
|
|
531
|
+
after_plan_script = None
|
|
532
|
+
for mf in context.mounted_files:
|
|
533
|
+
if "after_plan.sh" in mf.path:
|
|
534
|
+
after_plan_script = mf
|
|
535
|
+
break
|
|
536
|
+
assert after_plan_script is not None
|
|
537
|
+
assert "spaceforge runner" in after_plan_script.content
|
|
538
|
+
assert "after_plan" in after_plan_script.content
|
|
453
539
|
|
|
454
540
|
def test_generate_manifest(self) -> None:
|
|
455
541
|
"""Test complete manifest generation."""
|
|
456
542
|
generator = PluginGenerator(self.test_plugin_path)
|
|
543
|
+
generator.plugin_class = PluginExample
|
|
544
|
+
generator.plugin_working_directory = "/mnt/workspace/plugins/test_plugin"
|
|
545
|
+
generator.config = {
|
|
546
|
+
"setup_virtual_env": "cd /mnt/workspace/plugins/test_plugin && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
|
|
547
|
+
"plugin_mounted_path": "/mnt/workspace/plugins/test_plugin/plugin.py",
|
|
548
|
+
}
|
|
457
549
|
|
|
458
550
|
with patch.object(generator, "load_plugin"):
|
|
459
|
-
generator.plugin_class = PluginExample
|
|
460
551
|
manifest = generator.generate_manifest()
|
|
461
552
|
|
|
462
553
|
assert isinstance(manifest, PluginManifest)
|
|
@@ -474,7 +565,7 @@ class NotAPlugin:
|
|
|
474
565
|
"""Test YAML writing functionality."""
|
|
475
566
|
generator = PluginGenerator(output_path=self.test_output_path)
|
|
476
567
|
manifest = PluginManifest(
|
|
477
|
-
|
|
568
|
+
name="test",
|
|
478
569
|
version="1.0.0",
|
|
479
570
|
description="Test",
|
|
480
571
|
author="Test Author",
|
|
@@ -500,7 +591,7 @@ class NotAPlugin:
|
|
|
500
591
|
"""Test complete generate method."""
|
|
501
592
|
generator = PluginGenerator()
|
|
502
593
|
mock_manifest = PluginManifest(
|
|
503
|
-
|
|
594
|
+
name="test", version="1.0.0", description="Test", author="Test"
|
|
504
595
|
)
|
|
505
596
|
mock_generate_manifest.return_value = mock_manifest
|
|
506
597
|
|
|
@@ -606,11 +697,30 @@ class TestPluginGeneratorEdgeCases:
|
|
|
606
697
|
|
|
607
698
|
generator = PluginGenerator()
|
|
608
699
|
generator.plugin_class = SingleArchPlugin
|
|
700
|
+
generator.plugin_working_directory = "/mnt/workspace/plugins/singlearch"
|
|
701
|
+
generator.config = {
|
|
702
|
+
"setup_virtual_env": "cd /mnt/workspace/plugins/singlearch && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
|
|
703
|
+
"plugin_mounted_path": "/mnt/workspace/plugins/singlearch/plugin.py",
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
hooks: Dict[str, List[str]] = {"before_init": []}
|
|
707
|
+
mounted_files: List = []
|
|
708
|
+
generator._generate_binary_install_command(hooks, mounted_files)
|
|
709
|
+
|
|
710
|
+
# Should have added a script execution to hooks
|
|
711
|
+
assert len(hooks["before_init"]) == 1
|
|
712
|
+
command = hooks["before_init"][0]
|
|
609
713
|
|
|
610
|
-
|
|
714
|
+
# Should have added a mounted file with script content
|
|
715
|
+
assert len(mounted_files) == 1
|
|
716
|
+
script_content = mounted_files[0].content
|
|
611
717
|
|
|
612
|
-
|
|
613
|
-
assert "
|
|
718
|
+
# Check that hook command runs the script
|
|
719
|
+
assert "chmod +x" in command
|
|
720
|
+
assert "binary_install_single-arch.sh" in command
|
|
721
|
+
|
|
722
|
+
# Check script content contains the binary URL
|
|
723
|
+
assert "https://example.com/binary-amd64" in script_content
|
|
614
724
|
|
|
615
725
|
def test_context_merging_with_existing(self) -> None:
|
|
616
726
|
"""Test that generated hooks are merged with existing context hooks."""
|
|
@@ -638,11 +748,26 @@ class TestPluginGeneratorEdgeCases:
|
|
|
638
748
|
generator = PluginGenerator("/fake/path")
|
|
639
749
|
generator.plugin_class = ExistingHooksPlugin
|
|
640
750
|
generator.plugin_working_directory = "/mnt/workspace/plugins/existing_hooks"
|
|
751
|
+
generator.config = {
|
|
752
|
+
"setup_virtual_env": "cd /mnt/workspace/plugins/existing_hooks && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
|
|
753
|
+
"plugin_mounted_path": "/mnt/workspace/plugins/existing_hooks/plugin.py",
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
# Mock specific file contents with a custom open function
|
|
757
|
+
original_open = open
|
|
758
|
+
|
|
759
|
+
def mock_open_func(filename: str, *args: Any, **kwargs: Any) -> Any:
|
|
760
|
+
if filename == "/fake/path":
|
|
761
|
+
from io import StringIO
|
|
762
|
+
|
|
763
|
+
return StringIO("fake content")
|
|
764
|
+
else:
|
|
765
|
+
return original_open(filename, *args, **kwargs)
|
|
641
766
|
|
|
642
767
|
with patch("os.path.exists") as mock_exists:
|
|
643
768
|
# Only plugin file exists, not requirements.txt
|
|
644
769
|
mock_exists.side_effect = lambda path: path == "/fake/path"
|
|
645
|
-
with patch("builtins.open",
|
|
770
|
+
with patch("builtins.open", side_effect=mock_open_func):
|
|
646
771
|
contexts = generator.get_plugin_contexts()
|
|
647
772
|
|
|
648
773
|
context = contexts[0]
|
|
@@ -658,14 +783,101 @@ class TestPluginGeneratorEdgeCases:
|
|
|
658
783
|
assert len(existing_files) == 1
|
|
659
784
|
|
|
660
785
|
# Plugin file should be mounted (path contains the working directory)
|
|
661
|
-
|
|
786
|
+
# The new implementation creates both plugin.py and hook scripts
|
|
787
|
+
working_dir_files = [
|
|
662
788
|
mf
|
|
663
789
|
for mf in context.mounted_files
|
|
664
790
|
if "/mnt/workspace/plugins/existing_hooks" in mf.path
|
|
665
791
|
]
|
|
792
|
+
assert (
|
|
793
|
+
len(working_dir_files) >= 1
|
|
794
|
+
) # At least plugin.py, possibly hook scripts too
|
|
795
|
+
|
|
796
|
+
# Specifically check for plugin.py
|
|
797
|
+
plugin_files = [
|
|
798
|
+
mf for mf in context.mounted_files if mf.path.endswith("plugin.py")
|
|
799
|
+
]
|
|
666
800
|
assert len(plugin_files) == 1
|
|
667
801
|
|
|
668
802
|
# Should have existing env vars
|
|
669
803
|
assert context.env is not None
|
|
670
804
|
existing_vars = [var for var in context.env if var.key == "EXISTING"]
|
|
671
805
|
assert len(existing_vars) == 1
|
|
806
|
+
|
|
807
|
+
def test_parameters_and_variables_id_generation(self) -> None:
|
|
808
|
+
class ParametersAndVariablesPlugin(SpaceforgePlugin):
|
|
809
|
+
__plugin_name__ = "parameters_and_variables"
|
|
810
|
+
|
|
811
|
+
__parameters__ = [
|
|
812
|
+
Parameter(
|
|
813
|
+
name="api key",
|
|
814
|
+
description="API key for authentication",
|
|
815
|
+
required=True,
|
|
816
|
+
sensitive=True,
|
|
817
|
+
),
|
|
818
|
+
Parameter(
|
|
819
|
+
name="endpoint",
|
|
820
|
+
description="API endpoint URL",
|
|
821
|
+
required=False,
|
|
822
|
+
default="https://api.example.com",
|
|
823
|
+
),
|
|
824
|
+
]
|
|
825
|
+
|
|
826
|
+
__contexts__ = [
|
|
827
|
+
Context(
|
|
828
|
+
name_prefix="existing",
|
|
829
|
+
description="Existing context",
|
|
830
|
+
hooks={"before_init": ["echo 'existing hook'"]},
|
|
831
|
+
mounted_files=[
|
|
832
|
+
MountedFile(
|
|
833
|
+
path="/existing", content="existing", sensitive=False
|
|
834
|
+
)
|
|
835
|
+
],
|
|
836
|
+
env=[
|
|
837
|
+
Variable(
|
|
838
|
+
key="API_KEY",
|
|
839
|
+
value_from_parameter="api key",
|
|
840
|
+
sensitive=True,
|
|
841
|
+
),
|
|
842
|
+
Variable(key="ENDPOINT", value_from_parameter="endpoint"),
|
|
843
|
+
],
|
|
844
|
+
)
|
|
845
|
+
]
|
|
846
|
+
|
|
847
|
+
def after_plan(self) -> None:
|
|
848
|
+
pass
|
|
849
|
+
|
|
850
|
+
generator = PluginGenerator("/fake/path")
|
|
851
|
+
generator.plugin_class = ParametersAndVariablesPlugin
|
|
852
|
+
generator.plugin_working_directory = (
|
|
853
|
+
"/mnt/workspace/plugins/parametersandvariables"
|
|
854
|
+
)
|
|
855
|
+
generator.config = {
|
|
856
|
+
"setup_virtual_env": "cd /mnt/workspace/plugins/parametersandvariables && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
|
|
857
|
+
"plugin_mounted_path": "/mnt/workspace/plugins/parametersandvariables/plugin.py",
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
# Mock specific file contents with a custom open function
|
|
861
|
+
original_open = open
|
|
862
|
+
|
|
863
|
+
def mock_open_func(filename: str, *args: Any, **kwargs: Any) -> Any:
|
|
864
|
+
if filename == "/fake/path":
|
|
865
|
+
from io import StringIO
|
|
866
|
+
|
|
867
|
+
return StringIO("fake content")
|
|
868
|
+
else:
|
|
869
|
+
return original_open(filename, *args, **kwargs)
|
|
870
|
+
|
|
871
|
+
with patch("os.path.exists") as mock_exists:
|
|
872
|
+
# Only plugin file exists, not requirements.txt
|
|
873
|
+
mock_exists.side_effect = lambda path: path == "/fake/path"
|
|
874
|
+
with patch("builtins.open", side_effect=mock_open_func):
|
|
875
|
+
contexts = generator.get_plugin_contexts()
|
|
876
|
+
parameters = generator.get_plugin_parameters()
|
|
877
|
+
|
|
878
|
+
assert contexts[0].env is not None
|
|
879
|
+
assert parameters is not None
|
|
880
|
+
assert parameters[0].id is not None
|
|
881
|
+
assert parameters[1].id is not None
|
|
882
|
+
assert parameters[0].id == contexts[0].env[0].value_from_parameter
|
|
883
|
+
assert parameters[1].id == contexts[0].env[1].value_from_parameter
|
|
@@ -0,0 +1,194 @@
|
|
|
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
|
+
mounted_files: List = []
|
|
84
|
+
|
|
85
|
+
# Act
|
|
86
|
+
generator._generate_binary_install_command(hooks, mounted_files)
|
|
87
|
+
|
|
88
|
+
# Assert
|
|
89
|
+
# Should have added a script execution to hooks
|
|
90
|
+
assert len(hooks["before_init"]) == 1
|
|
91
|
+
command = hooks["before_init"][0]
|
|
92
|
+
|
|
93
|
+
# Should have added a mounted file with script content
|
|
94
|
+
assert len(mounted_files) == 1
|
|
95
|
+
script_content = mounted_files[0].content
|
|
96
|
+
|
|
97
|
+
# Check that hook command runs the script
|
|
98
|
+
assert "chmod +x" in command
|
|
99
|
+
assert "binary_install_multi-cli.sh" in command
|
|
100
|
+
|
|
101
|
+
# Check script content contains binary installation logic
|
|
102
|
+
assert "multi-cli" in script_content
|
|
103
|
+
assert "https://example.com/multi-cli-amd64" in script_content
|
|
104
|
+
assert "https://example.com/multi-cli-arm64" in script_content
|
|
105
|
+
|
|
106
|
+
def test_should_not_add_commands_when_no_binaries(self) -> None:
|
|
107
|
+
"""Should not add installation commands when plugin has no binaries."""
|
|
108
|
+
|
|
109
|
+
# Arrange
|
|
110
|
+
class NoBinariesPlugin(SpaceforgePlugin):
|
|
111
|
+
pass
|
|
112
|
+
|
|
113
|
+
generator = PluginGenerator()
|
|
114
|
+
generator.plugin_class = NoBinariesPlugin
|
|
115
|
+
generator.plugin_working_directory = "/mnt/workspace/plugins/nobinaries"
|
|
116
|
+
generator.config = {
|
|
117
|
+
"setup_virtual_env": "cd /mnt/workspace/plugins/nobinaries && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
|
|
118
|
+
"plugin_mounted_path": "/mnt/workspace/plugins/nobinaries/plugin.py",
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
hooks: Dict[str, List[str]] = {"before_init": []}
|
|
122
|
+
mounted_files: List = []
|
|
123
|
+
|
|
124
|
+
# Act
|
|
125
|
+
generator._generate_binary_install_command(hooks, mounted_files)
|
|
126
|
+
|
|
127
|
+
# Assert
|
|
128
|
+
# No binaries should mean no new commands or files added
|
|
129
|
+
assert len(hooks["before_init"]) == 0
|
|
130
|
+
assert len(mounted_files) == 0
|
|
131
|
+
|
|
132
|
+
def test_should_raise_error_when_binary_has_no_download_urls(self) -> None:
|
|
133
|
+
"""Should raise ValueError when binary has empty download URLs."""
|
|
134
|
+
|
|
135
|
+
# Arrange
|
|
136
|
+
class InvalidBinaryPlugin(SpaceforgePlugin):
|
|
137
|
+
__binaries__ = [Binary(name="invalid", download_urls={})]
|
|
138
|
+
|
|
139
|
+
generator = PluginGenerator()
|
|
140
|
+
generator.plugin_class = InvalidBinaryPlugin
|
|
141
|
+
generator.plugin_working_directory = "/mnt/workspace/plugins/invalidbinary"
|
|
142
|
+
generator.config = {
|
|
143
|
+
"setup_virtual_env": "cd /mnt/workspace/plugins/invalidbinary && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
|
|
144
|
+
"plugin_mounted_path": "/mnt/workspace/plugins/invalidbinary/plugin.py",
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
hooks: Dict[str, List[str]] = {"before_init": []}
|
|
148
|
+
mounted_files: List = []
|
|
149
|
+
|
|
150
|
+
# Act & Assert
|
|
151
|
+
with pytest.raises(ValueError, match="must have at least one download URL"):
|
|
152
|
+
generator._generate_binary_install_command(hooks, mounted_files)
|
|
153
|
+
|
|
154
|
+
def test_should_handle_single_architecture_binary(self) -> None:
|
|
155
|
+
"""Should generate appropriate commands for single architecture binary."""
|
|
156
|
+
|
|
157
|
+
# Arrange
|
|
158
|
+
class SingleArchPlugin(SpaceforgePlugin):
|
|
159
|
+
__binaries__ = [
|
|
160
|
+
Binary(
|
|
161
|
+
name="single-arch",
|
|
162
|
+
download_urls={"amd64": "https://example.com/binary-amd64"},
|
|
163
|
+
)
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
generator = PluginGenerator()
|
|
167
|
+
generator.plugin_class = SingleArchPlugin
|
|
168
|
+
generator.plugin_working_directory = "/mnt/workspace/plugins/singlearch"
|
|
169
|
+
generator.config = {
|
|
170
|
+
"setup_virtual_env": "cd /mnt/workspace/plugins/singlearch && python -m venv ./venv && source venv/bin/activate && pip install spaceforge",
|
|
171
|
+
"plugin_mounted_path": "/mnt/workspace/plugins/singlearch/plugin.py",
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
hooks: Dict[str, List[str]] = {"before_init": []}
|
|
175
|
+
mounted_files: List = []
|
|
176
|
+
|
|
177
|
+
# Act
|
|
178
|
+
generator._generate_binary_install_command(hooks, mounted_files)
|
|
179
|
+
|
|
180
|
+
# Assert
|
|
181
|
+
# Should have added a script execution to hooks
|
|
182
|
+
assert len(hooks["before_init"]) == 1
|
|
183
|
+
command = hooks["before_init"][0]
|
|
184
|
+
|
|
185
|
+
# Should have added a mounted file with script content
|
|
186
|
+
assert len(mounted_files) == 1
|
|
187
|
+
script_content = mounted_files[0].content
|
|
188
|
+
|
|
189
|
+
# Check that hook command runs the script
|
|
190
|
+
assert "chmod +x" in command
|
|
191
|
+
assert "binary_install_single-arch.sh" in command
|
|
192
|
+
|
|
193
|
+
# Check script content contains the binary URL
|
|
194
|
+
assert "https://example.com/binary-amd64" in script_content
|