Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e4c14fe
Instantiate plugin_config in plugin_config.py.
ehmatthes Sep 19, 2025
7e7c2ea
Adds CLI options, through cli.py and appropriate hook implementations.
ehmatthes Sep 19, 2025
4f993ce
Files for testing cli args.
ehmatthes Sep 19, 2025
bc723ba
Include new files in generation process.
ehmatthes Sep 19, 2025
2714cb5
Fix comment.
ehmatthes Sep 19, 2025
de3a520
Starting test for custom CLI arg.
ehmatthes Sep 19, 2025
b0b5bf8
Uncomments appropriate parts of cli.py in temp test env.
ehmatthes Sep 19, 2025
a217a7a
Helper function to get list of lines to uncomment.
ehmatthes Sep 20, 2025
21ffb8b
Uncomments test files.
ehmatthes Sep 20, 2025
ad792a1
Reference file for help output with a custom CLI arg.
ehmatthes Sep 21, 2025
7696b30
Print output of subprocess tests.
ehmatthes Sep 21, 2025
94b5c00
Check for failure in output, not '100%'.
ehmatthes Sep 21, 2025
8adb0c7
FAILURES -> FAILED
ehmatthes Sep 21, 2025
9b2ac45
Correct reference file.
ehmatthes Sep 21, 2025
0dd17b5
Message clarifying in test output that generated plugin is being modi…
ehmatthes Sep 21, 2025
e4be221
Modifies plugin's platform_deployer.py to exercise the example custom…
ehmatthes Sep 21, 2025
5a85b97
Distinct name for test of plugin cli.
ehmatthes Sep 21, 2025
5ef550e
Update reference plugins for integration tests.
ehmatthes Sep 21, 2025
9d3e0b7
All integration test reference files updated, all integration tests p…
ehmatthes Sep 21, 2025
e88d6c1
Update doc of generated project structure.
ehmatthes Sep 21, 2025
c43ae5c
Document changes to e2e test suite.
ehmatthes Sep 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/e2e_tests.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,3 +75,10 @@ test_newplatform_config.py .................. [100%]
This is really helpful for maintaining the test suite, and for development work.

**Note:** This can get a little flaky, because we're working in a temp environment that pytest will destroy when more resources are created. If you want to keep working with these resources, you can copy the entire `e2e_new_plugin_test0` directory to a more stable location. You'll have to rebuild the `django-simple-deploy/.venv` environment, but you'll have a useful, stable development environment to work with. You'll be able to run any tests you want from that environment.

Testing custom plugin CLI args
---

A plugin can extend the core django-simple-deploy CLI by defining its own custom CLI args. The code for doing this is included in the generated plugin, but code that would actually define a custom CLI arg is commented out.

The e2e test suite for this plugin generator includes `e2e_tests/test_plugin_cli.py`. This test uncomments the relevant code from a generated plugin. It inserts a block of code that uses the custom CLI arg in a `deploy` call, and asserts that the expected custom behavior works successfully. It also tests that `manage.py deploy --help` includes output documenting usage of the custom CLI arg.
7 changes: 5 additions & 2 deletions docs/plugin_structure.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ dsd-greenhost-high-traffic
│   └── README.md
├── dsd_greenhost_high_traffic
│   ├── __init__.py
│   ├── cli.py
│   ├── deploy.py
│   ├── deploy_messages.py
│   ├── platform_deployer.py
Expand All @@ -50,9 +51,11 @@ dsd-greenhost-high-traffic
│   └── utils.py
└── integration_tests
├── reference_files
└── test_greenhost_config.py
├── test_custom_cli_arg.py
├── test_greenhost_config.py
└── test_help_output.py

8 directories, 18 files
8 directories, 21 files
```

Notes
Expand Down
63 changes: 63 additions & 0 deletions plugin_template/plugin_pkg_name/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Extends the core django-simple-deploy CLI."""

import json
import shlex
import subprocess

from django_simple_deploy.management.commands.utils.plugin_utils import dsd_config
from django_simple_deploy.management.commands.utils.command_errors import (
DSDCommandError,
)

from .plugin_config import plugin_config


class PluginCLI:

def __init__(self, parser):
"""Add plugin-specific args."""
group_desc = "Plugin-specific CLI args for {{PackageName}}"
plugin_group = parser.add_argument_group(
title="Options for {{PackageName}}",
description = group_desc,
)

# plugin_group.add_argument(
# "--vm-size",
# type=str,
# help="Name for a preset vm-size configuration, ie `shared-cpu-2x`.",
# default="",
# )


def validate_cli(options):
"""Validate options that were passed to CLI."""

# vm_size = options["vm_size"]
# _validate_vm_size(vm_size)

pass


# --- Helper functions ---

# def _validate_vm_size(vm_size):
# """Validate the vm size arg that was passed."""
# if not vm_size:
# return

# if not dsd_config.unit_testing:
# cmd = "fly platform vm-sizes --json"
# cmd_parts = shlex.split(cmd)
# output = subprocess.run(cmd_parts, capture_output=True)
# allowed_sizes = list(json.loads(output.stdout).keys())
# else:
# allowed_sizes = ["shared-cup-1x", "shared-cpu-2x"]

# if vm_size not in allowed_sizes:
# msg = f"The vm-size {vm_size} requested is not available."
# msg += f"\n Allowed sizes: {' '.join(allowed_sizes)}"
# raise DSDCommandError(msg)

# # vm_size is valid. Set the relevant plugin_config attribute.
# plugin_config.vm_size = vm_size
16 changes: 14 additions & 2 deletions plugin_template/plugin_pkg_name/deploy.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,28 @@
import django_simple_deploy

from {{PluginName}}.platform_deployer import PlatformDeployer
from .plugin_config import PluginConfig
from .plugin_config import plugin_config
from .cli import PluginCLI, validate_cli


@django_simple_deploy.hookimpl
def dsd_get_plugin_config():
"""Get platform-specific attributes needed by core."""
plugin_config = PluginConfig()
return plugin_config


@django_simple_deploy.hookimpl
def dsd_get_plugin_cli(parser):
"""Get plugin's CLI extension."""
plugin_cli = PluginCLI(parser)


@django_simple_deploy.hookimpl
def dsd_validate_cli(options):
"""Validate and parse plugin-specific CLI args."""
validate_cli(options)


@django_simple_deploy.hookimpl
def dsd_deploy():
"""Carry out platform-specific deployment steps."""
Expand Down
1 change: 1 addition & 0 deletions plugin_template/plugin_pkg_name/platform_deployer.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ def _add_requirements(self):
import requests

from . import deploy_messages as platform_msgs
from .plugin_config import plugin_config

from django_simple_deploy.management.commands.utils import plugin_utils
from django_simple_deploy.management.commands.utils.plugin_utils import dsd_config
Expand Down
5 changes: 5 additions & 0 deletions plugin_template/plugin_pkg_name/plugin_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,8 @@ def __init__(self):
self.automate_all_supported = {{AutomateAllSupported}}
self.confirm_automate_all_msg = platform_msgs.confirm_automate_all
self.platform_name = "{{PlatformName}}"


# Create plugin_config once right here. This approach keeps from having to pass the config
# instance between core, plugins, and these utility functions.
plugin_config = PluginConfig()
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Options for dsd-flyio:
Full documentation: https://django-simple-deploy.readthedocs.io/en/latest/
quick_starts/quick_start_flyio/#customizing-the-deployment

--vm-size VM_SIZE Name for a preset vm-size configuration on Fly.io, ie
`shared-cpu-2x`.
22 changes: 22 additions & 0 deletions plugin_template/tests/integration_tests/test_custom_cli_arg.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
"""Test a custom plugin-specific CLI arg.
"""

import pytest

from tests.integration_tests.conftest import tmp_project
from tests.integration_tests.utils import manage_sample_project as msp

# Skip the default module-level `manage.py deploy call`, so we can call
# `deploy` with our own set of plugin-specific CLI args.
pytestmark = pytest.mark.skip_auto_dsd_call


# def test_vm_size_arg(tmp_project, request):
# """Test that a custom vm size is written to fly.toml."""
# cmd = "python manage.py deploy --vm-size shared-cpu-2x"
# msp.call_deploy(tmp_project, cmd, platform="fly_io")

# path = tmp_project / "fly.toml"
# contents_fly_toml = path.read_text()

# assert 'size = "shared-cpu-2x"' in contents_fly_toml
35 changes: 35 additions & 0 deletions plugin_template/tests/integration_tests/test_help_output.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"""Test the help output when {{PackageName}} is installed.

The core django-simple-deploy library tests its own help output.
This test checks that plugin-specific options are included in the help output.
"""

from pathlib import Path

import pytest

from tests.integration_tests.conftest import tmp_project
from tests.integration_tests.utils import manage_sample_project as msp

# Skip the default module-level `manage.py deploy call`, so we can call
# `deploy` with our own set of plugin-specific CLI args.
pytestmark = pytest.mark.skip_auto_dsd_call


# def test_plugin_help_output(tmp_project, request):
# """Test that {{PackageName}} CLI args are included in help output.

# Note: When updating this, run `manage.py deploy --help` in a terminal set
# to 80 characters wide. That splits help text at the same places as the
# test environment.
# On macOS, you can simply run:
# $ COLUMNS=80 python manage.py deploy --help
# """
# cmd = "python manage.py deploy --help"
# stdout, stderr = msp.call_deploy(tmp_project, cmd)

# path_reference = Path(__file__).parent / "reference_files" / "plugin_help_text.txt"
# help_lines = path_reference.read_text().splitlines()

# for line in help_lines:
# assert line in stdout
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
"""Integration tests for django-simple-deploy, targeting Fly.io."""
"""Integration tests for django-simple-deploy, targeting {{PlatformName}}."""

import sys
from pathlib import Path
Expand Down
2 changes: 2 additions & 0 deletions pytest.ini
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,5 @@ norecursedirs =

addopts = --import-mode=importlib
pythonpath = .

tmp_path_retention_count = 10
8 changes: 8 additions & 0 deletions tests/e2e_tests/reference_files/add_fly_toml.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# This is a dummy block to write a fly.toml file with only the
# text needed to pass the test_custom_cli_arg.py test. It is
# not a valid fly.toml file.
path = dsd_config.project_root / "fly.toml"

contents = "Dummy fly.toml file."
contents += '\n\nThe text \n\nsize = "shared-cpu-2x"\n\n appears in this file.\n'
plugin_utils.add_file(path, contents)
5 changes: 5 additions & 0 deletions tests/e2e_tests/reference_files/help_output_vm_size_arg.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Options for dsd-newfly:
Plugin-specific CLI args for dsd-newfly

--vm-size VM_SIZE Name for a preset vm-size configuration, ie `shared-
cpu-2x`.
2 changes: 1 addition & 1 deletion tests/e2e_tests/test_basic_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
- This makes an editable install of both django-simple-deploy and the new plugin.
- If there are issues, you can go the test env and modify both core and the new
plugin to troubleshoot.
- If you want to do this, ou may need to set run_core_plugin_tests to False, otherwise
- If you want to do this, you may need to use `--setup-plugins-only`, otherwise
the pytest temp dir will be garbage collected because so many temp dirs are being made.
"""

Expand Down
139 changes: 139 additions & 0 deletions tests/e2e_tests/test_plugin_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
"""Test a plugin that uses a custom CLI arg.

Some of the code that supports a custom CLI is commented out in the generated plugin.
So, we need to uncomment that code, then run the tests.

This test:
- Generates a new plugin.
- Modifies the generated files to enable a custom CLI arg.
- Modifies platform_deployer.py to use the custom CLI arg in a testable way.
- Sets up a development environment for django-simple-deploy core.
- Installs the new plugin to the development environment.
- Runs the plugin's integration tests, using a `deploy` call that includes the custom CLI arg.

Notes:
- This makes an editable install of both django-simple-deploy and the new plugin.
- If there are issues, you can go the test env and modify both core and the new
plugin to troubleshoot.
- If you want to do this, you may need to use `--setup-plugins-only`, otherwise
the pytest temp dir will be garbage collected because so many temp dirs are being made.
"""

from argparse import Namespace
import subprocess
import shlex
import shutil
from pathlib import Path

import pytest

from utils.plugin_config import PluginConfig
from tests.e2e_tests.utils import e2e_utils


# Skip these tests if uv is not available.
pytestmark = pytest.mark.skipif(
not e2e_utils.uv_available(), reason="uv must be installed in order to run e2e tests."
)


def test_custom_cli_arg(get_dev_env, cli_options):
"""Test a simple plugin config."""
dev_env_dir, path_to_python, path_dsd = get_dev_env

plugin_config = PluginConfig(
platform_name = "NewFly",
pkg_name = "dsd-newfly",
support_automate_all = True,
license_name = "eric",
)
e2e_utils.generate_plugin(get_dev_env, plugin_config)

msg = "\n*** Modifying plugin code to use the custom CLI arg that's commented out by default. ***\n"
print(msg)

# Uncomment CLI-related code.
path_plugin_dir = dev_env_dir / plugin_config.pkg_name
path_main_dir = path_plugin_dir / plugin_config.pkg_name.replace("-", "_")

path_cli = path_main_dir / "cli.py"
path_platform_deployer = path_main_dir / "platform_deployer.py"

path_tests = path_plugin_dir / "tests" / "integration_tests"
path_test_custom_cli = path_tests / "test_custom_cli_arg.py"
path_test_help = path_tests / "test_help_output.py"

# Assert these paths all exist.
assert all([path_cli.exists(), path_platform_deployer.exists(), path_test_custom_cli.exists(), path_test_help.exists()])

# Uncomment lines from relevant files.
uncomment_lines(path_cli, "25-30, 36-37, 44-63")
uncomment_lines(path_test_custom_cli, "14-22")
uncomment_lines(path_test_help, "19-35")

# Copy reference file for help output to new plugin.
path_help_reference = Path(__file__).parent / "reference_files" / "help_output_vm_size_arg.txt"
path_help_reference_plugin = path_tests / "reference_files" / "plugin_help_text.txt"
shutil.copyfile(path_help_reference, path_help_reference_plugin)

# Modify plugin's platform_deployer.py file to pass the uncommented test_custom_cli_arg.py file.
_write_add_fly_toml(path_platform_deployer)

if not cli_options.setup_plugins_only:
e2e_utils.run_core_plugin_tests(path_dsd, plugin_config, cli_options)

# --- Helper functions ---

def uncomment_lines(path, line_num_str):
"""Uncomment the given lines in a file.
"""
lines = path.read_text().splitlines()
new_lines = []

line_nums = _get_line_nums(line_num_str)
for line_num, line in enumerate(lines, start=1):
if line_num in line_nums:
new_lines.append(line.replace("# ", "", count=1))
else:
new_lines.append(line)

new_contents = "\n".join(new_lines)
path.write_text(new_contents)


def _get_line_nums(line_num_str):
"""Get list of lines from a string like "25-30, 36-37, 44-63"."""
range_strs = line_num_str.split(",")

line_nums = []
for range_str in range_strs:
if "-" not in range_str:
line_nums.append(int(range_str))
continue

start, end = range_str.split("-")
start, end = int(start), int(end)
line_nums += list(range(start, end+1))

return line_nums


def _write_add_fly_toml(path_platform_deployer):
"""Add code to platform_deployer that writes a fly.toml file to pass test_custom_cli_arg.py."""
target_string = "# Configure project for deployment to NewFly"

lines = path_platform_deployer.read_text().splitlines()

new_lines = []
for line in lines:
if target_string not in line:
new_lines.append(line)
continue

# This only runs once in the loop.
path_fly_toml_block = Path(__file__).parent / "reference_files" / "add_fly_toml.py"
fly_toml_lines = path_fly_toml_block.read_text().splitlines()
new_lines += fly_toml_lines

new_contents = "\n".join(new_lines)
path_platform_deployer.write_text(new_contents)
Loading