Skip to content

Commit d4e9367

Browse files
authored
Add comprehensive unit test suite for configuration-related modules (#6)
* Create tests folder and __init__.py * Implement and run tests/test_utils.py * Implement and run tests/test_copy_bettergi_config.py * Implement and run tests/test_launcher.py * Implement and run tests/test_generate_onedragon_config.py * Refactor test suites based on Code Review feedback * Apply code review styling suggestions to tests * Finalize CI workflow and address styling/robustness feedback --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
1 parent 2ce7f6b commit d4e9367

6 files changed

Lines changed: 389 additions & 0 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [ main, master ]
6+
pull_request:
7+
branches: [ main, master ]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- name: Checkout repository
15+
uses: actions/checkout@v4
16+
17+
- name: Set up Python
18+
uses: actions/setup-python@v5
19+
with:
20+
python-version: '3.12'
21+
22+
- name: Install dependencies
23+
run: |
24+
python -m pip install --upgrade pip
25+
pip install pyyaml
26+
27+
- name: Compile Python files
28+
run: |
29+
python -m compileall -q .
30+
31+
- name: Run tests
32+
run: |
33+
python -m unittest discover -s tests -p 'test*.py'

‎tests/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Test package for OneDragon-Helper

‎tests/test_copy_bettergi_config.py‎

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import os
2+
import unittest
3+
import tempfile
4+
import yaml
5+
import shutil
6+
from unittest.mock import patch
7+
import copy_bettergi_config
8+
9+
class TestCopyBettergiConfig(unittest.TestCase):
10+
11+
def setUp(self):
12+
# Create a temporary directory structure for our tests
13+
self.temp_dir = tempfile.TemporaryDirectory()
14+
self.addCleanup(self.temp_dir.cleanup)
15+
16+
# Paths for mock config, mock BGI sources, etc.
17+
self.mock_config_path = os.path.join(self.temp_dir.name, "config.yml")
18+
self.mock_our_bgi_dir = os.path.join(self.temp_dir.name, "BGI_User")
19+
os.makedirs(self.mock_our_bgi_dir, exist_ok=True)
20+
21+
# Put a dummy file inside mock_our_bgi_dir to test file copying
22+
with open(os.path.join(self.mock_our_bgi_dir, "test_file.json"), "w") as f:
23+
f.write('{"test": true}')
24+
25+
@patch('copy_bettergi_config.get_onedragon_yml_path_under_root')
26+
def test_get_BGI_user_dir_success(self, mock_get_yml):
27+
mock_get_yml.return_value = self.mock_config_path
28+
29+
# Case 1: '原神' exists in script_list
30+
config_data = {
31+
'script_list': [
32+
{
33+
'display_name': '鸣潮',
34+
'script_path': 'C:\\Games\\ok-ww\\ok-ww.exe'
35+
},
36+
{
37+
'display_name': '原神',
38+
'script_path': os.path.join(self.temp_dir.name, 'BetterGI', 'BetterGI.exe')
39+
}
40+
]
41+
}
42+
with open(self.mock_config_path, 'w', encoding='utf-8') as f:
43+
yaml.dump(config_data, f)
44+
45+
res = copy_bettergi_config.get_BGI_user_dir()
46+
expected = os.path.join(self.temp_dir.name, 'BetterGI', 'User')
47+
self.assertEqual(os.path.normpath(res), os.path.normpath(expected))
48+
49+
@patch('copy_bettergi_config.get_onedragon_yml_path_under_root')
50+
def test_get_BGI_user_dir_not_found(self, mock_get_yml):
51+
mock_get_yml.return_value = self.mock_config_path
52+
53+
# Case 2: '原神' does not exist in script_list
54+
config_data = {
55+
'script_list': [
56+
{
57+
'display_name': '鸣潮',
58+
'script_path': 'C:\\Games\\ok-ww\\ok-ww.exe'
59+
}
60+
]
61+
}
62+
with open(self.mock_config_path, 'w', encoding='utf-8') as f:
63+
yaml.dump(config_data, f)
64+
65+
res = copy_bettergi_config.get_BGI_user_dir()
66+
self.assertIsNone(res)
67+
68+
@patch('copy_bettergi_config.get_our_bgi_user_dir')
69+
@patch('copy_bettergi_config.get_BGI_user_dir')
70+
def test_copy_BGI_config(self, mock_get_bgi, mock_get_our_bgi):
71+
target_dir = os.path.join(self.temp_dir.name, 'TargetBGI', 'User')
72+
73+
mock_get_our_bgi.return_value = self.mock_our_bgi_dir
74+
mock_get_bgi.return_value = target_dir
75+
76+
# Ensure target_dir does not exist yet
77+
self.assertFalse(os.path.exists(target_dir))
78+
79+
copy_bettergi_config.copy_BGI_config()
80+
81+
# Verify copying actually occurred
82+
self.assertTrue(os.path.exists(os.path.join(target_dir, "test_file.json")))
83+
with open(os.path.join(target_dir, "test_file.json"), "r") as f:
84+
self.assertEqual(f.read(), '{"test": true}')
85+
86+
@patch('copy_bettergi_config.get_BGI_user_dir')
87+
@patch('shutil.copytree')
88+
def test_copy_BGI_config_none(self, mock_copytree, mock_get_bgi):
89+
mock_get_bgi.return_value = None
90+
91+
copy_bettergi_config.copy_BGI_config()
92+
mock_copytree.assert_not_called()
93+
94+
if __name__ == "__main__":
95+
unittest.main()
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import os
2+
import sys
3+
import unittest
4+
import tempfile
5+
import shutil
6+
from unittest.mock import patch, MagicMock
7+
8+
# Global reference to target module and original modules dictionary
9+
generate_onedragon_config = None
10+
original_modules = {}
11+
12+
def setUpModule():
13+
global generate_onedragon_config
14+
# Mock PySide6 and qfluentwidgets to avoid ModuleNotFoundError when importing generate_onedragon_config
15+
for mod in ['PySide6', 'PySide6.QtWidgets', 'PySide6.QtGui', 'qfluentwidgets']:
16+
original_modules[mod] = sys.modules.get(mod)
17+
sys.modules[mod] = MagicMock()
18+
19+
import generate_onedragon_config as goc
20+
generate_onedragon_config = goc
21+
22+
def tearDownModule():
23+
# Restore original modules to avoid affecting other tests in the same process
24+
for mod, orig in original_modules.items():
25+
if orig is None:
26+
sys.modules.pop(mod, None)
27+
else:
28+
sys.modules[mod] = orig
29+
30+
class TestGenerateOnedragonConfig(unittest.TestCase):
31+
32+
def setUp(self):
33+
self.temp_dir = tempfile.TemporaryDirectory()
34+
self.addCleanup(self.temp_dir.cleanup)
35+
36+
# Create temp source directory and target directory
37+
self.src_dir = os.path.join(self.temp_dir.name, "python_script")
38+
self.dest_dir = os.path.join(self.temp_dir.name, "scripts")
39+
os.makedirs(self.src_dir, exist_ok=True)
40+
os.makedirs(self.dest_dir, exist_ok=True)
41+
42+
# Create a dummy script file in source
43+
self.dummy_script_name = "test_script.py"
44+
with open(os.path.join(self.src_dir, self.dummy_script_name), "w") as f:
45+
f.write("# Dummy python script")
46+
47+
@patch('generate_onedragon_config.get_path_under_root')
48+
@patch('generate_onedragon_config.get_path_under_onedragon')
49+
def test_copy_python_scripts_not_exists(self, mock_get_path, mock_get_root):
50+
mock_get_root.return_value = self.src_dir
51+
mock_get_path.return_value = self.dest_dir
52+
53+
# The file does not exist in destination yet
54+
self.assertFalse(os.path.exists(os.path.join(self.dest_dir, self.dummy_script_name)))
55+
56+
generate_onedragon_config.copy_python_scripts()
57+
58+
# Verify file is copied
59+
self.assertTrue(os.path.exists(os.path.join(self.dest_dir, self.dummy_script_name)))
60+
61+
@patch('generate_onedragon_config.get_path_under_root')
62+
@patch('generate_onedragon_config.get_path_under_onedragon')
63+
@patch('shutil.copy')
64+
def test_copy_python_scripts_already_exists(self, mock_copy, mock_get_path, mock_get_root):
65+
mock_get_root.return_value = self.src_dir
66+
mock_get_path.return_value = self.dest_dir
67+
68+
# Pre-create the file in destination
69+
with open(os.path.join(self.dest_dir, self.dummy_script_name), "w") as f:
70+
f.write("# Pre-existing script")
71+
72+
generate_onedragon_config.copy_python_scripts()
73+
74+
# shutil.copy should not be called
75+
mock_copy.assert_not_called()
76+
77+
@patch('generate_onedragon_config.copy_python_scripts')
78+
@patch('generate_onedragon_config.run_config_ui')
79+
@patch('generate_onedragon_config.get_onedragon_yml_path_under_root')
80+
def test_config_workflow(self, mock_get_yml, mock_run_ui, mock_copy):
81+
mock_get_yml.return_value = "/mock/config.yml"
82+
83+
generate_onedragon_config.config_workflow()
84+
85+
mock_copy.assert_called_once()
86+
mock_run_ui.assert_called_once_with("/mock/config.yml")
87+
88+
if __name__ == "__main__":
89+
unittest.main()

‎tests/test_launcher.py‎

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import os
2+
import unittest
3+
import tempfile
4+
import yaml
5+
import copy
6+
from unittest.mock import patch, MagicMock
7+
import launcher
8+
9+
class TestLauncher(unittest.TestCase):
10+
11+
def setUp(self):
12+
self.temp_dir = tempfile.TemporaryDirectory()
13+
self.addCleanup(self.temp_dir.cleanup)
14+
15+
# Paths
16+
self.mock_root_dir = os.path.join(self.temp_dir.name, "root")
17+
self.mock_onedragon_dir = os.path.join(self.temp_dir.name, "onedragon")
18+
os.makedirs(self.mock_root_dir, exist_ok=True)
19+
os.makedirs(self.mock_onedragon_dir, exist_ok=True)
20+
21+
self.config_yml_path = os.path.join(self.mock_root_dir, "config.yml")
22+
23+
# Mock configuration data
24+
self.config_data = {
25+
'script_list': [
26+
{
27+
'display_name': 'Test Script 1',
28+
# Timeout values corresponding to Mon (100) through Sun (700), i.e., indices 0-6
29+
'weekly_timeouts': [100, 200, 300, 400, 500, 600, 700],
30+
'run_timeout_seconds': 0
31+
},
32+
{
33+
'display_name': 'Test Script 2 (No timeouts)',
34+
'run_timeout_seconds': 50
35+
}
36+
]
37+
}
38+
with open(self.config_yml_path, 'w', encoding='utf-8') as f:
39+
yaml.dump(self.config_data, f)
40+
41+
def test_get_week_num(self):
42+
wk = launcher.get_week_num()
43+
self.assertIsInstance(wk, int)
44+
self.assertTrue(0 <= wk <= 6)
45+
46+
@patch('launcher.get_week_num')
47+
@patch('launcher.get_root_dir')
48+
@patch('launcher.get_path_under_onedragon')
49+
def test_generate_OneDragon_script_chain_success(self, mock_get_path, mock_get_root, mock_get_week):
50+
mock_get_root.return_value = self.mock_root_dir
51+
52+
# Create output directory for script_chain config
53+
out_dir = os.path.join(self.mock_onedragon_dir, "config", "script_chain")
54+
os.makedirs(out_dir, exist_ok=True)
55+
mock_get_path.return_value = out_dir
56+
57+
# Test for Wednesday (weekday index 2)
58+
wednesday_index = 2
59+
wednesday_timeout = 300
60+
mock_get_week.return_value = wednesday_index
61+
62+
launcher.generate_OneDragon_script_chain()
63+
64+
output_file = os.path.join(out_dir, "01.yml")
65+
self.assertTrue(os.path.exists(output_file))
66+
67+
with open(output_file, 'r', encoding='utf-8') as f:
68+
output_data = yaml.safe_load(f)
69+
70+
# Assertions
71+
scripts = output_data.get('script_list', [])
72+
self.assertEqual(scripts[0]['run_timeout_seconds'], wednesday_timeout)
73+
self.assertEqual(scripts[1]['run_timeout_seconds'], 50) # Unchanged since no weekly_timeouts
74+
75+
@patch('launcher.get_week_num')
76+
@patch('launcher.get_root_dir')
77+
@patch('launcher.get_path_under_onedragon')
78+
def test_generate_OneDragon_script_chain_invalid_timeout_length(self, mock_get_path, mock_get_root, mock_get_week):
79+
mock_get_root.return_value = self.mock_root_dir
80+
out_dir = os.path.join(self.mock_onedragon_dir, "config", "script_chain")
81+
os.makedirs(out_dir, exist_ok=True)
82+
mock_get_path.return_value = out_dir
83+
mock_get_week.return_value = 0
84+
85+
# Write config with wrong timeout list size (6 instead of 7)
86+
invalid_config = copy.deepcopy(self.config_data)
87+
invalid_config['script_list'][0]['weekly_timeouts'] = [100, 200, 300, 400, 500, 600]
88+
with open(self.config_yml_path, 'w', encoding='utf-8') as f:
89+
yaml.dump(invalid_config, f)
90+
91+
with self.assertRaises(AssertionError):
92+
launcher.generate_OneDragon_script_chain()
93+
94+
@patch('launcher.get_path_under_onedragon')
95+
@patch('subprocess.run')
96+
def test_run_launcher(self, mock_run, mock_get_path):
97+
mock_get_path.return_value = "/mock/src/dir"
98+
99+
mock_res = MagicMock()
100+
mock_res.returncode = 123
101+
mock_run.return_value = mock_res
102+
103+
ret = launcher.run_launcher()
104+
105+
self.assertEqual(ret, 123)
106+
mock_get_path.assert_called_once_with("src")
107+
mock_run.assert_called_once()
108+
args, kwargs = mock_run.call_args
109+
self.assertEqual(kwargs['cwd'], "/mock/src/dir")
110+
self.assertIn("script_chainer.win_exe.launcher", args[0])
111+
112+
if __name__ == "__main__":
113+
unittest.main()

‎tests/test_utils.py‎

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import os
2+
import unittest
3+
import tempfile
4+
from unittest.mock import patch
5+
import utils
6+
7+
class TestUtils(unittest.TestCase):
8+
9+
def test_get_root_dir(self):
10+
root_dir = utils.get_root_dir()
11+
self.assertTrue(os.path.isabs(root_dir))
12+
self.assertTrue(os.path.isdir(root_dir))
13+
14+
def test_get_our_bgi_user_dir(self):
15+
bgi_user_dir = utils.get_our_bgi_user_dir()
16+
root_dir = utils.get_root_dir()
17+
self.assertEqual(bgi_user_dir, os.path.join(root_dir, "BGI_User"))
18+
19+
def test_get_onedragon_yml_path_under_root(self):
20+
yml_path = utils.get_onedragon_yml_path_under_root()
21+
root_dir = utils.get_root_dir()
22+
self.assertEqual(yml_path, os.path.join(root_dir, "config.yml"))
23+
24+
def test_get_path_under_root(self):
25+
# Without subdirs
26+
path = utils.get_path_under_root()
27+
self.assertEqual(path, utils.get_root_dir())
28+
29+
# With subdirs (using mock to avoid actually creating it if it doesn't exist)
30+
with patch('utils.join_dir_path_with_mk') as mock_join:
31+
mock_join.return_value = "mock_path"
32+
res = utils.get_path_under_root("sub1", "sub2")
33+
self.assertEqual(res, "mock_path")
34+
mock_join.assert_called_once_with(utils.get_root_dir(), "sub1", "sub2")
35+
36+
def test_get_path_under_onedragon(self):
37+
with patch('utils.join_dir_path_with_mk') as mock_join:
38+
mock_join.return_value = "mock_path"
39+
res = utils.get_path_under_onedragon("sub1")
40+
self.assertEqual(res, "mock_path")
41+
mock_join.assert_called_once_with(utils.get_root_dir(), "OneDragon-ScriptChainer", "sub1")
42+
43+
def test_join_dir_path_with_mk(self):
44+
with tempfile.TemporaryDirectory() as temp_dir:
45+
# Let's test joining normal subdirectories
46+
res = utils.join_dir_path_with_mk(temp_dir, "sub1", "sub2")
47+
expected = os.path.normpath(os.path.join(temp_dir, "sub1", "sub2"))
48+
self.assertEqual(os.path.normpath(res), expected)
49+
self.assertTrue(os.path.isdir(expected))
50+
51+
# Let's test handling None in subs
52+
res_none = utils.join_dir_path_with_mk(temp_dir, "sub3", None, "sub4")
53+
expected_none = os.path.normpath(os.path.join(temp_dir, "sub3", "sub4"))
54+
self.assertEqual(os.path.normpath(res_none), expected_none)
55+
self.assertTrue(os.path.isdir(expected_none))
56+
57+
if __name__ == "__main__":
58+
unittest.main()

0 commit comments

Comments
 (0)