Skip to content

Commit 0850ccd

Browse files
authored
feat(tests): add integration tests to tutor paragon plugin (#36)
1 parent da690fd commit 0850ccd

File tree

9 files changed

+313
-5
lines changed

9 files changed

+313
-5
lines changed

plugins/tutor-contrib-paragon/Makefile

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,15 @@ isort: ## Sort imports. This target is not mandatory because the output may be
2222
isort --skip=templates ${SRC_DIRS}
2323

2424
unittest: ## Run code tests cases
25-
pytest tests
25+
pytest tests --ignore=tests/integration
26+
27+
integration-test: ## Run integration tests cases
28+
pytest tests/integration --order-scope=module
2629

2730
dev-requirements: ## Install dev requirements
2831
pip install -e .[dev]
2932

30-
run-tests: test unittest # Run static analysis and unit tests
33+
run-tests: test unittest integration-test # Run all tests: static analysis, unit tests, and integration tests
3134

3235
ESCAPE = 
3336
help: ## Print this help

plugins/tutor-contrib-paragon/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ dependencies = [
3030
"tutor>=19.0.0,<20.0.0",
3131
]
3232

33-
optional-dependencies = { dev = ["tutor[dev]>=19.0.0,<20.0.0", "pytest>=8.3.4"] }
33+
optional-dependencies = { dev = ["tutor[dev]>=19.0.0,<20.0.0", "pytest>=8.3.4", "pytest-order>=1.3.0"] }
3434

3535
# These fields will be set by hatch_build.py
3636
dynamic = ["version"]

plugins/tutor-contrib-paragon/tests/integration/__init__.py

Whitespace-only changes.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""Common fixtures for integration tests."""
2+
3+
import pytest
4+
import subprocess
5+
6+
from .helpers import PARAGON_NAME, PARAGON_IMAGE
7+
8+
9+
@pytest.fixture(scope="package", autouse=True)
10+
def setup_tutor_paragon_plugin():
11+
"""
12+
Fixture to set up the Tutor Paragon plugin for integration tests.
13+
This fixture enables the Paragon plugin, builds the necessary Docker image,
14+
and ensures that the plugin is disabled after the tests are complete.
15+
"""
16+
17+
subprocess.run(
18+
["tutor", "plugins", "enable", PARAGON_NAME],
19+
check=True,
20+
capture_output=True,
21+
)
22+
23+
subprocess.run(
24+
["tutor", "images", "build", PARAGON_IMAGE],
25+
check=True,
26+
capture_output=True,
27+
)
28+
29+
yield
30+
31+
subprocess.run(
32+
["tutor", "plugins", "disable", PARAGON_NAME],
33+
check=True,
34+
capture_output=True,
35+
)
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""Helper functions for integration tests of Paragon plugin."""
2+
3+
import subprocess
4+
import logging
5+
6+
logger = logging.getLogger(__name__)
7+
8+
PARAGON_NAME = "paragon"
9+
PARAGON_IMAGE = "paragon-builder"
10+
PARAGON_JOB = "paragon-build-tokens"
11+
PARAGON_THEME_SOURCES_FOLDER = "env/plugins/paragon/theme-sources"
12+
PARAGON_COMPILED_THEMES_FOLDER = "env/plugins/paragon/compiled-themes"
13+
14+
15+
def execute_tutor_command(command: list[str]):
16+
"""Run a Tutor command and return the result.
17+
18+
Args:
19+
command (list[str]): List of Tutor args, without the 'tutor' prefix.
20+
21+
Returns:
22+
subprocess.CompletedProcess: Contains stdout, stderr, returncode.
23+
"""
24+
full_command = ["tutor"] + command
25+
result = subprocess.run(
26+
full_command,
27+
stdout=subprocess.PIPE,
28+
stderr=subprocess.PIPE,
29+
text=True,
30+
check=False,
31+
)
32+
33+
if result.returncode != 0:
34+
logger.error("Command failed: %s", " ".join(full_command))
35+
logger.error("stderr: %s", result.stderr.strip())
36+
37+
return result
38+
39+
40+
def get_tutor_root_path():
41+
"""Get the root path of the Tutor project.
42+
43+
Raises:
44+
RuntimeError: If the Tutor root path cannot be obtained.
45+
46+
Returns:
47+
str: The path to the Tutor root directory.
48+
"""
49+
result = execute_tutor_command(["config", "printroot"])
50+
51+
if result.returncode != 0:
52+
raise RuntimeError("Failed to get Tutor root path: " + result.stderr)
53+
54+
return result.stdout.strip()
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
"""
2+
Integration tests for the Tutor Paragon plugin functionality.
3+
4+
This module contains tests to verify that the Paragon plugin for Tutor
5+
is functioning correctly, including building tokens with and without options,
6+
and handling invalid flags or parameters.
7+
"""
8+
9+
import os
10+
import shutil
11+
import pytest
12+
import re
13+
14+
from .helpers import (
15+
execute_tutor_command,
16+
get_tutor_root_path,
17+
PARAGON_JOB,
18+
PARAGON_COMPILED_THEMES_FOLDER,
19+
)
20+
21+
22+
@pytest.fixture(autouse=True)
23+
def clear_compiled_themes_folder():
24+
"""
25+
Fixture to clear the PARAGON_COMPILED_THEMES_FOLDER after each test.
26+
"""
27+
yield
28+
tutor_root = get_tutor_root_path()
29+
compiled_path = os.path.join(tutor_root, PARAGON_COMPILED_THEMES_FOLDER)
30+
if os.path.exists(compiled_path):
31+
shutil.rmtree(compiled_path)
32+
33+
34+
@pytest.mark.order(1)
35+
def test_build_tokens_without_options():
36+
"""
37+
Verify that running the build-tokens job without additional options
38+
completes successfully and produces output in the compiled-themes folder.
39+
"""
40+
41+
result = execute_tutor_command(["local", "do", PARAGON_JOB])
42+
assert result.returncode == 0, f"Error running build-tokens job: {result.stderr}"
43+
44+
tutor_root = get_tutor_root_path()
45+
compiled_path = os.path.join(tutor_root, PARAGON_COMPILED_THEMES_FOLDER)
46+
47+
contents = os.listdir(compiled_path)
48+
assert contents, f"No files were generated in {compiled_path}."
49+
50+
51+
@pytest.mark.order(2)
52+
def test_build_tokens_with_specific_theme():
53+
"""
54+
Verify that running the build-tokens job with the --themes option
55+
for a specific theme (e.g., 'indigo') produces the expected output.
56+
"""
57+
theme = "indigo"
58+
59+
result = execute_tutor_command(["local", "do", PARAGON_JOB, "--themes", theme])
60+
assert result.returncode == 0, f"Error building {theme} theme: {result.stderr}"
61+
62+
tutor_root = get_tutor_root_path()
63+
compiled_path = os.path.join(tutor_root, PARAGON_COMPILED_THEMES_FOLDER, "themes")
64+
65+
entries = os.listdir(compiled_path)
66+
assert theme in entries, f"'{theme}' theme not found in {compiled_path}."
67+
68+
theme_path = os.path.join(compiled_path, theme)
69+
assert os.path.isdir(theme_path), f"Expected {theme_path} to be a directory."
70+
assert os.listdir(theme_path), f"No files were generated inside {theme_path}."
71+
72+
73+
@pytest.mark.order(3)
74+
def test_build_tokens_excluding_core():
75+
"""
76+
Verify that running the build-tokens job with the --exclude-core option
77+
excludes the core theme from the output.
78+
"""
79+
result = execute_tutor_command(["local", "do", PARAGON_JOB, "--exclude-core"])
80+
assert result.returncode == 0, f"Error excluding core theme: {result.stderr}"
81+
82+
tutor_root = get_tutor_root_path()
83+
compiled_path = os.path.join(tutor_root, PARAGON_COMPILED_THEMES_FOLDER)
84+
85+
entries = os.listdir(compiled_path)
86+
assert "core" not in entries, "Core theme should be excluded but was found."
87+
88+
89+
@pytest.mark.order(4)
90+
def test_build_tokens_without_output_token_references():
91+
"""
92+
Ensure that when the build-tokens job is run with --output-references=false,
93+
the generated variables.css file does not contain any CSS variable references (var(--...)).
94+
"""
95+
result = execute_tutor_command(
96+
["local", "do", PARAGON_JOB, "--output-references=false"]
97+
)
98+
assert (
99+
result.returncode == 0
100+
), f"Error running build-tokens with --output-references=false: {result.stderr}"
101+
102+
tutor_root = get_tutor_root_path()
103+
compiled_path = os.path.join(tutor_root, PARAGON_COMPILED_THEMES_FOLDER)
104+
105+
core_variables_css = os.path.join(compiled_path, "core", "variables.css")
106+
theme_variables_css = os.path.join(compiled_path, "themes", "light", "variables.css")
107+
108+
assert os.path.exists(core_variables_css), f"{core_variables_css} does not exist."
109+
assert os.path.exists(theme_variables_css), f"{theme_variables_css} does not exist."
110+
111+
with open(core_variables_css, "r", encoding="utf-8") as f:
112+
core_content = f.read()
113+
with open(theme_variables_css, "r", encoding="utf-8") as f:
114+
theme_content = f.read()
115+
116+
token_reference_pattern = re.compile(r"var\(--.*?\)")
117+
core_references = token_reference_pattern.findall(core_content)
118+
theme_references = token_reference_pattern.findall(theme_content)
119+
120+
assert (
121+
not core_references
122+
), f"{core_variables_css} should not contain token references, but found: {core_references}"
123+
assert (
124+
not theme_references
125+
), f"{theme_variables_css} should not contain token references, but found: {theme_references}"
126+
127+
128+
@pytest.mark.order(5)
129+
def test_build_tokens_with_source_tokens_only():
130+
"""
131+
Ensure that when the build-tokens job is run with --source-tokens-only,
132+
the utility-classes.css file is not generated.
133+
"""
134+
result = execute_tutor_command(["local", "do", PARAGON_JOB, "--source-tokens-only"])
135+
assert (
136+
result.returncode == 0
137+
), f"Error running build-tokens with --source-tokens-only: {result.stderr}"
138+
139+
tutor_root = get_tutor_root_path()
140+
light_theme_path = os.path.join(
141+
tutor_root, PARAGON_COMPILED_THEMES_FOLDER, "themes", "light"
142+
)
143+
utility_classes_css = os.path.join(light_theme_path, "utility-classes.css")
144+
145+
assert not os.path.exists(
146+
utility_classes_css
147+
), f"{utility_classes_css} should not exist when --source-tokens-only is used."
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
"""
2+
Integration tests for the Tutor Paragon plugin setup.
3+
4+
This module contains tests to verify that the Paragon plugin for Tutor
5+
is correctly installed, enabled, and has the expected directory structure
6+
and jobs available in the system.
7+
"""
8+
9+
from .helpers import (
10+
execute_tutor_command,
11+
get_tutor_root_path,
12+
PARAGON_NAME,
13+
PARAGON_JOB,
14+
PARAGON_COMPILED_THEMES_FOLDER,
15+
PARAGON_THEME_SOURCES_FOLDER,
16+
)
17+
18+
import pytest
19+
import logging
20+
import os
21+
22+
logger = logging.getLogger(__name__)
23+
24+
25+
@pytest.mark.order(1)
26+
def test_paragon_plugin_installed():
27+
"""Verify that the 'paragon' plugin is installed and enabled."""
28+
29+
plugins_list_cmd = ["plugins", "list"]
30+
result = execute_tutor_command(plugins_list_cmd)
31+
32+
assert result.returncode == 0, f"Error listing plugins: {result.stderr}"
33+
assert (
34+
PARAGON_NAME in result.stdout
35+
), f"The '{PARAGON_NAME}' plugin is not installed"
36+
assert "enabled" in result.stdout, f"The '{PARAGON_NAME}' plugin is not enabled"
37+
38+
39+
@pytest.mark.order(2)
40+
def test_paragon_plugin_folders_created():
41+
"""Verify that the 'paragon' plugin's folders exist in the filesystem."""
42+
43+
project_root = get_tutor_root_path()
44+
45+
folders_to_check = [
46+
PARAGON_THEME_SOURCES_FOLDER,
47+
PARAGON_COMPILED_THEMES_FOLDER,
48+
]
49+
50+
for folder in folders_to_check:
51+
folder_path = f"{project_root}/{folder}"
52+
53+
assert os.path.exists(folder_path), f"Folder {folder_path} does not exist."
54+
assert os.path.isdir(folder_path), f"{folder_path} is not a directory."
55+
56+
57+
@pytest.mark.order(3)
58+
def test_paragon_plugin_build_tokens_job_exists():
59+
"""Verify that the 'paragon-build-tokens' job exists in Tutor's configuration."""
60+
61+
jobs_list_cmd = ["local", "do", "-h"]
62+
result = execute_tutor_command(jobs_list_cmd)
63+
64+
assert result.returncode == 0, f"Error listing jobs: {result.stderr}"
65+
assert PARAGON_JOB in result.stdout, f"Job '{PARAGON_JOB}' does not exist"

plugins/tutor-contrib-paragon/tutorparagon/plugin.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,13 @@
55
import importlib_resources
66
from tutor import hooks
77
from tutor import config as tutor_config
8+
import logging
89

910
from .__about__ import __version__
1011
from .commands import paragon_build_tokens
1112

13+
logger = logging.getLogger(__name__)
14+
1215
########################################
1316
# CONFIGURATION
1417
########################################
@@ -53,10 +56,10 @@ def create_paragon_folders(project_root: str) -> None:
5356
(compiled_themes_path, "Compiled Themes"),
5457
]:
5558
if os.path.exists(path):
56-
print(f"[paragon] {label} folder already exists at: {path}")
59+
logger.info(f"[paragon] {label} folder already exists at: {path}")
5760
else:
5861
os.makedirs(path, exist_ok=True)
59-
print(f"[paragon] Created {label} folder at: {path}")
62+
logger.info(f"[paragon] Created {label} folder at: {path}")
6063

6164

6265
########################################

plugins/tutor-contrib-paragon/tutorparagon/templates/paragon/build/paragon-builder/entrypoint.sh

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ npx paragon build-tokens \
5353
# Moves the built themes to the final volume directory.
5454
mkdir -p "$FINAL_BUILD_DIR"
5555
cp -a "$TMP_BUILD_DIR/." "$FINAL_BUILD_DIR/"
56+
chmod -R a+rw "$FINAL_BUILD_DIR"
5657

5758
# Clean up
5859
rm -rf "$TMP_BUILD_DIR"

0 commit comments

Comments
 (0)