def test_configure(self) -> None: plugins_v0.DictPlugin({ "name": "plugin1", "config": { "add": { "PARAM1": "value1", "PARAM2": "value2" }, "set": { "PARAM3": "value3" }, "defaults": { "PARAM4": "value4" }, }, }) plugins.load("plugin1") base = tutor_config.get_base() defaults = tutor_config.get_defaults() self.assertEqual(base["PARAM3"], "value3") self.assertEqual(base["PLUGIN1_PARAM1"], "value1") self.assertEqual(base["PLUGIN1_PARAM2"], "value2") self.assertEqual(defaults["PLUGIN1_PARAM4"], "value4")
def test_enable_twice(self) -> None: plugins_v0.DictPlugin({"name": "plugin1"}) plugins.load("plugin1") plugins.load("plugin1") config: Config = {tutor_config.PLUGINS_CONFIG_KEY: []} tutor_config.save_enabled_plugins(config) self.assertEqual(["plugin1"], config[tutor_config.PLUGINS_CONFIG_KEY])
def test_renderer_is_reset_on_config_change(self) -> None: with tempfile.TemporaryDirectory() as plugin_templates: plugin1 = DictPlugin({ "name": "plugin1", "version": "0", "templates": plugin_templates }) # Create one template os.makedirs(os.path.join(plugin_templates, plugin1.name)) with open( os.path.join(plugin_templates, plugin1.name, "myplugin.txt"), "w", encoding="utf-8", ) as f: f.write("some content") # Load env once config: Config = {"PLUGINS": []} env1 = env.Renderer(config).environment # Enable plugins plugins.load("plugin1") # Load env a second time config["PLUGINS"] = ["myplugin"] env2 = env.Renderer(config).environment self.assertNotIn("plugin1/myplugin.txt", env1.loader.list_templates()) self.assertIn("plugin1/myplugin.txt", env2.loader.list_templates())
def upgrade_from_lilac(config: Config) -> None: if not plugins.is_installed("forum"): fmt.echo_alert( "The Open edX forum feature was moved to a separate plugin in Maple. To keep using this feature, " "you must install and enable the tutor-forum plugin: https://github.com/overhangio/tutor-forum" ) elif not plugins.is_loaded("forum"): fmt.echo_info( "The Open edX forum feature was moved to a separate plugin in Maple. To keep using this feature, " "we will now enable the 'forum' plugin. If you do not want to use this feature, you should disable the " "plugin with: `tutor plugins disable forum`.") plugins.load("forum") tutor_config.save_enabled_plugins(config) if not plugins.is_installed("mfe"): fmt.echo_alert( "In Maple the legacy courseware is no longer supported. You need to install and enable the 'mfe' plugin " "to make use of the new learning microfrontend: https://github.com/overhangio/tutor-mfe" ) elif not plugins.is_loaded("mfe"): fmt.echo_info( "In Maple the legacy courseware is no longer supported. To start using the new learning microfrontend, " "we will now enable the 'mfe' plugin. If you do not want to use this feature, you should disable the " "plugin with: `tutor plugins disable mfe`.") plugins.load("mfe") tutor_config.save_enabled_plugins(config)
def test_config_disable_plugin(self) -> None: plugins_v0.DictPlugin({ "name": "plugin1", "config": { "set": { "KEY1": "value1" } } }) plugins_v0.DictPlugin({ "name": "plugin2", "config": { "set": { "KEY2": "value2" } } }) plugins.load("plugin1") plugins.load("plugin2") with temporary_root() as root: config = tutor_config.load_minimal(root) config_pre = config.copy() with patch.object(fmt, "STDOUT"): hooks.Actions.PLUGIN_UNLOADED.do("plugin1", "", config) config_post = tutor_config.load_minimal(root) self.assertEqual("value1", config_pre["KEY1"]) self.assertEqual("value2", config_pre["KEY2"]) self.assertNotIn("KEY1", config) self.assertNotIn("KEY1", config_post) self.assertEqual("value2", config["KEY2"])
def test_patches(self) -> None: plugins_v0.DictPlugin({ "name": "plugin1", "patches": { "patch1": "Hello {{ ID }}" } }) plugins.load("plugin1") patches = list(plugins.iter_patches("patch1")) self.assertEqual(["Hello {{ ID }}"], patches)
def enable(context: Context, plugin_names: t.List[str]) -> None: config = tutor_config.load_minimal(context.root) for plugin in plugin_names: plugins.load(plugin) fmt.echo_info(f"Plugin {plugin} enabled") tutor_config.save_enabled_plugins(config) tutor_config.save_config_file(context.root, config) fmt.echo_info( "You should now re-generate your environment with `tutor config save`." )
def test_init_tasks(self) -> None: plugins_v0.DictPlugin({ "name": "plugin1", "hooks": { "init": ["myclient"] } }) plugins.load("plugin1") self.assertIn( ("myclient", ("plugin1", "hooks", "myclient", "init")), list(hooks.Filters.COMMANDS_INIT.iterate()), )
def test_configure_default_value_with_previous_definition(self) -> None: config: Config = {"PARAM1": "value"} plugins_v0.DictPlugin({ "name": "plugin1", "config": { "defaults": { "PARAM2": "{{ PARAM1 }}" } } }) plugins.load("plugin1") tutor_config.update_with_defaults(config) self.assertEqual("{{ PARAM1 }}", config["PLUGIN1_PARAM2"])
def test_configure_set_random_string(self) -> None: plugins_v0.DictPlugin({ "name": "plugin1", "config": { "set": { "PARAM1": "{{ 128|random_string }}" } }, }) plugins.load("plugin1") config = tutor_config.get_base() tutor_config.render_full(config) self.assertEqual(128, len(get_typed(config, "PARAM1", str)))
def test_plugin_templates(self) -> None: with tempfile.TemporaryDirectory() as plugin_templates: DictPlugin({ "name": "plugin1", "version": "0", "templates": plugin_templates }) # Create two templates os.makedirs(os.path.join(plugin_templates, "plugin1", "apps")) with open( os.path.join(plugin_templates, "plugin1", "unrendered.txt"), "w", encoding="utf-8", ) as f: f.write("This file should not be rendered") with open( os.path.join(plugin_templates, "plugin1", "apps", "rendered.txt"), "w", encoding="utf-8", ) as f: f.write("Hello my ID is {{ ID }}") # Render templates 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())
def upgrade_obsolete(config: Config) -> None: # Openedx-specific mysql passwords if "MYSQL_PASSWORD" in config: config["MYSQL_ROOT_PASSWORD"] = config["MYSQL_PASSWORD"] config["OPENEDX_MYSQL_PASSWORD"] = config["MYSQL_PASSWORD"] config.pop("MYSQL_PASSWORD") if "MYSQL_DATABASE" in config: config["OPENEDX_MYSQL_DATABASE"] = config.pop("MYSQL_DATABASE") if "MYSQL_USERNAME" in config: config["OPENEDX_MYSQL_USERNAME"] = config.pop("MYSQL_USERNAME") if "RUN_NOTES" in config: if config["RUN_NOTES"]: plugins.load("notes") save_enabled_plugins(config) config.pop("RUN_NOTES") if "RUN_XQUEUE" in config: if config["RUN_XQUEUE"]: plugins.load("xqueue") save_enabled_plugins(config) config.pop("RUN_XQUEUE") if "SECRET_KEY" in config: config["OPENEDX_SECRET_KEY"] = config.pop("SECRET_KEY") # Replace WEB_PROXY by RUN_CADDY if "WEB_PROXY" in config: config["RUN_CADDY"] = not config.pop("WEB_PROXY") # Rename ACTIVATE_HTTPS to ENABLE_HTTPS if "ACTIVATE_HTTPS" in config: config["ENABLE_HTTPS"] = config.pop("ACTIVATE_HTTPS") # Replace RUN_* variables by RUN_* for name in [ "ACTIVATE_LMS", "ACTIVATE_CMS", "ACTIVATE_ELASTICSEARCH", "ACTIVATE_MONGODB", "ACTIVATE_MYSQL", "ACTIVATE_REDIS", "ACTIVATE_SMTP", ]: if name in config: config[name.replace("ACTIVATE_", "RUN_")] = config.pop(name) # Replace RUN_CADDY by ENABLE_WEB_PROXY if "RUN_CADDY" in config: config["ENABLE_WEB_PROXY"] = config.pop("RUN_CADDY") # Replace RUN_CADDY by ENABLE_WEB_PROXY if "NGINX_HTTP_PORT" in config: config["CADDY_HTTP_PORT"] = config.pop("NGINX_HTTP_PORT")
def test_images_pull_plugin(self, image_pull: Mock) -> None: plugins.v0.DictPlugin( { "name": "plugin1", "hooks": { "remote-image": { "service1": "service1:1.0.0", "service2": "service2:2.0.0", } }, } ) plugins.load("plugin1") result = self.invoke(["images", "pull", "service1"]) self.assertIsNone(result.exception) self.assertEqual(0, result.exit_code) image_pull.assert_called_once_with("service1:1.0.0")
def test_images_printtag_plugin(self) -> None: plugins.v0.DictPlugin( { "name": "plugin1", "hooks": { "build-image": { "service1": "service1:1.0.0", "service2": "service2:2.0.0", } }, } ) plugins.load("plugin1") result = self.invoke(["images", "printtag", "service1"]) self.assertIsNone(result.exception) self.assertEqual(0, result.exit_code, result) self.assertEqual(result.output, "service1:1.0.0\n")
def test_configure_set_does_not_override(self) -> None: config: Config = {"ID1": "oldid"} plugins_v0.DictPlugin({ "name": "plugin1", "config": { "set": { "ID1": "newid", "ID2": "id2" } } }) plugins.load("plugin1") tutor_config.update_with_base(config) self.assertEqual("oldid", config["ID1"]) self.assertEqual("id2", config["ID2"])
def test_dict_plugin(self) -> None: plugin = plugins_v0.DictPlugin({ "name": "myplugin", "config": { "set": { "KEY": "value" } }, "version": "0.1" }) plugins.load("myplugin") overriden_items: t.List[t.Tuple[ str, t.Any]] = hooks.Filters.CONFIG_OVERRIDES.apply([]) versions = list(plugins.iter_info()) self.assertEqual("myplugin", plugin.name) self.assertEqual([("myplugin", "0.1")], versions) self.assertEqual([("KEY", "value")], overriden_items)
def test_config_load_from_plugins(self) -> None: config: Config = {} plugins_v0.DictPlugin({ "name": "plugin1", "config": { "add": { "PARAM1": "{{ 10|random_string }}" } } }) plugins.load("plugin1") tutor_config.update_with_base(config) tutor_config.update_with_defaults(config) tutor_config.render_full(config) value1 = get_typed(config, "PLUGIN1_PARAM1", str) self.assertEqual(10, len(value1))
def test_images_build_plugin(self, mock_image_build: Mock) -> None: plugins.v0.DictPlugin( { "name": "plugin1", "hooks": { "build-image": { "service1": "service1:1.0.0", "service2": "service2:2.0.0", } }, } ) plugins.load("plugin1") with temporary_root() as root: self.invoke_in_root(root, ["config", "save"]) result = self.invoke_in_root(root, ["images", "build", "service1"]) self.assertIsNone(result.exception) self.assertEqual(0, result.exit_code) mock_image_build.assert_called() self.assertIn("service1:1.0.0", mock_image_build.call_args[0])
def test_images_build_plugin_with_args(self, image_build: Mock) -> None: plugins.v0.DictPlugin( { "name": "plugin1", "hooks": { "build-image": { "service1": "service1:1.0.0", "service2": "service2:2.0.0", } }, } ) plugins.load("plugin1") build_args = [ "images", "build", "--no-cache", "-a", "myarg=value", "--add-host", "host", "--target", "target", "-d", "docker_args", "service1", ] with temporary_root() as root: self.invoke_in_root(root, ["config", "save"]) result = self.invoke_in_root(root, build_args) self.assertIsNone(result.exception) self.assertEqual(0, result.exit_code) image_build.assert_called() self.assertIn("service1:1.0.0", image_build.call_args[0]) for arg in image_build.call_args[0][2:]: # The only extra args are `--build-arg` if arg != "--build-arg": self.assertIn(arg, build_args)
def test_plugin_without_patches(self) -> None: plugins_v0.DictPlugin({"name": "plugin1"}) plugins.load("plugin1") patches = list(plugins.iter_patches("patch1")) self.assertEqual([], patches)