-
Notifications
You must be signed in to change notification settings - Fork 1
Enhance project foundation with coverage, ConfigManager tests, and modern types #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
8760725
531320e
a70bcb7
2056734
329e05d
143a383
81e7ab4
59817e3
1ddefef
cdf0e4d
98154f8
0884923
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| [run] | ||
| source = terminara/ | ||
| omit = | ||
| */tests/* | ||
| */.venv/* | ||
|
|
||
| [report] | ||
| omit = | ||
| */tests/* | ||
| */.venv/* | ||
| skip_empty = True | ||
| exclude_lines = | ||
| pragma: no cover | ||
| def __repr__ | ||
| def __str__ | ||
| raise AssertionError | ||
| raise NotImplementedError | ||
| if __name__ == .__main__.: |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,3 +15,10 @@ | |
| .env | ||
| /.coverage | ||
| /coverage.xml | ||
|
|
||
| # Virtual environment | ||
| /.venv/ | ||
|
|
||
| # Python cache | ||
| __pycache__/ | ||
| *.py[cod] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| from pathlib import Path | ||
| import pytest | ||
| from terminara.core.config_manager import ConfigManager | ||
| import platform | ||
|
|
||
| @pytest.fixture | ||
| def config_manager(tmp_path: Path, monkeypatch) -> ConfigManager: | ||
| """ | ||
| Fixture to create a ConfigManager that uses a temporary directory for its config file. | ||
| """ | ||
| # We monkeypatch the __init__ to use a temporary directory | ||
| # This is to isolate tests from the user's actual configuration | ||
| # and from each other. | ||
|
|
||
| def mock_init(self): | ||
| self.config_dir = tmp_path / "Terminara" | ||
| self.config_dir.mkdir(parents=True, exist_ok=True) | ||
| self.config_file = self.config_dir / "config.json" | ||
|
|
||
| monkeypatch.setattr(ConfigManager, "__init__", mock_init) | ||
|
|
||
| return ConfigManager() | ||
|
|
||
|
|
||
| class TestConfigManager: | ||
| """Test suite for the ConfigManager class.""" | ||
|
|
||
| # We test the real __init__ separately without the fixture | ||
| def test_init_creates_directory(self, monkeypatch, tmp_path: Path): | ||
| """Test that ConfigManager's constructor creates the config directory.""" | ||
|
|
||
| # We patch platform.system to control which config path is used | ||
| monkeypatch.setattr(platform, "system", lambda: "Linux") | ||
| # We patch Path.home to point to our temp directory for predictability | ||
| monkeypatch.setattr(Path, "home", lambda: tmp_path) | ||
|
|
||
| expected_config_dir = tmp_path / '.local' / 'share' / "Terminara" | ||
| assert not expected_config_dir.exists() | ||
|
|
||
| # This should create the directory | ||
| ConfigManager() | ||
|
|
||
| assert expected_config_dir.exists() | ||
|
|
||
| def test_get_config_no_file(self, config_manager: ConfigManager): | ||
| """Test get_config() when the config file doesn't exist.""" | ||
| assert config_manager.get_config() == {} | ||
|
|
||
| def test_get_config_empty_file(self, config_manager: ConfigManager): | ||
| """Test get_config() when the config file is empty and raises JSONDecodeError.""" | ||
| config_manager.config_file.touch() | ||
| assert config_manager.get_config() == {} | ||
|
|
||
| def test_get_config_invalid_json(self, config_manager: ConfigManager): | ||
| """Test get_config() with an invalid JSON file.""" | ||
| config_manager.config_file.write_text("this is not json") | ||
| assert config_manager.get_config() == {} | ||
|
|
||
| def test_save_and_get_config(self, config_manager: ConfigManager): | ||
| """Test saving a configuration and then retrieving it.""" | ||
| config_data = {"hello": "world", "number": 123} | ||
| config_manager.save_config(config_data) | ||
|
|
||
| retrieved_config = config_manager.get_config() | ||
| assert retrieved_config == config_data | ||
|
|
||
| def test_get_value(self, config_manager: ConfigManager): | ||
| """Test retrieving a single value from the configuration.""" | ||
| config_data = {"key1": "value1", "key2": "value2"} | ||
| config_manager.save_config(config_data) | ||
|
|
||
| assert config_manager.get_value("key1") == "value1" | ||
|
|
||
| def test_get_nonexistent_value(self, config_manager: ConfigManager): | ||
| """Test that get_value() returns None for a key that does not exist.""" | ||
| assert config_manager.get_value("nonexistent_key") is None | ||
|
|
||
| def test_set_value(self, config_manager: ConfigManager): | ||
| """Test setting a single value in the configuration.""" | ||
| config_manager.set_value("new_key", "new_value") | ||
|
|
||
| # Verify by reading the whole config | ||
| config = config_manager.get_config() | ||
| assert config.get("new_key") == "new_value" | ||
|
|
||
| # Verify using get_value | ||
| assert config_manager.get_value("new_key") == "new_value" | ||
|
|
||
| def test_set_value_overwrites_existing(self, config_manager: ConfigManager): | ||
| """Test that set_value() overwrites an existing value.""" | ||
| config_manager.set_value("key_to_overwrite", "initial_value") | ||
| config_manager.set_value("key_to_overwrite", "updated_value") | ||
|
|
||
| assert config_manager.get_value("key_to_overwrite") == "updated_value" | ||
|
|
||
| def test_delete_value(self, config_manager: ConfigManager): | ||
| """Test removing a single value from the configuration.""" | ||
| config_data = {"key_to_keep": "A", "key_to_delete": "B"} | ||
| config_manager.save_config(config_data) | ||
|
|
||
| config_manager.delete_value("key_to_delete") | ||
|
|
||
| config = config_manager.get_config() | ||
| assert "key_to_keep" in config | ||
| assert "key_to_delete" not in config | ||
|
|
||
| def test_delete_nonexistent_value(self, config_manager: ConfigManager): | ||
| """Test that deleting a nonexistent key does not raise an error.""" | ||
| config_data = {"existing_key": "value"} | ||
| config_manager.save_config(config_data) | ||
|
|
||
| config_manager.delete_value("nonexistent_key") | ||
|
|
||
| # Check that the original data is untouched | ||
| assert config_manager.get_config() == config_data |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,16 +1,16 @@ | ||
| import json | ||
| import unittest | ||
| from pathlib import Path | ||
| import json | ||
|
|
||
| from terminara.core.world_handler import load_world | ||
| from terminara.objects.scenario import Scenario, Choice, VariableAction | ||
| from terminara.objects.scenario import Scenario, VariableAction | ||
| from terminara.objects.world_settings import ( | ||
| WorldSettings, | ||
| WorldInfo, | ||
| AiPrompt, | ||
| Item, | ||
| NumericVariable, | ||
| ScenarioSettings, | ||
| ScenarioSettings, TextVariable, | ||
| ) | ||
|
|
||
|
|
||
|
|
@@ -32,6 +32,11 @@ def setUp(self): | |
| "value": 100, | ||
| "min_value": 0, | ||
| "max_value": 100, | ||
| }, | ||
| "rank": { | ||
| "type": "text", | ||
| "description": "Player rank", | ||
| "value": "Novice", | ||
| } | ||
| }, | ||
| "items": { | ||
|
|
@@ -56,6 +61,15 @@ def setUp(self): | |
| "value": "-10" | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "text": "3. Flee into the misty woods", | ||
| "actions": [ | ||
| { | ||
| "item_name": "potion", | ||
| "quantity": 1 | ||
| } | ||
| ] | ||
| } | ||
| ] | ||
| } | ||
|
|
@@ -81,6 +95,8 @@ def test_load_world(self): | |
| self.assertIn("health", world_settings.variables) | ||
| self.assertIsInstance(world_settings.variables["health"], NumericVariable) | ||
| self.assertEqual(world_settings.variables["health"].value, 100) | ||
| self.assertIsInstance(world_settings.variables["rank"], TextVariable) | ||
| self.assertEqual(world_settings.variables["rank"].value, "Novice") | ||
|
|
||
| self.assertIn("potion", world_settings.items) | ||
| self.assertIsInstance(world_settings.items["potion"], Item) | ||
|
|
@@ -91,7 +107,7 @@ def test_load_world(self): | |
| self.assertIsNotNone(world_settings.scenario.init) | ||
| self.assertIsInstance(world_settings.scenario.init, Scenario) | ||
| self.assertEqual(world_settings.scenario.init.text, "You stand at the entrance to a mysterious cave in the test world.") | ||
| self.assertEqual(len(world_settings.scenario.init.choices), 2) | ||
| self.assertEqual(len(world_settings.scenario.init.choices), 3) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. While the test correctly asserts that there are 3 choices, it doesn't verify the content of the newly added third choice. Please add assertions to check its text and actions to ensure the parsing logic is correct for You can add the following assertions at the end of the test method: self.assertEqual(world_settings.scenario.init.choices[2].text, "3. Flee into the misty woods")
self.assertEqual(len(world_settings.scenario.init.choices[2].actions), 1)
self.assertIsInstance(world_settings.scenario.init.choices[2].actions[0], ItemAction)
self.assertEqual(world_settings.scenario.init.choices[2].actions[0].item_name, "potion")
self.assertEqual(world_settings.scenario.init.choices[2].actions[0].quantity, 1)Note that you'll need to import |
||
| self.assertEqual(world_settings.scenario.init.choices[0].text, "1. Enter the cave boldly") | ||
| self.assertEqual(len(world_settings.scenario.init.choices[0].actions), 0) | ||
| self.assertEqual(world_settings.scenario.init.choices[1].text, "2. Peek inside cautiously") | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To properly test the new
ItemActionfunctionality, you should importItemActionhere. This will allow you to useassertIsInstanceto verify the action type intest_load_world, as suggested in another comment.