From 1ef4d6521099880cac1c6159e81d7c9d01f193e1 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Wed, 27 May 2026 23:53:48 +0000 Subject: [PATCH] CodeRabbit Generated Unit Tests: Add generated unit tests --- tests/__init__.py | 0 tests/__pycache__/__init__.cpython-311.pyc | 0 .../conftest.cpython-311-pytest-9.0.3.pyc | 0 ...ild_templates.cpython-311-pytest-9.0.3.pyc | 0 .../test_common.cpython-311-pytest-9.0.3.pyc | 0 ...reter_helpers.cpython-311-pytest-9.0.3.pyc | 0 ...compile_types.cpython-311-pytest-9.0.3.pyc | 0 ...ersion_config.cpython-311-pytest-9.0.3.pyc | 0 tests/conftest.py | 60 ++ tests/test_build_templates.py | 222 +++++++ tests/test_common.py | 88 +++ tests/test_compile_interpreter_helpers.py | 127 ++++ tests/test_compile_types.py | 347 +++++++++++ tests/test_version_config.py | 574 ++++++++++++++++++ 14 files changed, 1418 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/__pycache__/__init__.cpython-311.pyc create mode 100644 tests/__pycache__/conftest.cpython-311-pytest-9.0.3.pyc create mode 100644 tests/__pycache__/test_build_templates.cpython-311-pytest-9.0.3.pyc create mode 100644 tests/__pycache__/test_common.cpython-311-pytest-9.0.3.pyc create mode 100644 tests/__pycache__/test_compile_interpreter_helpers.cpython-311-pytest-9.0.3.pyc create mode 100644 tests/__pycache__/test_compile_types.cpython-311-pytest-9.0.3.pyc create mode 100644 tests/__pycache__/test_version_config.cpython-311-pytest-9.0.3.pyc create mode 100644 tests/conftest.py create mode 100644 tests/test_build_templates.py create mode 100644 tests/test_common.py create mode 100644 tests/test_compile_interpreter_helpers.py create mode 100644 tests/test_compile_types.py create mode 100644 tests/test_version_config.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/__pycache__/__init__.cpython-311.pyc b/tests/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..e69de29 diff --git a/tests/__pycache__/conftest.cpython-311-pytest-9.0.3.pyc b/tests/__pycache__/conftest.cpython-311-pytest-9.0.3.pyc new file mode 100644 index 0000000..e69de29 diff --git a/tests/__pycache__/test_build_templates.cpython-311-pytest-9.0.3.pyc b/tests/__pycache__/test_build_templates.cpython-311-pytest-9.0.3.pyc new file mode 100644 index 0000000..e69de29 diff --git a/tests/__pycache__/test_common.cpython-311-pytest-9.0.3.pyc b/tests/__pycache__/test_common.cpython-311-pytest-9.0.3.pyc new file mode 100644 index 0000000..e69de29 diff --git a/tests/__pycache__/test_compile_interpreter_helpers.cpython-311-pytest-9.0.3.pyc b/tests/__pycache__/test_compile_interpreter_helpers.cpython-311-pytest-9.0.3.pyc new file mode 100644 index 0000000..e69de29 diff --git a/tests/__pycache__/test_compile_types.cpython-311-pytest-9.0.3.pyc b/tests/__pycache__/test_compile_types.cpython-311-pytest-9.0.3.pyc new file mode 100644 index 0000000..e69de29 diff --git a/tests/__pycache__/test_version_config.cpython-311-pytest-9.0.3.pyc b/tests/__pycache__/test_version_config.cpython-311-pytest-9.0.3.pyc new file mode 100644 index 0000000..e69de29 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..a36551c --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,60 @@ +"""Shared pytest fixtures for Minecraft-Script tests.""" +import json +import pytest +from pathlib import Path + +import minecraft_script.version_config as vc + + +MINIMAL_PROFILE = { + "minecraft_version": "0.0.test", + "pack_format": 99, + "paths": { + "function_dir": "functions", + "function_tag_dir": "functions", + }, + "constants": { + "testConst": "hello", + }, + "templates": { + "simple": "hello {{name}}", + "multiline": "line1\nline2 {{value}}\nline3", + "no_params": "static text", + "with_const": "{{testConst}} world", + "datapack_ref": "function {{datapack_id}}:something", + "conditional": "{{#if flag}}yes{{else}}no{{/if}}", + "literal.save": "data modify storage {{storage}} {{nbt}} set value {{value}}", + "literal.delete": "data remove storage {{storage}} {{nbt}}", + "literal.set_to_current": "data modify storage {{destStorage}} current set from storage {{storage}} {{nbt}}", + "literal.list.length": "data modify storage {{storage}} {{nbt}}.length set value {{length}}", + "literal.list.element": "data modify storage {{storage}} {{nbt}}.{{index}} set from storage {{srcStorage}} current", + }, +} + + +@pytest.fixture(autouse=False) +def version_context(): + """Initialize and clean up a version context using the real 1.20.4 profile.""" + ctx = vc.init_version_context("test_pack") + yield ctx + vc.clear_version_context() + + +@pytest.fixture +def minimal_version_context(): + """Initialize and clean up a version context using a minimal test profile.""" + vc._profile_cache["0.0.test"] = MINIMAL_PROFILE + ctx = vc.VersionContext("test_pack", profile=MINIMAL_PROFILE) + # Set as global context + vc._version_ctx = ctx + yield ctx + vc.clear_version_context() + vc._profile_cache.pop("0.0.test", None) + + +@pytest.fixture(autouse=True) +def clear_version_ctx(): + """Always ensure the version context is cleared between tests.""" + yield + vc.clear_version_context() + vc._profile_cache.clear() \ No newline at end of file diff --git a/tests/test_build_templates.py b/tests/test_build_templates.py new file mode 100644 index 0000000..33fdf48 --- /dev/null +++ b/tests/test_build_templates.py @@ -0,0 +1,222 @@ +"""Tests for the versioned build template files added/moved in this PR. + +Covers: +- minecraft_script/compiler/build_templates/builtins/1.20.4/*.mcfunction +- minecraft_script/compiler/build_templates/math/1.20.4/*.mcfunction +- minecraft_script/compiler/build_templates/tags/1.20.4/block/no_collision.json +""" +import json +import os +from pathlib import Path + +import minecraft_script.version_config as vc + +TEMPLATES_BASE = Path(__file__).resolve().parents[1] / "minecraft_script" / "compiler" / "build_templates" +BUILTINS_1204 = TEMPLATES_BASE / "builtins" / "1.20.4" +MATH_1204 = TEMPLATES_BASE / "math" / "1.20.4" +TAGS_1204 = TEMPLATES_BASE / "tags" / "1.20.4" + + +# =========================================================================== +# Directory structure tests +# =========================================================================== + +class TestVersionedDirectoryStructure: + def test_builtins_1204_dir_exists(self): + assert BUILTINS_1204.is_dir() + + def test_math_1204_dir_exists(self): + assert MATH_1204.is_dir() + + def test_tags_1204_dir_exists(self): + assert TAGS_1204.is_dir() + + def test_old_unversioned_builtins_dir_gone(self): + old_path = TEMPLATES_BASE / "builtins" / "give_item.mcfunction" + # The old file should no longer exist at unversioned path + assert not old_path.is_file() + + def test_old_unversioned_math_dir_gone(self): + # Old path was math/add.mcfunction — should not exist + old_path = TEMPLATES_BASE / "math" / "add.mcfunction" + assert not old_path.is_file() + + +# =========================================================================== +# give_item.mcfunction — key PR change (format changed) +# =========================================================================== + +class TestGiveItemMcfunction: + def setup_method(self): + self.path = BUILTINS_1204 / "give_item.mcfunction" + + def test_file_exists(self): + assert self.path.is_file() + + def test_file_starts_with_macro_prefix(self): + content = self.path.read_text(encoding="utf-8") + assert content.startswith("$give @s") + + def test_file_has_components_without_brackets(self): + """New format uses $(components) directly without square brackets. + + Old format: $give @s $(item)[$(components)] $(count) + New format: $give @s $(item)$(components) $(count) + """ + content = self.path.read_text(encoding="utf-8").strip() + assert "$(components)" in content + # The old format wrapped components in square brackets + assert "[$(components)]" not in content + + def test_file_has_item_placeholder(self): + content = self.path.read_text(encoding="utf-8") + assert "$(item)" in content + + def test_file_has_count_placeholder(self): + content = self.path.read_text(encoding="utf-8") + assert "$(count)" in content + + def test_file_exact_content(self): + content = self.path.read_text(encoding="utf-8").strip() + assert content == "$give @s $(item)$(components) $(count)" + + +# =========================================================================== +# Other builtin mcfunction files +# =========================================================================== + +class TestBuiltinMcfunctions: + EXPECTED_FILES = ["command.mcfunction", "give_item.mcfunction", "log.mcfunction", "set_block.mcfunction"] + + def test_all_expected_files_present(self): + for fname in self.EXPECTED_FILES: + assert (BUILTINS_1204 / fname).is_file(), f"Missing: {fname}" + + def test_command_mcfunction_not_empty(self): + content = (BUILTINS_1204 / "command.mcfunction").read_text(encoding="utf-8") + assert len(content.strip()) > 0 + + def test_log_mcfunction_not_empty(self): + content = (BUILTINS_1204 / "log.mcfunction").read_text(encoding="utf-8") + assert len(content.strip()) > 0 + + def test_set_block_mcfunction_contains_setblock_command(self): + content = (BUILTINS_1204 / "set_block.mcfunction").read_text(encoding="utf-8") + assert "setblock" in content.lower() or "$(block)" in content + + +# =========================================================================== +# Math mcfunction files +# =========================================================================== + +class TestMathMcfunctions: + EXPECTED_MATH_OPS = [ + "add", "subtract", "multiply", "divide", "modulus", + "equals", "greater_than", "greater_equals_than", + "less_than", "less_equals_than", + "u_add", "u_subtract", "u_not", + ] + + def test_all_math_files_present(self): + for op in self.EXPECTED_MATH_OPS: + path = MATH_1204 / f"{op}.mcfunction" + assert path.is_file(), f"Missing math template: {op}.mcfunction" + + def test_add_uses_scoreboard_operation(self): + content = (MATH_1204 / "add.mcfunction").read_text(encoding="utf-8") + assert "scoreboard players operation" in content + + def test_add_uses_out_scoreboard(self): + content = (MATH_1204 / "add.mcfunction").read_text(encoding="utf-8") + assert ".out mcs_math" in content + + def test_add_uses_a_and_b_inputs(self): + content = (MATH_1204 / "add.mcfunction").read_text(encoding="utf-8") + assert ".a mcs_math" in content + assert ".b mcs_math" in content + + def test_subtract_produces_subtraction(self): + content = (MATH_1204 / "subtract.mcfunction").read_text(encoding="utf-8") + assert "-=" in content + + def test_multiply_produces_multiplication(self): + content = (MATH_1204 / "multiply.mcfunction").read_text(encoding="utf-8") + assert "*=" in content + + def test_divide_produces_division(self): + content = (MATH_1204 / "divide.mcfunction").read_text(encoding="utf-8") + assert "/=" in content + + def test_modulus_produces_modulo(self): + content = (MATH_1204 / "modulus.mcfunction").read_text(encoding="utf-8") + assert "%=" in content + + def test_equals_uses_scoreboard_comparison(self): + content = (MATH_1204 / "equals.mcfunction").read_text(encoding="utf-8") + assert "scoreboard" in content + + def test_u_not_contains_not_logic(self): + content = (MATH_1204 / "u_not.mcfunction").read_text(encoding="utf-8") + # u_not should flip a boolean value + assert "scoreboard" in content or "execute" in content + + def test_no_math_files_outside_versioned_folder(self): + """Ensure math files are not duplicated at the unversioned level.""" + for op in self.EXPECTED_MATH_OPS: + old_path = TEMPLATES_BASE / "math" / f"{op}.mcfunction" + assert not old_path.is_file(), f"Unexpected file at unversioned path: {old_path}" + + +# =========================================================================== +# Tags — no_collision.json +# =========================================================================== + +class TestNoCollisionTag: + def setup_method(self): + self.path = TAGS_1204 / "block" / "no_collision.json" + + def test_file_exists(self): + assert self.path.is_file() + + def test_file_is_valid_json(self): + content = self.path.read_text(encoding="utf-8") + data = json.loads(content) + assert isinstance(data, dict) + + def test_json_has_values_key(self): + data = json.loads(self.path.read_text(encoding="utf-8")) + assert "values" in data + + def test_values_is_list(self): + data = json.loads(self.path.read_text(encoding="utf-8")) + assert isinstance(data["values"], list) + + def test_includes_minecraft_air(self): + data = json.loads(self.path.read_text(encoding="utf-8")) + assert "minecraft:air" in data["values"] + + def test_includes_void_air(self): + data = json.loads(self.path.read_text(encoding="utf-8")) + assert "minecraft:void_air" in data["values"] + + def test_old_unversioned_no_collision_gone(self): + old_path = TEMPLATES_BASE / "tags" / "block" / "no_collision.json" + assert not old_path.is_file() + + +# =========================================================================== +# predefined_root() points to versioned dirs (integration) +# =========================================================================== + +class TestPredefinedRootIntegration: + def test_math_predefined_root_contains_add_mcfunction(self): + root = vc.predefined_root("math", "1.20.4") + assert (Path(root) / "add.mcfunction").is_file() + + def test_builtins_predefined_root_contains_give_item_mcfunction(self): + root = vc.predefined_root("builtins", "1.20.4") + assert (Path(root) / "give_item.mcfunction").is_file() + + def test_tags_predefined_root_contains_no_collision_json(self): + root = vc.predefined_root("tags", "1.20.4") + assert (Path(root) / "block" / "no_collision.json").is_file() \ No newline at end of file diff --git a/tests/test_common.py b/tests/test_common.py new file mode 100644 index 0000000..61c4405 --- /dev/null +++ b/tests/test_common.py @@ -0,0 +1,88 @@ +"""Tests for minecraft_script/common.py changes in this PR. + +The PR replaced platform-specific module_folder detection with a +cross-platform pathlib-based approach. +""" +import os +from pathlib import Path + +import minecraft_script.common as common_module + + +class TestModuleFolder: + """Tests for the module_folder variable (changed from platform-specific to pathlib).""" + + def test_module_folder_is_string(self): + assert isinstance(common_module.module_folder, str) + + def test_module_folder_is_absolute_path(self): + assert os.path.isabs(common_module.module_folder) + + def test_module_folder_exists(self): + assert os.path.isdir(common_module.module_folder) + + def test_module_folder_contains_config_json(self): + config_path = os.path.join(common_module.module_folder, "config.json") + assert os.path.isfile(config_path) + + def test_module_folder_points_to_minecraft_script_package(self): + # The folder should be the minecraft_script package directory + folder_name = os.path.basename(common_module.module_folder) + assert folder_name == "minecraft_script" + + def test_module_folder_matches_pathlib_calculation(self): + # Verify the value matches what the new code computes + expected = str(Path(common_module.__file__).resolve().parent) + assert common_module.module_folder == expected + + def test_module_folder_does_not_contain_backslashes_on_non_windows(self): + # The old code joined with '/' explicitly; pathlib should give OS-native separator + # On Linux/macOS this means no backslashes + if os.name != "nt": + assert "\\" not in common_module.module_folder + + def test_module_folder_does_not_end_with_separator(self): + # Should not have trailing slash + assert not common_module.module_folder.endswith(os.sep) + assert not common_module.module_folder.endswith("/") + + +class TestCommonConfig: + """Tests that COMMON_CONFIG is loaded correctly using the new module_folder.""" + + def test_common_config_is_dict(self): + assert isinstance(common_module.COMMON_CONFIG, dict) + + def test_common_config_has_minecraft_version(self): + assert "minecraft_version" in common_module.COMMON_CONFIG + + def test_common_config_minecraft_version_is_string(self): + assert isinstance(common_module.COMMON_CONFIG["minecraft_version"], str) + + def test_common_config_has_debug_comments(self): + assert "debug_comments" in common_module.COMMON_CONFIG + + def test_common_config_has_verbose(self): + assert "verbose" in common_module.COMMON_CONFIG + + def test_common_config_has_default_output_path(self): + assert "default_output_path" in common_module.COMMON_CONFIG + + +class TestGenerateUuid: + """Tests for generate_uuid() (unchanged but used throughout the module).""" + + def test_returns_string(self): + uuid = common_module.generate_uuid() + assert isinstance(uuid, str) + + def test_returns_unique_values(self): + uuids = {common_module.generate_uuid() for _ in range(100)} + assert len(uuids) == 100 + + def test_uuid_format(self): + uuid = common_module.generate_uuid() + # UUID4 has 32 hex chars + 4 dashes = 36 chars total + assert len(uuid) == 36 + parts = uuid.split("-") + assert len(parts) == 5 \ No newline at end of file diff --git a/tests/test_compile_interpreter_helpers.py b/tests/test_compile_interpreter_helpers.py new file mode 100644 index 0000000..a8379bc --- /dev/null +++ b/tests/test_compile_interpreter_helpers.py @@ -0,0 +1,127 @@ +"""Tests for changes in minecraft_script/compiler/compile_interpreter.py. + +This PR changed: +- add_comment(): now preserves list->tuple conversion even when debug_comments=False, + and handles list input for the prefix case. +""" +import pytest +from unittest.mock import patch + +import minecraft_script.version_config as vc +from minecraft_script.common import COMMON_CONFIG + + +def _setup_version_ctx(): + """Helper: ensure a version context exists for tests that indirectly need it.""" + if vc._version_ctx is None: + vc.init_version_context("test_dp") + + +# --------------------------------------------------------------------------- +# Tests for add_comment() +# --------------------------------------------------------------------------- + +class TestAddComment: + """Tests for the add_comment() function changed in this PR.""" + + @pytest.fixture(autouse=True) + def enable_debug_comments(self): + original = COMMON_CONFIG.get("debug_comments") + COMMON_CONFIG["debug_comments"] = True + yield + COMMON_CONFIG["debug_comments"] = original + + def _get_add_comment(self): + from minecraft_script.compiler.compile_interpreter import add_comment + return add_comment + + # ---- debug_comments = True ---- + + def test_tuple_input_prepends_comment(self): + add_comment = self._get_add_comment() + result = add_comment(("cmd1", "cmd2"), "my comment") + assert result[0] == "\n# my comment" + assert "cmd1" in result + assert "cmd2" in result + + def test_string_input_prepends_comment(self): + add_comment = self._get_add_comment() + result = add_comment("single cmd", "my comment") + assert result.startswith("\n# my comment\n") + assert "single cmd" in result + + def test_list_input_becomes_tuple_with_comment(self): + add_comment = self._get_add_comment() + result = add_comment(["cmd1", "cmd2"], "a comment") + assert isinstance(result, tuple) + assert result[0] == "\n# a comment" + assert "cmd1" in result + assert "cmd2" in result + + def test_list_input_returns_tuple_not_list(self): + add_comment = self._get_add_comment() + result = add_comment(["a", "b"], "comment") + assert isinstance(result, tuple) + + # ---- debug_comments = False ---- + + def test_tuple_returned_unchanged_when_debug_off(self): + add_comment = self._get_add_comment() + COMMON_CONFIG["debug_comments"] = False + cmds = ("cmd1", "cmd2") + result = add_comment(cmds, "ignored") + assert result == cmds + + def test_string_returned_unchanged_when_debug_off(self): + add_comment = self._get_add_comment() + COMMON_CONFIG["debug_comments"] = False + result = add_comment("single cmd", "ignored") + assert result == "single cmd" + + def test_list_converted_to_tuple_when_debug_off(self): + """PR change: list input is now converted to tuple even when debug is off.""" + add_comment = self._get_add_comment() + COMMON_CONFIG["debug_comments"] = False + result = add_comment(["a", "b"], "ignored comment") + assert isinstance(result, tuple) + assert result == ("a", "b") + + def test_list_debug_off_preserves_content(self): + add_comment = self._get_add_comment() + COMMON_CONFIG["debug_comments"] = False + result = add_comment(["x", "y", "z"], "ignored") + assert list(result) == ["x", "y", "z"] + + # ---- error cases ---- + + def test_raises_value_error_for_non_iterable_input(self): + add_comment = self._get_add_comment() + with pytest.raises(ValueError) as exc_info: + add_comment(42, "comment") + assert "tuple, list, or str" in str(exc_info.value) + + def test_raises_value_error_for_dict_input(self): + add_comment = self._get_add_comment() + with pytest.raises(ValueError): + add_comment({"key": "val"}, "comment") + + def test_raises_value_error_for_none_input(self): + add_comment = self._get_add_comment() + with pytest.raises(ValueError): + add_comment(None, "comment") + + def test_empty_tuple_with_comment(self): + add_comment = self._get_add_comment() + result = add_comment((), "comment") + assert result == ("\n# comment",) + + def test_empty_list_with_comment(self): + add_comment = self._get_add_comment() + result = add_comment([], "comment") + assert isinstance(result, tuple) + assert result == ("\n# comment",) + + def test_empty_string_with_comment(self): + add_comment = self._get_add_comment() + result = add_comment("", "comment") + assert result.startswith("\n# comment\n") \ No newline at end of file diff --git a/tests/test_compile_types.py b/tests/test_compile_types.py new file mode 100644 index 0000000..9ce25e3 --- /dev/null +++ b/tests/test_compile_types.py @@ -0,0 +1,347 @@ +"""Tests for changes in minecraft_script/compiler/compile_types.py. + +This PR changed MCSObject, MCSVariable, MCSList, MCSNull to use +get_version_context() for generating storage commands instead of +hard-coded f-strings. +""" +import pytest + +import minecraft_script.version_config as vc +from minecraft_script.compiler import compile_types + + +# --------------------------------------------------------------------------- +# Shared fake context for constructing MCS objects in tests +# --------------------------------------------------------------------------- + +class FakeContext: + """Minimal context object that satisfies compile_types requirements.""" + def __init__(self, uuid: str = "deadbeef-0000-0000-0000-000000000001"): + self.uuid = uuid + + +FAKE_UUID_1 = "deadbeef-0000-0000-0000-000000000001" +FAKE_UUID_2 = "cafef00d-0000-0000-0000-000000000002" + + +@pytest.fixture(autouse=True) +def version_context_1204(): + """Ensure the 1.20.4 version context is active for all tests in this file.""" + ctx = vc.init_version_context("test_pack") + yield ctx + vc.clear_version_context() + + +# =========================================================================== +# MCSObject +# =========================================================================== + +class TestMCSObjectStorageCommands: + """Tests for MCSObject methods changed in this PR.""" + + def test_get_storage_returns_mcs_prefix_plus_uuid(self): + ctx = FakeContext(FAKE_UUID_1) + obj = compile_types.MCSNumber(ctx) + assert obj.get_storage() == f"mcs_{FAKE_UUID_1}" + + def test_get_nbt_contains_storage_compartment(self): + ctx = FakeContext(FAKE_UUID_1) + obj = compile_types.MCSNumber(ctx) + assert obj.get_nbt().startswith("number.") + + def test_save_to_storage_cmd_returns_string(self): + ctx = FakeContext(FAKE_UUID_1) + obj = compile_types.MCSNumber(ctx) + cmd = obj.save_to_storage_cmd(42) + assert isinstance(cmd, str) + + def test_save_to_storage_cmd_contains_storage(self): + ctx = FakeContext(FAKE_UUID_1) + obj = compile_types.MCSNumber(ctx) + cmd = obj.save_to_storage_cmd(42) + assert obj.get_storage() in cmd + + def test_save_to_storage_cmd_contains_nbt(self): + ctx = FakeContext(FAKE_UUID_1) + obj = compile_types.MCSNumber(ctx) + cmd = obj.save_to_storage_cmd(42) + assert obj.get_nbt() in cmd + + def test_save_to_storage_cmd_contains_value(self): + ctx = FakeContext(FAKE_UUID_1) + obj = compile_types.MCSNumber(ctx) + cmd = obj.save_to_storage_cmd(99) + assert "99" in cmd + + def test_save_to_storage_cmd_uses_data_modify(self): + ctx = FakeContext(FAKE_UUID_1) + obj = compile_types.MCSNumber(ctx) + cmd = obj.save_to_storage_cmd(1) + assert "data modify storage" in cmd + + def test_delete_from_storage_cmd_returns_string(self): + ctx = FakeContext(FAKE_UUID_1) + obj = compile_types.MCSNumber(ctx) + cmd = obj.delete_from_storage_cmd() + assert isinstance(cmd, str) + + def test_delete_from_storage_cmd_contains_data_remove(self): + ctx = FakeContext(FAKE_UUID_1) + obj = compile_types.MCSNumber(ctx) + cmd = obj.delete_from_storage_cmd() + assert "data remove storage" in cmd + + def test_delete_from_storage_cmd_contains_storage(self): + ctx = FakeContext(FAKE_UUID_1) + obj = compile_types.MCSNumber(ctx) + cmd = obj.delete_from_storage_cmd() + assert obj.get_storage() in cmd + + def test_delete_from_storage_cmd_contains_nbt(self): + ctx = FakeContext(FAKE_UUID_1) + obj = compile_types.MCSNumber(ctx) + cmd = obj.delete_from_storage_cmd() + assert obj.get_nbt() in cmd + + def test_set_to_current_cmd_returns_string(self): + ctx1 = FakeContext(FAKE_UUID_1) + ctx2 = FakeContext(FAKE_UUID_2) + obj = compile_types.MCSNumber(ctx1) + cmd = obj.set_to_current_cmd(ctx2) + assert isinstance(cmd, str) + + def test_set_to_current_cmd_contains_dest_storage(self): + ctx1 = FakeContext(FAKE_UUID_1) + ctx2 = FakeContext(FAKE_UUID_2) + obj = compile_types.MCSNumber(ctx1) + cmd = obj.set_to_current_cmd(ctx2) + assert f"mcs_{FAKE_UUID_2}" in cmd + + def test_set_to_current_cmd_contains_source_storage(self): + ctx1 = FakeContext(FAKE_UUID_1) + ctx2 = FakeContext(FAKE_UUID_2) + obj = compile_types.MCSNumber(ctx1) + cmd = obj.set_to_current_cmd(ctx2) + assert f"mcs_{FAKE_UUID_1}" in cmd + + def test_set_to_current_cmd_references_current_key(self): + ctx1 = FakeContext(FAKE_UUID_1) + ctx2 = FakeContext(FAKE_UUID_2) + obj = compile_types.MCSNumber(ctx1) + cmd = obj.set_to_current_cmd(ctx2) + assert "current" in cmd + + def test_set_to_current_cmd_uses_data_modify(self): + ctx1 = FakeContext(FAKE_UUID_1) + ctx2 = FakeContext(FAKE_UUID_2) + obj = compile_types.MCSNumber(ctx1) + cmd = obj.set_to_current_cmd(ctx2) + assert "data modify storage" in cmd + + +# =========================================================================== +# MCSVariable +# =========================================================================== + +class TestMCSVariable: + def test_get_nbt_uses_variable_prefix(self): + ctx = FakeContext(FAKE_UUID_1) + var = compile_types.MCSVariable("my_var", ctx) + assert var.get_nbt() == "variable.my_var" + + def test_get_storage_uses_context_uuid(self): + ctx = FakeContext(FAKE_UUID_1) + var = compile_types.MCSVariable("x", ctx) + assert var.get_storage() == f"mcs_{FAKE_UUID_1}" + + def test_set_to_current_cmd_returns_string(self): + ctx1 = FakeContext(FAKE_UUID_1) + ctx2 = FakeContext(FAKE_UUID_2) + var = compile_types.MCSVariable("myvar", ctx1) + cmd = var.set_to_current_cmd(ctx2) + assert isinstance(cmd, str) + + def test_set_to_current_cmd_references_dest_storage(self): + ctx1 = FakeContext(FAKE_UUID_1) + ctx2 = FakeContext(FAKE_UUID_2) + var = compile_types.MCSVariable("myvar", ctx1) + cmd = var.set_to_current_cmd(ctx2) + assert f"mcs_{FAKE_UUID_2}" in cmd + + def test_set_to_current_cmd_references_variable_nbt(self): + ctx1 = FakeContext(FAKE_UUID_1) + ctx2 = FakeContext(FAKE_UUID_2) + var = compile_types.MCSVariable("myvar", ctx1) + cmd = var.set_to_current_cmd(ctx2) + assert "variable.myvar" in cmd + + def test_set_to_current_uses_data_modify(self): + ctx1 = FakeContext(FAKE_UUID_1) + ctx2 = FakeContext(FAKE_UUID_2) + var = compile_types.MCSVariable("v", ctx1) + cmd = var.set_to_current_cmd(ctx2) + assert "data modify storage" in cmd + + +# =========================================================================== +# MCSNull +# =========================================================================== + +class TestMCSNull: + def test_save_to_storage_cmd_returns_string(self): + ctx = FakeContext(FAKE_UUID_1) + null = compile_types.MCSNull(ctx) + cmd = null.save_to_storage_cmd() + assert isinstance(cmd, str) + + def test_save_to_storage_cmd_contains_null_value(self): + ctx = FakeContext(FAKE_UUID_1) + null = compile_types.MCSNull(ctx) + cmd = null.save_to_storage_cmd() + assert "0b" in cmd + + def test_save_to_storage_cmd_uses_data_modify(self): + ctx = FakeContext(FAKE_UUID_1) + null = compile_types.MCSNull(ctx) + cmd = null.save_to_storage_cmd() + assert "data modify storage" in cmd + + def test_set_to_current_cmd_returns_string(self): + ctx1 = FakeContext(FAKE_UUID_1) + ctx2 = FakeContext(FAKE_UUID_2) + null = compile_types.MCSNull(ctx1) + cmd = null.set_to_current_cmd(ctx2) + assert isinstance(cmd, str) + + def test_set_to_current_cmd_sets_to_null(self): + ctx1 = FakeContext(FAKE_UUID_1) + ctx2 = FakeContext(FAKE_UUID_2) + null = compile_types.MCSNull(ctx1) + cmd = null.set_to_current_cmd(ctx2) + assert "0b" in cmd + + def test_set_to_current_cmd_targets_dest_storage(self): + ctx1 = FakeContext(FAKE_UUID_1) + ctx2 = FakeContext(FAKE_UUID_2) + null = compile_types.MCSNull(ctx1) + cmd = null.set_to_current_cmd(ctx2) + assert f"mcs_{FAKE_UUID_2}" in cmd + + def test_set_to_current_cmd_targets_current_nbt(self): + ctx1 = FakeContext(FAKE_UUID_1) + ctx2 = FakeContext(FAKE_UUID_2) + null = compile_types.MCSNull(ctx1) + cmd = null.set_to_current_cmd(ctx2) + assert "current" in cmd + + def test_repr(self): + ctx = FakeContext(FAKE_UUID_1) + null = compile_types.MCSNull(ctx) + assert repr(null) == "MCSNull()" + + +# =========================================================================== +# MCSList +# =========================================================================== + +class TestMCSList: + def _make_simple_element(self, ctx, value=42): + """Create a MCSNumber element with a save command.""" + elem = compile_types.MCSNumber(ctx) + return elem + + def test_save_to_storage_cmd_returns_list(self): + ctx = FakeContext(FAKE_UUID_1) + lst = compile_types.MCSList(ctx) + cmds = lst.save_to_storage_cmd([]) + assert isinstance(cmds, list) + + def test_save_empty_list_has_length_zero(self): + ctx = FakeContext(FAKE_UUID_1) + lst = compile_types.MCSList(ctx) + cmds = lst.save_to_storage_cmd([]) + # First command sets the length + assert "length" in cmds[0] + assert "0" in cmds[0] + + def test_save_list_length_command_is_first(self): + ctx = FakeContext(FAKE_UUID_1) + lst = compile_types.MCSList(ctx) + elem = compile_types.MCSNumber(ctx) + cmds = lst.save_to_storage_cmd([elem]) + assert "length" in cmds[0] + + def test_save_list_with_elements_produces_commands(self): + ctx = FakeContext(FAKE_UUID_1) + lst = compile_types.MCSList(ctx) + elem1 = compile_types.MCSNumber(ctx) + elem2 = compile_types.MCSString(ctx) + cmds = lst.save_to_storage_cmd([elem1, elem2]) + # length + 2*(set_to_current + element) = 5 total + assert len(cmds) == 5 + + def test_save_list_element_references_index_zero(self): + ctx = FakeContext(FAKE_UUID_1) + lst = compile_types.MCSList(ctx) + elem = compile_types.MCSNumber(ctx) + cmds = lst.save_to_storage_cmd([elem]) + # One of the commands should reference ".0" + assert any(".0" in cmd for cmd in cmds) + + def test_save_list_element_command_uses_data_modify(self): + ctx = FakeContext(FAKE_UUID_1) + lst = compile_types.MCSList(ctx) + elem = compile_types.MCSNumber(ctx) + cmds = lst.save_to_storage_cmd([elem]) + assert all("data modify" in cmd or "data remove" in cmd for cmd in cmds if cmd) + + def test_save_list_length_uses_version_template(self): + ctx = FakeContext(FAKE_UUID_1) + lst = compile_types.MCSList(ctx) + cmds = lst.save_to_storage_cmd([]) + # Template renders: data modify storage .length set value + assert "data modify storage" in cmds[0] + assert ".length" in cmds[0] + + def test_repr(self): + ctx = FakeContext(FAKE_UUID_1) + lst = compile_types.MCSList(ctx) + assert "MCSList" in repr(lst) + + +# =========================================================================== +# Various MCS types: storage compartments +# =========================================================================== + +class TestMCSTypeCompartments: + """Verify each type uses the right storage compartment.""" + + def test_mcs_number_compartment(self): + ctx = FakeContext(FAKE_UUID_1) + obj = compile_types.MCSNumber(ctx) + assert obj.get_nbt().startswith("number.") + + def test_mcs_string_compartment(self): + ctx = FakeContext(FAKE_UUID_1) + obj = compile_types.MCSString(ctx) + assert obj.get_nbt().startswith("string.") + + def test_mcs_boolean_compartment(self): + ctx = FakeContext(FAKE_UUID_1) + obj = compile_types.MCSBoolean(ctx) + assert obj.get_nbt().startswith("boolean.") + + def test_mcs_unknown_compartment(self): + ctx = FakeContext(FAKE_UUID_1) + obj = compile_types.MCSUnknown(ctx) + assert obj.get_nbt().startswith("unknown.") + + def test_mcs_null_compartment(self): + ctx = FakeContext(FAKE_UUID_1) + obj = compile_types.MCSNull(ctx) + assert obj.get_nbt().startswith("null.") + + def test_mcs_list_compartment(self): + ctx = FakeContext(FAKE_UUID_1) + obj = compile_types.MCSList(ctx) + assert obj.get_nbt().startswith("list.") \ No newline at end of file diff --git a/tests/test_version_config.py b/tests/test_version_config.py new file mode 100644 index 0000000..ce6b38a --- /dev/null +++ b/tests/test_version_config.py @@ -0,0 +1,574 @@ +"""Tests for minecraft_script/version_config.py (new module added in this PR).""" +import json +import pytest +from pathlib import Path +from unittest.mock import patch, MagicMock + +import minecraft_script.version_config as vc +from minecraft_script import common + + +# --------------------------------------------------------------------------- +# Helpers / fixtures +# --------------------------------------------------------------------------- + +MINIMAL_PROFILE = { + "minecraft_version": "0.0.test", + "pack_format": 99, + "paths": { + "function_dir": "functions", + "function_tag_dir": "functions", + }, + "constants": { + "testConst": "hello", + }, + "templates": { + "simple": "hello {{name}}", + "multiline": "line1\nline2 {{value}}\nline3", + "no_params": "static text", + "with_const": "{{testConst}} world", + "datapack_ref": "function {{datapack_id}}:something", + "conditional": "{{#if flag}}yes{{else}}no{{/if}}", + }, +} + + +# =========================================================================== +# get_minecraft_version +# =========================================================================== + +class TestGetMinecraftVersion: + def test_returns_value_from_common_config(self): + with patch.object(vc, "COMMON_CONFIG", {"minecraft_version": "1.20.4"}): + assert vc.get_minecraft_version() == "1.20.4" + + def test_returns_different_version(self): + with patch.object(vc, "COMMON_CONFIG", {"minecraft_version": "1.99.0"}): + assert vc.get_minecraft_version() == "1.99.0" + + +# =========================================================================== +# load_version_profile +# =========================================================================== + +class TestLoadVersionProfile: + def test_loads_real_1_20_4_profile(self): + profile = vc.load_version_profile("1.20.4") + assert profile["minecraft_version"] == "1.20.4" + assert profile["pack_format"] == 41 + assert "templates" in profile + assert "paths" in profile + + def test_profile_has_required_keys(self): + profile = vc.load_version_profile("1.20.4") + assert "paths" in profile + assert "function_dir" in profile["paths"] + assert "function_tag_dir" in profile["paths"] + assert "constants" in profile + assert "templates" in profile + + def test_caches_profile_after_first_load(self): + vc._profile_cache.clear() + profile1 = vc.load_version_profile("1.20.4") + profile2 = vc.load_version_profile("1.20.4") + assert profile1 is profile2 # same object from cache + + def test_uses_common_config_version_when_none_given(self): + vc._profile_cache.clear() + with patch.object(vc, "COMMON_CONFIG", {"minecraft_version": "1.20.4"}): + profile = vc.load_version_profile(None) + assert profile["minecraft_version"] == "1.20.4" + + def test_raises_file_not_found_for_unknown_version(self): + with pytest.raises(FileNotFoundError) as exc_info: + vc.load_version_profile("9.99.99") + assert "9.99.99" in str(exc_info.value) + assert "Unknown minecraft version" in str(exc_info.value) + + def test_error_message_includes_supported_versions(self): + with pytest.raises(FileNotFoundError) as exc_info: + vc.load_version_profile("9.99.99") + error_text = str(exc_info.value) + # Should mention supported versions or 'none' + assert "Supported:" in error_text + + def test_uses_cached_profile_without_reading_file(self): + vc._profile_cache["fake_version"] = MINIMAL_PROFILE + profile = vc.load_version_profile("fake_version") + assert profile is MINIMAL_PROFILE + + +# =========================================================================== +# list_supported_versions +# =========================================================================== + +class TestListSupportedVersions: + def test_returns_list(self): + versions = vc.list_supported_versions() + assert isinstance(versions, list) + + def test_includes_1_20_4(self): + versions = vc.list_supported_versions() + assert "1.20.4" in versions + + def test_excludes_index_json(self): + versions = vc.list_supported_versions() + assert "index" not in versions + + def test_returns_sorted_list(self): + versions = vc.list_supported_versions() + assert versions == sorted(versions) + + def test_returns_empty_when_versions_dir_missing(self, tmp_path): + nonexistent = tmp_path / "no_such_dir" + with patch.object(vc, "module_folder", str(tmp_path / "fake_module")): + result = vc.list_supported_versions() + assert result == [] + + def test_returns_only_json_stems(self): + versions = vc.list_supported_versions() + for v in versions: + assert "." in v or v.isidentifier() # version strings should be reasonable + + +# =========================================================================== +# predefined_root +# =========================================================================== + +class TestPredefinedRoot: + def test_returns_string(self): + result = vc.predefined_root("math", "1.20.4") + assert isinstance(result, str) + + def test_path_contains_version(self): + result = vc.predefined_root("math", "1.20.4") + assert "1.20.4" in result + + def test_path_contains_category(self): + result = vc.predefined_root("math", "1.20.4") + assert "math" in result + + def test_path_ends_with_version_and_category(self): + result = vc.predefined_root("builtins", "1.20.4") + assert result.endswith("builtins/1.20.4") + + def test_uses_current_version_when_none(self): + with patch.object(vc, "COMMON_CONFIG", {"minecraft_version": "1.20.4"}): + result = vc.predefined_root("tags") + assert "1.20.4" in result + assert "tags" in result + + def test_explicit_version_overrides_config(self): + with patch.object(vc, "COMMON_CONFIG", {"minecraft_version": "1.20.4"}): + result = vc.predefined_root("math", "2.0.0") + assert "2.0.0" in result + assert "1.20.4" not in result + + def test_math_path_exists_for_1_20_4(self): + result = vc.predefined_root("math", "1.20.4") + assert Path(result).is_dir() + + def test_builtins_path_exists_for_1_20_4(self): + result = vc.predefined_root("builtins", "1.20.4") + assert Path(result).is_dir() + + def test_tags_path_exists_for_1_20_4(self): + result = vc.predefined_root("tags", "1.20.4") + assert Path(result).is_dir() + + +# =========================================================================== +# VersionRenderer +# =========================================================================== + +class TestVersionRenderer: + def setup_method(self): + self.renderer = vc.VersionRenderer(MINIMAL_PROFILE) + + def test_render_simple_template(self): + result = self.renderer.render("simple", name="world") + assert result == "hello world" + + def test_render_no_params_template(self): + result = self.renderer.render("no_params") + assert result == "static text" + + def test_render_strips_whitespace(self): + profile = {**MINIMAL_PROFILE, "templates": {"padded": " hello "}} + renderer = vc.VersionRenderer(profile) + assert renderer.render("padded") == "hello" + + def test_render_raises_key_error_for_missing_key(self): + with pytest.raises(KeyError) as exc_info: + self.renderer.render("nonexistent_key") + assert "nonexistent_key" in str(exc_info.value) + + def test_render_raises_key_error_when_no_templates_key(self): + renderer = vc.VersionRenderer({"minecraft_version": "0.0"}) + with pytest.raises(KeyError): + renderer.render("anything") + + def test_render_compiles_template_once_and_caches(self): + self.renderer.render("simple", name="first") + compiled_before = dict(self.renderer._compiled) + self.renderer.render("simple", name="second") + assert set(self.renderer._compiled.keys()) == set(compiled_before.keys()) + + def test_render_lines_splits_on_newlines(self): + result = self.renderer.render_lines("multiline", value="test") + assert result == ["line1", "line2 test", "line3"] + + def test_render_lines_single_line(self): + result = self.renderer.render_lines("simple", name="x") + assert result == ["hello x"] + + def test_render_lines_returns_empty_list_for_empty_output(self): + profile = {**MINIMAL_PROFILE, "templates": {"empty": ""}} + renderer = vc.VersionRenderer(profile) + result = renderer.render_lines("empty") + assert result == [] + + def test_render_conditional_true(self): + result = self.renderer.render("conditional", flag=True) + assert result == "yes" + + def test_render_conditional_false(self): + result = self.renderer.render("conditional", flag=False) + assert result == "no" + + +# =========================================================================== +# VersionContext +# =========================================================================== + +class TestVersionContext: + def test_init_with_explicit_profile(self): + ctx = vc.VersionContext("my_pack", profile=MINIMAL_PROFILE) + assert ctx.datapack_id == "my_pack" + assert ctx.profile is MINIMAL_PROFILE + + def test_init_reads_function_dir_from_profile(self): + ctx = vc.VersionContext("my_pack", profile=MINIMAL_PROFILE) + assert ctx.function_dir == "functions" + + def test_init_reads_function_tag_dir_from_profile(self): + ctx = vc.VersionContext("my_pack", profile=MINIMAL_PROFILE) + assert ctx.function_tag_dir == "functions" + + def test_init_sets_pack_format_as_string(self): + ctx = vc.VersionContext("my_pack", profile=MINIMAL_PROFILE) + assert ctx.pack_format == "99" + assert isinstance(ctx.pack_format, str) + + def test_init_loads_real_profile_when_none_given(self): + vc._profile_cache.clear() + with patch.object(vc, "COMMON_CONFIG", {"minecraft_version": "1.20.4"}): + ctx = vc.VersionContext("dp") + assert ctx.profile["minecraft_version"] == "1.20.4" + + def test_params_merges_constants(self): + ctx = vc.VersionContext("my_pack", profile=MINIMAL_PROFILE) + params = ctx._params(extra="val") + assert params["testConst"] == "hello" + assert params["extra"] == "val" + + def test_params_adds_datapack_id(self): + ctx = vc.VersionContext("my_pack", profile=MINIMAL_PROFILE) + params = ctx._params() + assert params["datapack_id"] == "my_pack" + + def test_params_caller_overrides_datapack_id(self): + ctx = vc.VersionContext("my_pack", profile=MINIMAL_PROFILE) + params = ctx._params(datapack_id="override") + assert params["datapack_id"] == "override" + + def test_params_caller_overrides_constant(self): + ctx = vc.VersionContext("my_pack", profile=MINIMAL_PROFILE) + params = ctx._params(testConst="overridden") + assert params["testConst"] == "overridden" + + def test_render_injects_datapack_id(self): + ctx = vc.VersionContext("my_pack", profile=MINIMAL_PROFILE) + result = ctx.render("datapack_ref") + assert result == "function my_pack:something" + + def test_render_injects_constants(self): + ctx = vc.VersionContext("my_pack", profile=MINIMAL_PROFILE) + result = ctx.render("with_const") + assert result == "hello world" + + def test_render_passes_caller_params(self): + ctx = vc.VersionContext("my_pack", profile=MINIMAL_PROFILE) + result = ctx.render("simple", name="earth") + assert result == "hello earth" + + def test_render_lines_returns_list(self): + ctx = vc.VersionContext("my_pack", profile=MINIMAL_PROFILE) + result = ctx.render_lines("multiline", value="x") + assert isinstance(result, list) + assert len(result) == 3 + + def test_render_lines_splits_correctly(self): + ctx = vc.VersionContext("my_pack", profile=MINIMAL_PROFILE) + result = ctx.render_lines("multiline", value="abc") + assert result[1] == "line2 abc" + + def test_render_raises_key_error_for_missing_template(self): + ctx = vc.VersionContext("my_pack", profile=MINIMAL_PROFILE) + with pytest.raises(KeyError) as exc_info: + ctx.render("nonexistent") + assert "nonexistent" in str(exc_info.value) + + +# =========================================================================== +# 1.20.4 profile templates (smoke tests for actual content) +# =========================================================================== + +class TestRealProfile1204Templates: + """Smoke tests verifying key templates in the 1.20.4 profile render correctly.""" + + def setup_method(self): + vc._profile_cache.clear() + self.ctx = vc.VersionContext("my_dp", profile=vc.load_version_profile("1.20.4")) + + def test_datapack_init_contains_scoreboard(self): + result = self.ctx.render("datapack.init") + assert "scoreboard objectives add mcs_math" in result + + def test_datapack_init_references_datapack_id(self): + result = self.ctx.render("datapack.init") + assert "my_dp" in result + + def test_datapack_kill_contains_datapack_name(self): + result = self.ctx.render("datapack.kill", datapackName="test_dp_name") + assert "test_dp_name" in result + + def test_datapack_main_references_datapack_id(self): + result = self.ctx.render("datapack.main") + assert "my_dp" in result + + def test_literal_save_template(self): + result = self.ctx.render("literal.save", storage="my_storage", nbt="my.nbt", value=42) + assert "data modify storage my_storage my.nbt set value 42" in result + + def test_literal_delete_template(self): + result = self.ctx.render("literal.delete", storage="my_storage", nbt="my.nbt") + assert "data remove storage my_storage my.nbt" in result + + def test_literal_set_to_current_template(self): + result = self.ctx.render( + "literal.set_to_current", + destStorage="dest_store", + storage="src_store", + nbt="my.nbt", + ) + assert "data modify storage dest_store current set from storage src_store my.nbt" in result + + def test_variable_declare_template(self): + result = self.ctx.render( + "variable.declare", + ownerStorage="mcs_abc", + name="myVar", + valueStorage="mcs_xyz", + valueNbt="number.123", + ) + assert "data modify storage mcs_abc variable.myVar set from storage mcs_xyz number.123" in result + + def test_math_binary_template(self): + result = self.ctx.render( + "math.binary", + leftStorage="mcs_l", + leftNbt="number.1", + rightStorage="mcs_r", + rightNbt="number.2", + operation="add", + resultStorage="mcs_res", + resultNbt="number.out", + ) + lines = result.split("\n") + assert any("score .a mcs_math" in l for l in lines) + assert any("score .b mcs_math" in l for l in lines) + assert any("math/add" in l for l in lines) + + def test_math_unary_template(self): + result = self.ctx.render( + "math.unary", + rootStorage="mcs_r", + rootNbt="number.1", + operation="u_not", + resultStorage="mcs_res", + resultNbt="boolean.out", + ) + assert "math/u_not" in result + + def test_control_if_branch_template(self): + result = self.ctx.render( + "control.if.branch", + exprStorage="mcs_e", + exprNbt="boolean.1", + branchStorage="mcs_b", + parentStorage="mcs_p", + branchPath="code_blocks/foo", + ) + assert ".out mcs_math" in result + assert "return 0" in result + + def test_control_for_init_template(self): + result = self.ctx.render( + "control.for.init", + loopId="loop123", + iterableStorage="mcs_i", + iterableNbt="list.1", + loopPath="code_blocks/loop", + ) + assert "loop_iter_loop123" in result + assert "loop_end_loop123" in result + + def test_kill_remove_compartment_template(self): + result = self.ctx.render( + "kill.remove_compartment", + ctxStorage="mcs_abc123", + compartment="number", + ) + assert "data remove storage mcs_abc123 number" in result + + def test_function_call_invoke_template(self): + result = self.ctx.render("function.call.invoke", functionName="my_func") + assert "my_dp:user_functions/my_func" in result + + def test_click_check_template(self): + result = self.ctx.render("click.check") + assert "mcs_click" in result + assert "my_dp" in result + + def test_builtin_log_template(self): + result = self.ctx.render("builtin.log", storageSuffix=" {s0: foo}") + assert "builtins/log" in result + assert "my_dp" in result + + def test_builtin_give_item_setup_with_components(self): + result = self.ctx.render( + "builtin.give_item.setup", + ctxStorage="mcs_ctx", + itemStorage="mcs_item", + itemNbt="string.1", + hasComponents=True, + componentsStorage="mcs_comp", + componentsNbt="string.2", + hasCount=False, + ) + assert "current.item" in result + assert "current.components set from" in result + assert "builtins/give_item" in result + + def test_builtin_give_item_setup_without_components(self): + result = self.ctx.render( + "builtin.give_item.setup", + ctxStorage="mcs_ctx", + itemStorage="mcs_item", + itemNbt="string.1", + hasComponents=False, + hasCount=False, + ) + assert "current.components set value ''" in result + + def test_builtin_give_item_setup_with_count(self): + result = self.ctx.render( + "builtin.give_item.setup", + ctxStorage="mcs_ctx", + itemStorage="mcs_item", + itemNbt="string.1", + hasComponents=False, + hasCount=True, + countStorage="mcs_count", + countNbt="number.3", + ) + assert "current.count set from" in result + + def test_builtin_give_item_setup_default_count(self): + result = self.ctx.render( + "builtin.give_item.setup", + ctxStorage="mcs_ctx", + itemStorage="mcs_item", + itemNbt="string.1", + hasComponents=False, + hasCount=False, + ) + assert "current.count set value 1" in result + + def test_pack_mcmeta_contains_pack_format(self): + result = self.ctx.render("pack.mcmeta", pack_format=41) + assert "41" in result + assert "pack_format" in result + + def test_builtin_raycast_block_loop_has_no_loop_function(self): + result = self.ctx.render( + "builtin.raycast_block.loop", + raycastId="abc", + hitFunction="my_hit", + hasLoopFunction=False, + loopFunction="", + loopPath="code_blocks/rc", + ) + assert "# No loop function" in result + + def test_builtin_raycast_block_loop_with_loop_function(self): + result = self.ctx.render( + "builtin.raycast_block.loop", + raycastId="abc", + hitFunction="my_hit", + hasLoopFunction=True, + loopFunction="my_loop", + loopPath="code_blocks/rc", + ) + assert "user_functions/my_loop" in result + assert "# No loop function" not in result + + +# =========================================================================== +# init_version_context / get_version_context / clear_version_context +# =========================================================================== + +class TestVersionContextLifecycle: + def test_init_version_context_returns_context(self): + ctx = vc.init_version_context("test_pack") + assert isinstance(ctx, vc.VersionContext) + + def test_init_version_context_sets_datapack_id(self): + ctx = vc.init_version_context("my_dp_id") + assert ctx.datapack_id == "my_dp_id" + + def test_get_version_context_returns_same_object(self): + ctx = vc.init_version_context("dp") + got = vc.get_version_context() + assert got is ctx + + def test_get_version_context_raises_when_not_initialized(self): + vc.clear_version_context() + with pytest.raises(RuntimeError) as exc_info: + vc.get_version_context() + assert "not initialized" in str(exc_info.value).lower() + + def test_clear_version_context_causes_get_to_raise(self): + vc.init_version_context("dp") + vc.clear_version_context() + with pytest.raises(RuntimeError): + vc.get_version_context() + + def test_init_replaces_existing_context(self): + ctx1 = vc.init_version_context("dp1") + ctx2 = vc.init_version_context("dp2") + assert vc.get_version_context() is ctx2 + assert ctx1 is not ctx2 + + def test_clear_allows_re_init(self): + vc.init_version_context("dp") + vc.clear_version_context() + ctx = vc.init_version_context("dp2") + assert vc.get_version_context() is ctx + + def test_get_raises_with_helpful_message(self): + vc.clear_version_context() + with pytest.raises(RuntimeError) as exc_info: + vc.get_version_context() + assert "init_version_context" in str(exc_info.value)