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
|
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-05-30 16:12:00 +00:00
|
|
|
renderer = env.Renderer()
|
2020-11-07 15:37:43 +00:00
|
|
|
path = renderer.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"}))
|
|
|
|
|
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"
|
2019-09-03 08:36:10 +00:00
|
|
|
rendered = env.render_file(config, "hooks", "mysql", "init")
|
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))
|
2022-04-11 15:26:17 +00:00
|
|
|
self.assertEqual("nutmeg", 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))
|