2019-07-02 20:16:44 +00:00
|
|
|
import os
|
2019-05-11 22:11:44 +00:00
|
|
|
import tempfile
|
2019-05-11 17:31:18 +00:00
|
|
|
import unittest
|
2023-02-08 04:35:25 +00:00
|
|
|
from io import StringIO
|
2021-11-23 08:25:09 +00:00
|
|
|
from unittest.mock import Mock, patch
|
2019-05-11 17:31:18 +00:00
|
|
|
|
2022-02-07 17:11:43 +00:00
|
|
|
from tests.helpers import PluginsTestCase, temporary_root
|
2019-06-03 22:44:12 +00:00
|
|
|
from tutor import config as tutor_config
|
2022-02-07 17:11:43 +00:00
|
|
|
from tutor import env, exceptions, fmt, plugins
|
|
|
|
from tutor.__about__ import __version__
|
|
|
|
from tutor.plugins.v0 import DictPlugin
|
2021-04-06 10:09:00 +00:00
|
|
|
from tutor.types import Config
|
2019-05-11 17:31:18 +00:00
|
|
|
|
|
|
|
|
2022-02-07 17:11:43 +00:00
|
|
|
class EnvTests(PluginsTestCase):
|
2021-02-25 08:09:14 +00:00
|
|
|
def test_walk_templates(self) -> None:
|
2022-05-30 16:12:00 +00:00
|
|
|
renderer = env.Renderer()
|
2020-01-16 10:52:53 +00:00
|
|
|
templates = list(renderer.walk_templates("local"))
|
2019-05-11 17:31:18 +00:00
|
|
|
self.assertIn("local/docker-compose.yml", templates)
|
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def test_walk_templates_partials_are_ignored(self) -> None:
|
2020-01-16 10:52:53 +00:00
|
|
|
template_name = "apps/openedx/settings/partials/common_all.py"
|
2022-05-30 16:12:00 +00:00
|
|
|
renderer = env.Renderer()
|
2020-01-16 10:52:53 +00:00
|
|
|
templates = list(renderer.walk_templates("apps"))
|
2021-06-02 13:16:32 +00:00
|
|
|
self.assertIn(template_name, renderer.environment.loader.list_templates())
|
2020-01-16 10:52:53 +00:00
|
|
|
self.assertNotIn(template_name, templates)
|
|
|
|
|
2022-05-30 19:41:06 +00:00
|
|
|
def test_files_are_rendered(self) -> None:
|
|
|
|
self.assertTrue(env.is_rendered("some/file"))
|
|
|
|
self.assertFalse(env.is_rendered(".git"))
|
|
|
|
self.assertFalse(env.is_rendered(".git/subdir"))
|
|
|
|
self.assertFalse(env.is_rendered("directory/.git"))
|
|
|
|
self.assertFalse(env.is_rendered("directory/.git/somefile"))
|
|
|
|
self.assertFalse(env.is_rendered("directory/somefile.pyc"))
|
|
|
|
self.assertTrue(env.is_rendered("directory/somedir.pyc/somefile"))
|
|
|
|
self.assertFalse(env.is_rendered("directory/__pycache__"))
|
|
|
|
self.assertFalse(env.is_rendered("directory/__pycache__/somefile"))
|
|
|
|
self.assertFalse(env.is_rendered("directory/partials/extra.scss"))
|
|
|
|
self.assertFalse(env.is_rendered("directory/partials"))
|
|
|
|
self.assertFalse(env.is_rendered("partials/somefile"))
|
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def test_is_binary_file(self) -> None:
|
2020-01-16 14:40:38 +00:00
|
|
|
self.assertTrue(env.is_binary_file("/home/somefile.ico"))
|
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def test_find_os_path(self) -> None:
|
2022-11-21 08:56:59 +00:00
|
|
|
environment = env.JinjaEnvironment()
|
|
|
|
path = environment.find_os_path("local/docker-compose.yml")
|
2020-01-16 14:40:38 +00:00
|
|
|
self.assertTrue(os.path.exists(path))
|
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def test_pathjoin(self) -> None:
|
2022-01-08 11:19:46 +00:00
|
|
|
with temporary_root() as root:
|
2021-11-23 08:25:09 +00:00
|
|
|
self.assertEqual(
|
2022-01-08 11:19:46 +00:00
|
|
|
os.path.join(env.base_dir(root), "dummy"), env.pathjoin(root, "dummy")
|
2021-11-23 08:25:09 +00:00
|
|
|
)
|
2019-05-11 17:31:18 +00:00
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def test_render_str(self) -> None:
|
2019-05-11 17:31:18 +00:00
|
|
|
self.assertEqual(
|
|
|
|
"hello world", env.render_str({"name": "world"}, "hello {{ name }}")
|
|
|
|
)
|
|
|
|
|
2021-04-13 20:14:43 +00:00
|
|
|
def test_render_unknown(self) -> None:
|
|
|
|
config: Config = {
|
|
|
|
"var1": "a",
|
|
|
|
}
|
|
|
|
self.assertEqual("ab", env.render_unknown(config, "{{ var1 }}b"))
|
|
|
|
self.assertEqual({"x": "ac"}, env.render_unknown(config, {"x": "{{ var1 }}c"}))
|
2023-09-05 14:00:01 +00:00
|
|
|
self.assertEqual(["x", "ac"], env.render_unknown(config, ["x", "{{ var1 }}c"]))
|
2021-04-13 20:14:43 +00:00
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def test_common_domain(self) -> None:
|
2019-05-21 10:34:29 +00:00
|
|
|
self.assertEqual(
|
|
|
|
"mydomain.com",
|
|
|
|
env.render_str(
|
|
|
|
{"d1": "d1.mydomain.com", "d2": "d2.mydomain.com"},
|
|
|
|
"{{ d1|common_domain(d2) }}",
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def test_render_str_missing_configuration(self) -> None:
|
2019-05-11 17:31:18 +00:00
|
|
|
self.assertRaises(exceptions.TutorError, env.render_str, {}, "hello {{ name }}")
|
2019-05-11 22:11:44 +00:00
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def test_render_file(self) -> None:
|
2021-04-06 10:09:00 +00:00
|
|
|
config: Config = {}
|
2021-11-08 13:46:38 +00:00
|
|
|
tutor_config.update_with_base(config)
|
|
|
|
tutor_config.update_with_defaults(config)
|
|
|
|
tutor_config.render_full(config)
|
|
|
|
|
2019-05-11 22:11:44 +00:00
|
|
|
config["MYSQL_ROOT_PASSWORD"] = "testpassword"
|
2022-10-19 15:46:31 +00:00
|
|
|
rendered = env.render_file(config, "jobs", "init", "mysql.sh")
|
2019-05-11 22:11:44 +00:00
|
|
|
self.assertIn("testpassword", rendered)
|
|
|
|
|
feat: strongly typed hooks
Now that the mypy bugs have been resolved, we are able to define more precisely
and cleanly the types of Actions and Filters.
Moreover, can now strongly type named actions and hooks (in consts.py). With
such a strong typing, we get early alerts of hooks called with incorrect
arguments, which is nothing short of awesome :)
This change breaks the hooks API by removing the `context=...` argument. The
reason for that is that we cannot insert arbitrary arguments between `P.args,
P.kwargs`: https://peps.python.org/pep-0612/#the-components-of-a-paramspec
> A function declared as def inner(a: A, b: B, *args: P.args, **kwargs:
> P.kwargs) -> R has type Callable[Concatenate[A, B, P], R]. Placing
> keyword-only parameters between the *args and **kwargs is forbidden.
Getting the documentation to build in nitpicky mode is quite difficult... We
need to add `nitpick_ignore` to the docs conf.py, otherwise sphinx complains
about many missing class references. This, despite upgrading almost all doc
requirements (except docutils).
2022-10-06 10:05:01 +00:00
|
|
|
@patch.object(fmt, "echo")
|
2021-02-25 08:09:14 +00:00
|
|
|
def test_render_file_missing_configuration(self, _: Mock) -> None:
|
2019-05-11 22:11:44 +00:00
|
|
|
self.assertRaises(
|
|
|
|
exceptions.TutorError, env.render_file, {}, "local", "docker-compose.yml"
|
|
|
|
)
|
2019-05-20 17:09:58 +00:00
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def test_save_full(self) -> None:
|
2022-01-08 11:19:46 +00:00
|
|
|
with temporary_root() as root:
|
2021-11-08 13:46:38 +00:00
|
|
|
config = tutor_config.load_full(root)
|
2021-02-25 08:09:14 +00:00
|
|
|
with patch.object(fmt, "STDOUT"):
|
2019-12-24 16:22:12 +00:00
|
|
|
env.save(root, config)
|
2019-07-02 20:16:44 +00:00
|
|
|
self.assertTrue(
|
2022-01-08 11:19:46 +00:00
|
|
|
os.path.exists(
|
|
|
|
os.path.join(env.base_dir(root), "local", "docker-compose.yml")
|
|
|
|
)
|
2019-07-02 20:16:44 +00:00
|
|
|
)
|
2019-05-21 10:34:29 +00:00
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def test_save_full_with_https(self) -> None:
|
2022-01-08 11:19:46 +00:00
|
|
|
with temporary_root() as root:
|
2021-11-08 13:46:38 +00:00
|
|
|
config = tutor_config.load_full(root)
|
2020-09-17 10:53:14 +00:00
|
|
|
config["ENABLE_HTTPS"] = True
|
2021-02-25 08:09:14 +00:00
|
|
|
with patch.object(fmt, "STDOUT"):
|
2019-12-24 16:22:12 +00:00
|
|
|
env.save(root, config)
|
2022-01-08 11:19:46 +00:00
|
|
|
with open(
|
|
|
|
os.path.join(env.base_dir(root), "apps", "caddy", "Caddyfile"),
|
|
|
|
encoding="utf-8",
|
|
|
|
) as f:
|
2021-11-09 10:42:39 +00:00
|
|
|
self.assertIn("www.myopenedx.com{$default_site_port}", f.read())
|
2019-05-29 09:14:06 +00:00
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def test_patch(self) -> None:
|
2019-05-29 09:14:06 +00:00
|
|
|
patches = {"plugin1": "abcd", "plugin2": "efgh"}
|
2021-02-25 08:09:14 +00:00
|
|
|
with patch.object(
|
feat: strongly typed hooks
Now that the mypy bugs have been resolved, we are able to define more precisely
and cleanly the types of Actions and Filters.
Moreover, can now strongly type named actions and hooks (in consts.py). With
such a strong typing, we get early alerts of hooks called with incorrect
arguments, which is nothing short of awesome :)
This change breaks the hooks API by removing the `context=...` argument. The
reason for that is that we cannot insert arbitrary arguments between `P.args,
P.kwargs`: https://peps.python.org/pep-0612/#the-components-of-a-paramspec
> A function declared as def inner(a: A, b: B, *args: P.args, **kwargs:
> P.kwargs) -> R has type Callable[Concatenate[A, B, P], R]. Placing
> keyword-only parameters between the *args and **kwargs is forbidden.
Getting the documentation to build in nitpicky mode is quite difficult... We
need to add `nitpick_ignore` to the docs conf.py, otherwise sphinx complains
about many missing class references. This, despite upgrading almost all doc
requirements (except docutils).
2022-10-06 10:05:01 +00:00
|
|
|
plugins, "iter_patches", return_value=patches.values()
|
2019-05-29 09:14:06 +00:00
|
|
|
) as mock_iter_patches:
|
|
|
|
rendered = env.render_str({}, '{{ patch("location") }}')
|
2022-02-07 17:11:43 +00:00
|
|
|
mock_iter_patches.assert_called_once_with("location")
|
2019-05-29 09:14:06 +00:00
|
|
|
self.assertEqual("abcd\nefgh", rendered)
|
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def test_patch_separator_suffix(self) -> None:
|
2019-05-29 09:14:06 +00:00
|
|
|
patches = {"plugin1": "abcd", "plugin2": "efgh"}
|
feat: strongly typed hooks
Now that the mypy bugs have been resolved, we are able to define more precisely
and cleanly the types of Actions and Filters.
Moreover, can now strongly type named actions and hooks (in consts.py). With
such a strong typing, we get early alerts of hooks called with incorrect
arguments, which is nothing short of awesome :)
This change breaks the hooks API by removing the `context=...` argument. The
reason for that is that we cannot insert arbitrary arguments between `P.args,
P.kwargs`: https://peps.python.org/pep-0612/#the-components-of-a-paramspec
> A function declared as def inner(a: A, b: B, *args: P.args, **kwargs:
> P.kwargs) -> R has type Callable[Concatenate[A, B, P], R]. Placing
> keyword-only parameters between the *args and **kwargs is forbidden.
Getting the documentation to build in nitpicky mode is quite difficult... We
need to add `nitpick_ignore` to the docs conf.py, otherwise sphinx complains
about many missing class references. This, despite upgrading almost all doc
requirements (except docutils).
2022-10-06 10:05:01 +00:00
|
|
|
with patch.object(plugins, "iter_patches", return_value=patches.values()):
|
2019-05-29 09:14:06 +00:00
|
|
|
rendered = env.render_str(
|
|
|
|
{}, '{{ patch("location", separator=",\n", suffix=",") }}'
|
|
|
|
)
|
|
|
|
self.assertEqual("abcd,\nefgh,", rendered)
|
2019-07-02 20:16:44 +00:00
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def test_plugin_templates(self) -> None:
|
2019-07-02 20:16:44 +00:00
|
|
|
with tempfile.TemporaryDirectory() as plugin_templates:
|
2022-02-07 17:11:43 +00:00
|
|
|
DictPlugin(
|
2020-01-16 10:52:53 +00:00
|
|
|
{"name": "plugin1", "version": "0", "templates": plugin_templates}
|
|
|
|
)
|
2019-07-02 20:16:44 +00:00
|
|
|
# Create two templates
|
|
|
|
os.makedirs(os.path.join(plugin_templates, "plugin1", "apps"))
|
|
|
|
with open(
|
2022-01-08 11:19:46 +00:00
|
|
|
os.path.join(plugin_templates, "plugin1", "unrendered.txt"),
|
|
|
|
"w",
|
|
|
|
encoding="utf-8",
|
2019-07-02 20:16:44 +00:00
|
|
|
) as f:
|
|
|
|
f.write("This file should not be rendered")
|
|
|
|
with open(
|
2022-01-08 11:19:46 +00:00
|
|
|
os.path.join(plugin_templates, "plugin1", "apps", "rendered.txt"),
|
|
|
|
"w",
|
|
|
|
encoding="utf-8",
|
2019-07-02 20:16:44 +00:00
|
|
|
) as f:
|
|
|
|
f.write("Hello my ID is {{ ID }}")
|
|
|
|
|
2020-01-16 10:52:53 +00:00
|
|
|
# Render templates
|
2022-02-07 17:11:43 +00:00
|
|
|
with temporary_root() as root:
|
|
|
|
# Create configuration
|
|
|
|
config: Config = tutor_config.load_full(root)
|
|
|
|
config["ID"] = "Hector Rumblethorpe"
|
|
|
|
plugins.load("plugin1")
|
|
|
|
tutor_config.save_enabled_plugins(config)
|
|
|
|
|
|
|
|
# Render environment
|
|
|
|
with patch.object(fmt, "STDOUT"):
|
|
|
|
env.save(root, config)
|
|
|
|
|
|
|
|
# Check that plugin template was rendered
|
|
|
|
root_env = os.path.join(root, "env")
|
|
|
|
dst_unrendered = os.path.join(
|
|
|
|
root_env, "plugins", "plugin1", "unrendered.txt"
|
|
|
|
)
|
|
|
|
dst_rendered = os.path.join(
|
|
|
|
root_env, "plugins", "plugin1", "apps", "rendered.txt"
|
|
|
|
)
|
|
|
|
self.assertFalse(os.path.exists(dst_unrendered))
|
|
|
|
self.assertTrue(os.path.exists(dst_rendered))
|
|
|
|
with open(dst_rendered, encoding="utf-8") as f:
|
|
|
|
self.assertEqual("Hello my ID is Hector Rumblethorpe", f.read())
|
2019-07-10 08:20:43 +00:00
|
|
|
|
2021-02-25 08:09:14 +00:00
|
|
|
def test_renderer_is_reset_on_config_change(self) -> None:
|
2019-08-20 15:46:53 +00:00
|
|
|
with tempfile.TemporaryDirectory() as plugin_templates:
|
2022-02-07 17:11:43 +00:00
|
|
|
plugin1 = DictPlugin(
|
2020-01-16 10:52:53 +00:00
|
|
|
{"name": "plugin1", "version": "0", "templates": plugin_templates}
|
|
|
|
)
|
2022-02-07 17:11:43 +00:00
|
|
|
|
2019-08-20 15:46:53 +00:00
|
|
|
# Create one template
|
2020-01-16 10:52:53 +00:00
|
|
|
os.makedirs(os.path.join(plugin_templates, plugin1.name))
|
|
|
|
with open(
|
2022-01-08 11:19:46 +00:00
|
|
|
os.path.join(plugin_templates, plugin1.name, "myplugin.txt"),
|
|
|
|
"w",
|
|
|
|
encoding="utf-8",
|
2020-01-16 10:52:53 +00:00
|
|
|
) as f:
|
2019-08-20 15:46:53 +00:00
|
|
|
f.write("some content")
|
|
|
|
|
2020-01-16 10:52:53 +00:00
|
|
|
# Load env once
|
2021-04-06 10:09:00 +00:00
|
|
|
config: Config = {"PLUGINS": []}
|
2022-05-30 16:12:00 +00:00
|
|
|
env1 = env.Renderer(config).environment
|
2020-01-16 10:52:53 +00:00
|
|
|
|
2022-02-07 17:11:43 +00:00
|
|
|
# Enable plugins
|
|
|
|
plugins.load("plugin1")
|
|
|
|
|
|
|
|
# Load env a second time
|
|
|
|
config["PLUGINS"] = ["myplugin"]
|
2022-05-30 16:12:00 +00:00
|
|
|
env2 = env.Renderer(config).environment
|
2020-01-16 10:52:53 +00:00
|
|
|
|
2021-06-02 13:16:32 +00:00
|
|
|
self.assertNotIn("plugin1/myplugin.txt", env1.loader.list_templates())
|
|
|
|
self.assertIn("plugin1/myplugin.txt", env2.loader.list_templates())
|
2021-04-13 20:14:43 +00:00
|
|
|
|
|
|
|
def test_iter_values_named(self) -> None:
|
|
|
|
config: Config = {
|
|
|
|
"something0_test_app": 0,
|
|
|
|
"something1_test_not_app": 1,
|
|
|
|
"notsomething_test_app": 2,
|
|
|
|
"something3_test_app": 3,
|
|
|
|
}
|
2022-05-30 16:12:00 +00:00
|
|
|
renderer = env.Renderer(config)
|
2021-04-13 20:14:43 +00:00
|
|
|
self.assertEqual([2, 3], list(renderer.iter_values_named(suffix="test_app")))
|
|
|
|
self.assertEqual([1, 3], list(renderer.iter_values_named(prefix="something")))
|
|
|
|
self.assertEqual(
|
|
|
|
[0, 3],
|
|
|
|
list(
|
|
|
|
renderer.iter_values_named(
|
|
|
|
prefix="something", suffix="test_app", allow_empty=True
|
|
|
|
)
|
|
|
|
),
|
|
|
|
)
|
2022-01-08 11:19:46 +00:00
|
|
|
|
2022-01-08 10:24:50 +00:00
|
|
|
|
|
|
|
class CurrentVersionTests(unittest.TestCase):
|
2022-01-08 11:19:46 +00:00
|
|
|
def test_current_version_in_empty_env(self) -> None:
|
|
|
|
with temporary_root() as root:
|
|
|
|
self.assertIsNone(env.current_version(root))
|
2022-01-08 10:24:50 +00:00
|
|
|
self.assertIsNone(env.get_env_release(root))
|
|
|
|
self.assertIsNone(env.should_upgrade_from_release(root))
|
2022-01-08 11:19:46 +00:00
|
|
|
self.assertTrue(env.is_up_to_date(root))
|
|
|
|
|
|
|
|
def test_current_version_in_lilac_env(self) -> None:
|
|
|
|
with temporary_root() as root:
|
|
|
|
os.makedirs(env.base_dir(root))
|
|
|
|
with open(
|
|
|
|
os.path.join(env.base_dir(root), env.VERSION_FILENAME),
|
|
|
|
"w",
|
|
|
|
encoding="utf-8",
|
|
|
|
) as f:
|
|
|
|
f.write("12.0.46")
|
|
|
|
self.assertEqual("12.0.46", env.current_version(root))
|
2022-01-08 10:24:50 +00:00
|
|
|
self.assertEqual("lilac", env.get_env_release(root))
|
|
|
|
self.assertEqual("lilac", env.should_upgrade_from_release(root))
|
2022-01-08 11:19:46 +00:00
|
|
|
self.assertFalse(env.is_up_to_date(root))
|
|
|
|
|
|
|
|
def test_current_version_in_latest_env(self) -> None:
|
|
|
|
with temporary_root() as root:
|
|
|
|
os.makedirs(env.base_dir(root))
|
|
|
|
with open(
|
|
|
|
os.path.join(env.base_dir(root), env.VERSION_FILENAME),
|
|
|
|
"w",
|
|
|
|
encoding="utf-8",
|
|
|
|
) as f:
|
|
|
|
f.write(__version__)
|
|
|
|
self.assertEqual(__version__, env.current_version(root))
|
2023-04-12 08:35:00 +00:00
|
|
|
self.assertEqual("palm", env.get_env_release(root))
|
2022-01-08 10:24:50 +00:00
|
|
|
self.assertIsNone(env.should_upgrade_from_release(root))
|
2022-01-08 11:19:46 +00:00
|
|
|
self.assertTrue(env.is_up_to_date(root))
|
2023-02-08 04:35:25 +00:00
|
|
|
|
|
|
|
|
|
|
|
class PatchRendererTests(unittest.TestCase):
|
|
|
|
def setUp(self) -> None:
|
|
|
|
self.render = env.PatchRenderer()
|
|
|
|
self.render.current_template = "current_template"
|
|
|
|
return super().setUp()
|
|
|
|
|
|
|
|
@patch("tutor.env.Renderer.render_template")
|
|
|
|
def test_render_template(self, render_template_mock: Mock) -> None:
|
|
|
|
"""Test that render_template changes the current template and
|
|
|
|
calls once render_template from Renderer with the current template."""
|
|
|
|
self.render.render_template("new_template")
|
|
|
|
|
|
|
|
self.assertEqual(self.render.current_template, "new_template")
|
|
|
|
render_template_mock.assert_called_once_with("new_template")
|
|
|
|
|
|
|
|
@patch("tutor.env.Renderer.patch")
|
|
|
|
def test_patch_with_first_patch(self, patch_mock: Mock) -> None:
|
|
|
|
"""Test that patch is called from Renderer and adds patches_locations
|
|
|
|
when we didn't have that patch."""
|
|
|
|
self.render.patches_locations = {}
|
|
|
|
|
|
|
|
self.render.patch("first_patch")
|
|
|
|
|
|
|
|
patch_mock.assert_called_once_with("first_patch", separator="\n", suffix="")
|
|
|
|
self.assertEqual(
|
|
|
|
self.render.patches_locations,
|
|
|
|
{"first_patch": [self.render.current_template]},
|
|
|
|
)
|
|
|
|
|
|
|
|
def test_patch_with_patch_multiple_locations(self) -> None:
|
|
|
|
"""Test add more locations to a patch."""
|
|
|
|
self.render.patches_locations = {"first_patch": ["template_1"]}
|
|
|
|
|
|
|
|
self.render.patch("first_patch")
|
|
|
|
|
|
|
|
self.assertEqual(
|
|
|
|
self.render.patches_locations,
|
|
|
|
{"first_patch": ["template_1", "current_template"]},
|
|
|
|
)
|
|
|
|
|
|
|
|
@patch("tutor.env.plugins.iter_patches")
|
|
|
|
def test_patch_with_custom_patch_in_a_plugin_patch(
|
|
|
|
self, iter_patches_mock: Mock
|
|
|
|
) -> None:
|
|
|
|
"""Test the patch function with a plugin with a custom patch.
|
|
|
|
Examples:
|
|
|
|
- When first_patch is in a plugin patches and has a 'custom_patch',
|
|
|
|
the patches_locations will reflect that 'custom_patch' is from
|
|
|
|
first_patch location.
|
|
|
|
- If in tutor-mfe/tutormfe/patches/caddyfile you add a custom patch
|
|
|
|
inside the caddyfile patch, the patches_locations will reflect that.
|
|
|
|
|
|
|
|
Expected behavior:
|
|
|
|
- Process the first_patch and find the custom_patch in a plugin with
|
|
|
|
first_patch patch.
|
|
|
|
- Process the custom_patch and add "within patch: first_patch" in the
|
|
|
|
patches_locations."""
|
|
|
|
iter_patches_mock.side_effect = [
|
|
|
|
["""{{ patch('custom_patch')|indent(4) }}"""],
|
|
|
|
[],
|
|
|
|
]
|
|
|
|
self.render.patches_locations = {}
|
|
|
|
calls = [unittest.mock.call("first_patch"), unittest.mock.call("custom_patch")]
|
|
|
|
|
|
|
|
self.render.patch("first_patch")
|
|
|
|
|
|
|
|
iter_patches_mock.assert_has_calls(calls)
|
|
|
|
self.assertEqual(
|
|
|
|
self.render.patches_locations,
|
|
|
|
{
|
|
|
|
"first_patch": ["current_template"],
|
|
|
|
"custom_patch": ["within patch: first_patch"],
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
@patch("tutor.env.plugins.iter_patches")
|
|
|
|
def test_patch_with_processed_patch_in_a_plugin_patch(
|
|
|
|
self, iter_patches_mock: Mock
|
|
|
|
) -> None:
|
|
|
|
"""Test the patch function with a plugin with a processed patch.
|
|
|
|
Example:
|
|
|
|
- When first_patch was processed and the second_patch is used in a
|
|
|
|
plugin and call the first_patch again. Then the patches_locations will
|
|
|
|
reflect that first_patch also have a location from second_patch."""
|
|
|
|
iter_patches_mock.side_effect = [
|
|
|
|
["""{{ patch('first_patch')|indent(4) }}"""],
|
|
|
|
[],
|
|
|
|
]
|
|
|
|
self.render.patches_locations = {"first_patch": ["current_template"]}
|
|
|
|
|
|
|
|
self.render.patch("second_patch")
|
|
|
|
|
|
|
|
self.assertEqual(
|
|
|
|
self.render.patches_locations,
|
|
|
|
{
|
|
|
|
"first_patch": ["current_template", "within patch: second_patch"],
|
|
|
|
"second_patch": ["current_template"],
|
|
|
|
},
|
|
|
|
)
|
|
|
|
|
|
|
|
@patch("tutor.env.Renderer.iter_templates_in")
|
|
|
|
@patch("tutor.env.PatchRenderer.render_template")
|
|
|
|
def test_render_all(
|
|
|
|
self, render_template_mock: Mock, iter_templates_in_mock: Mock
|
|
|
|
) -> None:
|
|
|
|
"""Test render_template was called for templates in iter_templates_in."""
|
|
|
|
iter_templates_in_mock.return_value = ["template_1", "template_2"]
|
|
|
|
calls = [unittest.mock.call("template_1"), unittest.mock.call("template_2")]
|
|
|
|
|
|
|
|
self.render.render_all()
|
|
|
|
|
|
|
|
iter_templates_in_mock.assert_called_once()
|
|
|
|
render_template_mock.assert_has_calls(calls)
|
|
|
|
|
|
|
|
@patch("sys.stdout", new_callable=StringIO)
|
|
|
|
@patch("tutor.env.PatchRenderer.render_all")
|
|
|
|
def test_print_patches_locations(
|
|
|
|
self, render_all_mock: Mock, stdout_mock: Mock
|
|
|
|
) -> None:
|
|
|
|
"""Test render_all was called and the output of print_patches_locations."""
|
|
|
|
self.render.patches_locations = {"first_patch": ["template_1", "template_2"]}
|
|
|
|
|
|
|
|
self.render.print_patches_locations()
|
|
|
|
|
|
|
|
render_all_mock.assert_called_once()
|
|
|
|
self.assertEqual(
|
|
|
|
"""
|
|
|
|
PATCH LOCATIONS
|
|
|
|
first_patch template_1
|
|
|
|
template_2
|
|
|
|
""".strip(),
|
|
|
|
stdout_mock.getvalue().strip(),
|
|
|
|
)
|