[ADD] test: add unit tests for refactoring
Ensure safe refactoring by covering all 6 target scripts with 121 tests: execute.py, todo_i18n.py, kill_process_by_port.py, git_tool.py, and todo.py. Generated by Claude Code 2.1.72 model claude-sonnet-4-6 Co-Authored-By: Mathieu Benoit <mathben@technolibre.ca>
This commit is contained in:
parent
f78f578552
commit
054c12af6d
5 changed files with 1323 additions and 0 deletions
169
test/test_execute.py
Normal file
169
test/test_execute.py
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
#!/usr/bin/env python3
|
||||
# © 2025 TechnoLibre (http://www.technolibre.ca)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
|
||||
import os
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from script.execute.execute import Execute
|
||||
|
||||
|
||||
class TestExecuteInit(unittest.TestCase):
|
||||
"""Test Execute class initialization."""
|
||||
|
||||
@patch("shutil.which")
|
||||
def test_init_with_gnome_terminal(self, mock_which):
|
||||
mock_which.side_effect = lambda x: (
|
||||
"/usr/bin/gnome-terminal" if x == "gnome-terminal" else None
|
||||
)
|
||||
exe = Execute()
|
||||
self.assertIn("gnome-terminal", exe.cmd_source_erplibre)
|
||||
self.assertIn(".venv.erplibre", exe.cmd_source_erplibre)
|
||||
self.assertIn("gnome-terminal", exe.cmd_source_default)
|
||||
|
||||
@patch("shutil.which")
|
||||
def test_init_with_osascript(self, mock_which):
|
||||
mock_which.side_effect = lambda x: (
|
||||
"/usr/bin/osascript" if x == "osascript" else None
|
||||
)
|
||||
exe = Execute()
|
||||
self.assertIn("osascript", exe.cmd_source_erplibre)
|
||||
|
||||
@patch("shutil.which", return_value=None)
|
||||
def test_init_fallback_source(self, mock_which):
|
||||
exe = Execute()
|
||||
self.assertIn(".venv.erplibre/bin/activate", exe.cmd_source_erplibre)
|
||||
self.assertEqual(exe.cmd_source_default, "")
|
||||
|
||||
|
||||
class TestExecCommandLive(unittest.TestCase):
|
||||
"""Test exec_command_live method."""
|
||||
|
||||
def setUp(self):
|
||||
with patch("shutil.which", return_value=None):
|
||||
self.exe = Execute()
|
||||
|
||||
def test_simple_command_returns_zero(self):
|
||||
result = self.exe.exec_command_live(
|
||||
"echo hello",
|
||||
source_erplibre=False,
|
||||
quiet=True,
|
||||
)
|
||||
self.assertEqual(result, 0)
|
||||
|
||||
def test_failing_command_returns_nonzero(self):
|
||||
result = self.exe.exec_command_live(
|
||||
"exit 42",
|
||||
source_erplibre=False,
|
||||
quiet=True,
|
||||
)
|
||||
self.assertEqual(result, 42)
|
||||
|
||||
def test_return_status_and_output(self):
|
||||
status, output = self.exe.exec_command_live(
|
||||
"echo hello",
|
||||
source_erplibre=False,
|
||||
quiet=True,
|
||||
return_status_and_output=True,
|
||||
)
|
||||
self.assertEqual(status, 0)
|
||||
self.assertEqual(output, ["hello"])
|
||||
|
||||
def test_return_status_and_output_multiline(self):
|
||||
status, output = self.exe.exec_command_live(
|
||||
"echo -e 'line1\nline2\nline3'",
|
||||
source_erplibre=False,
|
||||
quiet=True,
|
||||
return_status_and_output=True,
|
||||
)
|
||||
self.assertEqual(status, 0)
|
||||
self.assertEqual(output, ["line1", "line2", "line3"])
|
||||
|
||||
def test_return_status_and_command(self):
|
||||
status, cmd = self.exe.exec_command_live(
|
||||
"echo test",
|
||||
source_erplibre=False,
|
||||
quiet=True,
|
||||
return_status_and_command=True,
|
||||
)
|
||||
self.assertEqual(status, 0)
|
||||
self.assertEqual(cmd, "echo test")
|
||||
|
||||
def test_return_status_and_output_and_command(self):
|
||||
status, cmd, output = self.exe.exec_command_live(
|
||||
"echo result",
|
||||
source_erplibre=False,
|
||||
quiet=True,
|
||||
return_status_and_output_and_command=True,
|
||||
)
|
||||
self.assertEqual(status, 0)
|
||||
self.assertEqual(cmd, "echo result")
|
||||
self.assertEqual(output, ["result"])
|
||||
|
||||
def test_source_erplibre_prepends_activate(self):
|
||||
status, cmd = self.exe.exec_command_live(
|
||||
"echo test",
|
||||
source_erplibre=True,
|
||||
quiet=True,
|
||||
return_status_and_command=True,
|
||||
)
|
||||
self.assertIn(".venv.erplibre/bin/activate", cmd)
|
||||
|
||||
def test_single_source_erplibre(self):
|
||||
status, cmd = self.exe.exec_command_live(
|
||||
"echo test",
|
||||
source_erplibre=False,
|
||||
single_source_erplibre=True,
|
||||
quiet=True,
|
||||
return_status_and_command=True,
|
||||
)
|
||||
self.assertIn(".venv.erplibre/bin/activate", cmd)
|
||||
self.assertIn("echo test", cmd)
|
||||
|
||||
def test_single_source_odoo_no_version_returns_error(self):
|
||||
with patch("os.path.exists", return_value=False):
|
||||
result = self.exe.exec_command_live(
|
||||
"echo test",
|
||||
source_erplibre=False,
|
||||
single_source_odoo=True,
|
||||
source_odoo="",
|
||||
quiet=True,
|
||||
)
|
||||
self.assertEqual(result, -1)
|
||||
|
||||
def test_single_source_odoo_with_version(self):
|
||||
status, cmd = self.exe.exec_command_live(
|
||||
"echo test",
|
||||
source_erplibre=False,
|
||||
single_source_odoo=True,
|
||||
source_odoo="odoo18",
|
||||
quiet=True,
|
||||
return_status_and_command=True,
|
||||
)
|
||||
self.assertIn(".venv.odoo18/bin/activate", cmd)
|
||||
|
||||
def test_new_env_passed_to_subprocess(self):
|
||||
status, output = self.exe.exec_command_live(
|
||||
"echo $MY_TEST_VAR",
|
||||
source_erplibre=False,
|
||||
quiet=True,
|
||||
new_env={"MY_TEST_VAR": "test_value_123"},
|
||||
return_status_and_output=True,
|
||||
)
|
||||
self.assertEqual(status, 0)
|
||||
self.assertEqual(output, ["test_value_123"])
|
||||
|
||||
def test_empty_output_command(self):
|
||||
status, output = self.exe.exec_command_live(
|
||||
"true",
|
||||
source_erplibre=False,
|
||||
quiet=True,
|
||||
return_status_and_output=True,
|
||||
)
|
||||
self.assertEqual(status, 0)
|
||||
self.assertEqual(output, [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
327
test/test_git_tool.py
Normal file
327
test/test_git_tool.py
Normal file
|
|
@ -0,0 +1,327 @@
|
|||
#!/usr/bin/env python3
|
||||
# © 2025 TechnoLibre (http://www.technolibre.ca)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from collections import OrderedDict
|
||||
from unittest.mock import MagicMock, mock_open, patch
|
||||
|
||||
from script.git.git_tool import (
|
||||
CST_EL_GITHUB_TOKEN,
|
||||
CST_FILE_SOURCE_REPO_ADDONS,
|
||||
DEFAULT_PROJECT_NAME,
|
||||
DEFAULT_REMOTE_URL,
|
||||
DEFAULT_WEBSITE,
|
||||
GitTool,
|
||||
Struct,
|
||||
)
|
||||
|
||||
|
||||
class TestStruct(unittest.TestCase):
|
||||
def test_basic_attributes(self):
|
||||
s = Struct(a=1, b="hello")
|
||||
self.assertEqual(s.a, 1)
|
||||
self.assertEqual(s.b, "hello")
|
||||
|
||||
def test_empty_struct(self):
|
||||
s = Struct()
|
||||
self.assertEqual(s.__dict__, {})
|
||||
|
||||
def test_override_existing(self):
|
||||
s = Struct(x=10)
|
||||
self.assertEqual(s.x, 10)
|
||||
|
||||
|
||||
class TestGetUrl(unittest.TestCase):
|
||||
def test_https_to_git(self):
|
||||
url, url_https, url_git = GitTool.get_url(
|
||||
"https://github.com/OCA/server-tools.git"
|
||||
)
|
||||
self.assertEqual(url, "https://github.com/OCA/server-tools.git")
|
||||
self.assertEqual(url_https, "https://github.com/OCA/server-tools.git")
|
||||
self.assertEqual(url_git, "git@github.com:OCA/server-tools.git")
|
||||
|
||||
def test_git_to_https(self):
|
||||
url, url_https, url_git = GitTool.get_url(
|
||||
"git@github.com:OCA/server-tools.git"
|
||||
)
|
||||
self.assertEqual(url_https, "https://github.com/OCA/server-tools.git")
|
||||
self.assertEqual(url_git, "git@github.com:OCA/server-tools.git")
|
||||
|
||||
def test_https_without_git_suffix(self):
|
||||
url, url_https, url_git = GitTool.get_url(
|
||||
"https://github.com/ERPLibre/ERPLibre"
|
||||
)
|
||||
self.assertEqual(url_https, "https://github.com/ERPLibre/ERPLibre")
|
||||
self.assertEqual(url_git, "git@github.com:ERPLibre/ERPLibre")
|
||||
|
||||
|
||||
class TestGetTransformedRepoInfo(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.gt = GitTool()
|
||||
|
||||
def test_https_url_as_submodule(self):
|
||||
result = self.gt.get_transformed_repo_info_from_url(
|
||||
"https://github.com/OCA/server-tools.git",
|
||||
repo_path=".",
|
||||
get_obj=False,
|
||||
)
|
||||
self.assertEqual(result["organization"], "OCA")
|
||||
self.assertEqual(result["repo_name"], "server-tools")
|
||||
self.assertEqual(result["project_name"], "server-tools.git")
|
||||
self.assertEqual(result["path"], "addons/OCA_server-tools")
|
||||
self.assertTrue(result["is_submodule"])
|
||||
|
||||
def test_git_url_as_submodule(self):
|
||||
result = self.gt.get_transformed_repo_info_from_url(
|
||||
"git@github.com:ERPLibre/erplibre_addons.git",
|
||||
repo_path=".",
|
||||
get_obj=False,
|
||||
)
|
||||
self.assertEqual(result["organization"], "ERPLibre")
|
||||
self.assertEqual(result["repo_name"], "erplibre_addons")
|
||||
|
||||
def test_not_submodule(self):
|
||||
result = self.gt.get_transformed_repo_info_from_url(
|
||||
"https://github.com/OCA/server-tools.git",
|
||||
repo_path="/tmp/test",
|
||||
get_obj=False,
|
||||
is_submodule=False,
|
||||
)
|
||||
self.assertEqual(result["path"], "/tmp/test")
|
||||
self.assertFalse(result["is_submodule"])
|
||||
|
||||
def test_get_obj_true_returns_struct(self):
|
||||
result = self.gt.get_transformed_repo_info_from_url(
|
||||
"https://github.com/OCA/web.git",
|
||||
get_obj=True,
|
||||
)
|
||||
self.assertIsInstance(result, Struct)
|
||||
self.assertEqual(result.organization, "OCA")
|
||||
|
||||
def test_organization_force(self):
|
||||
result = self.gt.get_transformed_repo_info_from_url(
|
||||
"https://github.com/OCA/server-tools.git",
|
||||
get_obj=False,
|
||||
organization_force="MyOrg",
|
||||
)
|
||||
self.assertEqual(result["organization"], "MyOrg")
|
||||
self.assertEqual(result["original_organization"], "OCA")
|
||||
self.assertIn("MyOrg", result["url_https"])
|
||||
|
||||
def test_custom_sub_path(self):
|
||||
result = self.gt.get_transformed_repo_info_from_url(
|
||||
"https://github.com/OCA/server-tools.git",
|
||||
get_obj=False,
|
||||
sub_path="custom",
|
||||
)
|
||||
self.assertEqual(result["path"], "custom/OCA_server-tools")
|
||||
|
||||
def test_empty_sub_path(self):
|
||||
result = self.gt.get_transformed_repo_info_from_url(
|
||||
"https://github.com/OCA/server-tools.git",
|
||||
get_obj=False,
|
||||
sub_path="",
|
||||
)
|
||||
self.assertEqual(result["path"], "server-tools")
|
||||
|
||||
def test_dot_sub_path(self):
|
||||
result = self.gt.get_transformed_repo_info_from_url(
|
||||
"https://github.com/OCA/server-tools.git",
|
||||
get_obj=False,
|
||||
sub_path=".",
|
||||
)
|
||||
self.assertEqual(result["path"], "server-tools")
|
||||
|
||||
def test_revision_and_clone_depth(self):
|
||||
result = self.gt.get_transformed_repo_info_from_url(
|
||||
"https://github.com/OCA/web.git",
|
||||
get_obj=False,
|
||||
revision="16.0",
|
||||
clone_depth="1",
|
||||
)
|
||||
self.assertEqual(result["revision"], "16.0")
|
||||
self.assertEqual(result["clone_depth"], "1")
|
||||
|
||||
def test_url_without_git_suffix(self):
|
||||
result = self.gt.get_transformed_repo_info_from_url(
|
||||
"https://github.com/OCA/server-tools",
|
||||
get_obj=False,
|
||||
)
|
||||
self.assertEqual(result["repo_name"], "server-tools")
|
||||
|
||||
|
||||
class TestDefaultProperties(unittest.TestCase):
|
||||
def test_default_project_name(self):
|
||||
gt = GitTool()
|
||||
self.assertEqual(gt.default_project_name, DEFAULT_PROJECT_NAME)
|
||||
|
||||
def test_default_website(self):
|
||||
gt = GitTool()
|
||||
self.assertEqual(gt.default_website, DEFAULT_WEBSITE)
|
||||
|
||||
def test_default_remote_url(self):
|
||||
gt = GitTool()
|
||||
self.assertEqual(gt.default_remote_url, DEFAULT_REMOTE_URL)
|
||||
|
||||
@patch(
|
||||
"builtins.open",
|
||||
mock_open(read_data="18.0"),
|
||||
)
|
||||
def test_odoo_version(self):
|
||||
gt = GitTool()
|
||||
self.assertEqual(gt.odoo_version, "18.0")
|
||||
|
||||
@patch(
|
||||
"builtins.open",
|
||||
mock_open(read_data="16.0"),
|
||||
)
|
||||
def test_odoo_version_long(self):
|
||||
gt = GitTool()
|
||||
self.assertEqual(gt.odoo_version_long, "odoo16.0")
|
||||
|
||||
|
||||
class TestStrInsert(unittest.TestCase):
|
||||
def test_insert_middle(self):
|
||||
result = GitTool.str_insert("abcdef", "XY", 3)
|
||||
self.assertEqual(result, "abcXYdef")
|
||||
|
||||
def test_insert_beginning(self):
|
||||
result = GitTool.str_insert("hello", "X", 0)
|
||||
self.assertEqual(result, "Xhello")
|
||||
|
||||
def test_insert_end(self):
|
||||
result = GitTool.str_insert("hello", "X", 5)
|
||||
self.assertEqual(result, "helloX")
|
||||
|
||||
|
||||
class TestGetProjectConfig(unittest.TestCase):
|
||||
def test_reads_github_token(self):
|
||||
content = (
|
||||
"#!/bin/bash\n"
|
||||
'EL_GITHUB_TOKEN="my_token_123"\n'
|
||||
'OTHER_VAR="value"\n'
|
||||
)
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".sh", delete=False, dir="/tmp"
|
||||
) as f:
|
||||
f.write(content)
|
||||
f.flush()
|
||||
tmpdir = os.path.dirname(f.name)
|
||||
tmpname = os.path.basename(f.name)
|
||||
try:
|
||||
# We need env_var.sh in a directory
|
||||
env_var_path = os.path.join(tmpdir, "env_var.sh")
|
||||
os.rename(f.name, env_var_path)
|
||||
result = GitTool.get_project_config(repo_path=tmpdir)
|
||||
self.assertEqual(result[CST_EL_GITHUB_TOKEN], "my_token_123")
|
||||
finally:
|
||||
if os.path.exists(env_var_path):
|
||||
os.unlink(env_var_path)
|
||||
|
||||
|
||||
class TestGetRepoInfoSubmodule(unittest.TestCase):
|
||||
def test_parses_gitmodules(self):
|
||||
gitmodules_content = (
|
||||
'[submodule "addons/OCA_server-tools"]\n'
|
||||
"\turl = https://github.com/OCA/server-tools.git\n"
|
||||
"\tpath = addons/OCA_server-tools\n"
|
||||
"\n"
|
||||
'[submodule "addons/OCA_web"]\n'
|
||||
"\turl = https://github.com/OCA/web.git\n"
|
||||
"\tpath = addons/OCA_web\n"
|
||||
)
|
||||
gt = GitTool()
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
gitmodules_path = os.path.join(tmpdir, ".gitmodules")
|
||||
with open(gitmodules_path, "w") as f:
|
||||
f.write(gitmodules_content)
|
||||
|
||||
result = gt.get_repo_info_submodule(
|
||||
repo_path=tmpdir, add_root=False
|
||||
)
|
||||
self.assertEqual(len(result), 2)
|
||||
names = [r["name"] for r in result]
|
||||
self.assertIn("addons/OCA_server-tools", names)
|
||||
self.assertIn("addons/OCA_web", names)
|
||||
|
||||
def test_single_submodule(self):
|
||||
gitmodules_content = (
|
||||
'[submodule "addons/test"]\n'
|
||||
"\turl = https://github.com/Test/repo.git\n"
|
||||
"\tpath = addons/test\n"
|
||||
)
|
||||
gt = GitTool()
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
with open(os.path.join(tmpdir, ".gitmodules"), "w") as f:
|
||||
f.write(gitmodules_content)
|
||||
|
||||
result = gt.get_repo_info_submodule(repo_path=tmpdir)
|
||||
self.assertEqual(len(result), 1)
|
||||
self.assertEqual(result[0]["name"], "addons/test")
|
||||
self.assertIn("https://", result[0]["url_https"])
|
||||
self.assertIn("git@", result[0]["url_git"])
|
||||
|
||||
|
||||
class TestGetManifestXmlInfo(unittest.TestCase):
|
||||
def test_parses_manifest(self):
|
||||
xml_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<manifest>
|
||||
<remote name="OCA" fetch="https://github.com/OCA/"/>
|
||||
<default remote="OCA" revision="16.0"/>
|
||||
<project name="server-tools.git" path="addons/OCA_server-tools"
|
||||
remote="OCA" groups="odoo16.0"/>
|
||||
</manifest>
|
||||
"""
|
||||
gt = GitTool()
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".xml", delete=False
|
||||
) as f:
|
||||
f.write(xml_content)
|
||||
f.flush()
|
||||
try:
|
||||
dct_remote, dct_project, default_remote = (
|
||||
gt.get_manifest_xml_info(filename=f.name)
|
||||
)
|
||||
self.assertIn("OCA", dct_remote)
|
||||
self.assertIn("server-tools.git", dct_project)
|
||||
self.assertEqual(default_remote["@remote"], "OCA")
|
||||
finally:
|
||||
os.unlink(f.name)
|
||||
|
||||
def test_empty_manifest(self):
|
||||
xml_content = """<?xml version="1.0" encoding="UTF-8"?>
|
||||
<manifest/>
|
||||
"""
|
||||
gt = GitTool()
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".xml", delete=False
|
||||
) as f:
|
||||
f.write(xml_content)
|
||||
f.flush()
|
||||
try:
|
||||
dct_remote, dct_project, default_remote = (
|
||||
gt.get_manifest_xml_info(filename=f.name)
|
||||
)
|
||||
self.assertEqual(dct_remote, {})
|
||||
self.assertEqual(dct_project, {})
|
||||
self.assertIsNone(default_remote)
|
||||
finally:
|
||||
os.unlink(f.name)
|
||||
|
||||
|
||||
class TestConstants(unittest.TestCase):
|
||||
def test_file_source_repo_addons(self):
|
||||
self.assertEqual(CST_FILE_SOURCE_REPO_ADDONS, "source_repo_addons.csv")
|
||||
|
||||
def test_default_project_name(self):
|
||||
self.assertEqual(DEFAULT_PROJECT_NAME, "ERPLibre")
|
||||
|
||||
def test_default_website(self):
|
||||
self.assertEqual(DEFAULT_WEBSITE, "erplibre.ca")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
251
test/test_kill_process_by_port.py
Normal file
251
test/test_kill_process_by_port.py
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
#!/usr/bin/env python3
|
||||
# © 2025 TechnoLibre (http://www.technolibre.ca)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import psutil
|
||||
|
||||
from script.process.kill_process_by_port import (
|
||||
PROTECTED_NAMES,
|
||||
STOP_PARENT_KILL,
|
||||
choose_target,
|
||||
find_listeners,
|
||||
get_ancestry,
|
||||
kill_process,
|
||||
kill_tree,
|
||||
proc_desc,
|
||||
)
|
||||
|
||||
|
||||
def _make_proc(pid, name="python", cmdline=None, username="user"):
|
||||
"""Create a mock psutil.Process."""
|
||||
p = MagicMock(spec=psutil.Process)
|
||||
p.pid = pid
|
||||
p.name.return_value = name
|
||||
p.cmdline.return_value = cmdline or [name]
|
||||
p.username.return_value = username
|
||||
return p
|
||||
|
||||
|
||||
class TestProcDesc(unittest.TestCase):
|
||||
def test_normal_process(self):
|
||||
p = _make_proc(123, "python", ["python", "app.py"])
|
||||
result = proc_desc(p)
|
||||
self.assertIn("pid=123", result)
|
||||
self.assertIn("python app.py", result)
|
||||
self.assertIn("user=user", result)
|
||||
|
||||
def test_empty_cmdline_uses_name(self):
|
||||
p = _make_proc(42, "bash", [])
|
||||
result = proc_desc(p)
|
||||
self.assertIn("pid=42", result)
|
||||
self.assertIn("name=bash", result)
|
||||
|
||||
def test_psutil_error_fallback(self):
|
||||
p = MagicMock(spec=psutil.Process)
|
||||
p.pid = 99
|
||||
p.cmdline.side_effect = psutil.Error("gone")
|
||||
result = proc_desc(p)
|
||||
self.assertEqual(result, "pid=99")
|
||||
|
||||
|
||||
class TestGetAncestry(unittest.TestCase):
|
||||
@patch("script.process.kill_process_by_port.psutil.Process")
|
||||
def test_nonexistent_pid(self, mock_proc_cls):
|
||||
mock_proc_cls.side_effect = psutil.NoSuchProcess(99999)
|
||||
chain = get_ancestry(99999)
|
||||
self.assertEqual(chain, [])
|
||||
|
||||
@patch("script.process.kill_process_by_port.psutil.Process")
|
||||
def test_chain_stops_at_pid_1(self, mock_proc_cls):
|
||||
p_child = MagicMock()
|
||||
p_child.pid = 100
|
||||
p_child.ppid.return_value = 1
|
||||
|
||||
p_init = MagicMock()
|
||||
p_init.pid = 1
|
||||
|
||||
mock_proc_cls.side_effect = [p_child, p_init]
|
||||
chain = get_ancestry(100)
|
||||
self.assertEqual(len(chain), 2)
|
||||
self.assertEqual(chain[0].pid, 100)
|
||||
self.assertEqual(chain[1].pid, 1)
|
||||
|
||||
@patch("script.process.kill_process_by_port.psutil.Process")
|
||||
def test_chain_stops_at_ppid_zero(self, mock_proc_cls):
|
||||
p = MagicMock()
|
||||
p.pid = 50
|
||||
p.ppid.return_value = 0
|
||||
|
||||
mock_proc_cls.return_value = p
|
||||
chain = get_ancestry(50)
|
||||
self.assertEqual(len(chain), 1)
|
||||
self.assertEqual(chain[0].pid, 50)
|
||||
|
||||
|
||||
class TestChooseTarget(unittest.TestCase):
|
||||
def test_empty_chain_returns_none(self):
|
||||
result = choose_target([], 1)
|
||||
self.assertIsNone(result)
|
||||
|
||||
def test_stops_at_run_sh(self):
|
||||
p1 = _make_proc(100, "python", ["python", "odoo-bin"])
|
||||
p2 = _make_proc(99, "bash", ["./run.sh"])
|
||||
p3 = _make_proc(1, "systemd", ["systemd"])
|
||||
chain = [p1, p2, p3]
|
||||
target, lst = choose_target(chain, 1)
|
||||
self.assertEqual(target.pid, 99)
|
||||
|
||||
def test_stops_at_odoo_bin_sh(self):
|
||||
p1 = _make_proc(200, "python", ["python", "odoo-bin"])
|
||||
p2 = _make_proc(199, "bash", ["./odoo_bin.sh"])
|
||||
chain = [p1, p2]
|
||||
target, lst = choose_target(chain, 1)
|
||||
self.assertEqual(target.pid, 199)
|
||||
|
||||
def test_no_stop_marker_uses_nb_parent(self):
|
||||
p1 = _make_proc(300, "python", ["python"])
|
||||
p2 = _make_proc(299, "bash", ["bash"])
|
||||
p3 = _make_proc(1, "systemd", ["systemd"])
|
||||
chain = [p1, p2, p3]
|
||||
target, lst = choose_target(chain, 2)
|
||||
self.assertEqual(target.pid, 299)
|
||||
|
||||
|
||||
class TestKillProcess(unittest.TestCase):
|
||||
def test_terminate_called(self):
|
||||
p = _make_proc(10)
|
||||
kill_process(p, force=False)
|
||||
p.terminate.assert_called_once()
|
||||
p.kill.assert_not_called()
|
||||
|
||||
def test_kill_called_with_force(self):
|
||||
p = _make_proc(10)
|
||||
kill_process(p, force=True)
|
||||
p.kill.assert_called_once()
|
||||
p.terminate.assert_not_called()
|
||||
|
||||
|
||||
class TestKillTree(unittest.TestCase):
|
||||
@patch("script.process.kill_process_by_port.psutil.wait_procs")
|
||||
def test_kills_children_then_root(self, mock_wait):
|
||||
root = _make_proc(10, "root_proc")
|
||||
child1 = _make_proc(11, "child1")
|
||||
child2 = _make_proc(12, "child2")
|
||||
root.children.return_value = [child1, child2]
|
||||
mock_wait.return_value = (
|
||||
[root, child1, child2],
|
||||
[],
|
||||
)
|
||||
|
||||
alive = kill_tree(root, force=False)
|
||||
child1.terminate.assert_called_once()
|
||||
child2.terminate.assert_called_once()
|
||||
root.terminate.assert_called_once()
|
||||
self.assertEqual(alive, [])
|
||||
|
||||
@patch("script.process.kill_process_by_port.psutil.wait_procs")
|
||||
def test_returns_alive_processes(self, mock_wait):
|
||||
root = _make_proc(10)
|
||||
root.children.return_value = []
|
||||
mock_wait.return_value = ([], [root])
|
||||
|
||||
alive = kill_tree(root, force=True)
|
||||
self.assertEqual(len(alive), 1)
|
||||
|
||||
@patch("script.process.kill_process_by_port.psutil.wait_procs")
|
||||
def test_handles_psutil_error_on_child(self, mock_wait):
|
||||
root = _make_proc(10)
|
||||
child = _make_proc(11)
|
||||
child.terminate.side_effect = psutil.NoSuchProcess(11)
|
||||
root.children.return_value = [child]
|
||||
mock_wait.return_value = ([root], [])
|
||||
|
||||
alive = kill_tree(root, force=False)
|
||||
self.assertEqual(alive, [])
|
||||
|
||||
|
||||
class TestFindListeners(unittest.TestCase):
|
||||
@patch("script.process.kill_process_by_port.psutil.net_connections")
|
||||
def test_finds_listening_pid(self, mock_conns):
|
||||
conn = MagicMock()
|
||||
conn.laddr = MagicMock()
|
||||
conn.laddr.port = 8069
|
||||
conn.status = psutil.CONN_LISTEN
|
||||
conn.pid = 1234
|
||||
mock_conns.return_value = [conn]
|
||||
|
||||
result = find_listeners(8069)
|
||||
self.assertEqual(result, [1234])
|
||||
|
||||
@patch("script.process.kill_process_by_port.psutil.net_connections")
|
||||
def test_ignores_different_port(self, mock_conns):
|
||||
conn = MagicMock()
|
||||
conn.laddr = MagicMock()
|
||||
conn.laddr.port = 9999
|
||||
conn.status = psutil.CONN_LISTEN
|
||||
conn.pid = 1234
|
||||
mock_conns.return_value = [conn]
|
||||
|
||||
result = find_listeners(8069)
|
||||
self.assertEqual(result, [])
|
||||
|
||||
@patch("script.process.kill_process_by_port.psutil.net_connections")
|
||||
def test_ignores_non_listen_status(self, mock_conns):
|
||||
conn = MagicMock()
|
||||
conn.laddr = MagicMock()
|
||||
conn.laddr.port = 8069
|
||||
conn.status = psutil.CONN_ESTABLISHED
|
||||
conn.pid = 1234
|
||||
mock_conns.return_value = [conn]
|
||||
|
||||
result = find_listeners(8069)
|
||||
self.assertEqual(result, [])
|
||||
|
||||
@patch("script.process.kill_process_by_port.psutil.net_connections")
|
||||
def test_no_connections(self, mock_conns):
|
||||
mock_conns.return_value = []
|
||||
result = find_listeners(8069)
|
||||
self.assertEqual(result, [])
|
||||
|
||||
@patch("script.process.kill_process_by_port.psutil.net_connections")
|
||||
def test_deduplicates_pids(self, mock_conns):
|
||||
conn1 = MagicMock()
|
||||
conn1.laddr = MagicMock()
|
||||
conn1.laddr.port = 8069
|
||||
conn1.status = psutil.CONN_LISTEN
|
||||
conn1.pid = 100
|
||||
conn2 = MagicMock()
|
||||
conn2.laddr = MagicMock()
|
||||
conn2.laddr.port = 8069
|
||||
conn2.status = psutil.CONN_LISTEN
|
||||
conn2.pid = 100
|
||||
mock_conns.return_value = [conn1, conn2]
|
||||
|
||||
result = find_listeners(8069)
|
||||
self.assertEqual(result, [100])
|
||||
|
||||
|
||||
class TestProtectedNames(unittest.TestCase):
|
||||
def test_systemd_is_protected(self):
|
||||
self.assertIn("systemd", PROTECTED_NAMES)
|
||||
|
||||
def test_gnome_shell_is_protected(self):
|
||||
self.assertIn("gnome-shell", PROTECTED_NAMES)
|
||||
|
||||
def test_sshd_is_protected(self):
|
||||
self.assertIn("sshd", PROTECTED_NAMES)
|
||||
|
||||
|
||||
class TestStopParentKill(unittest.TestCase):
|
||||
def test_contains_run_sh(self):
|
||||
self.assertIn("./run.sh", STOP_PARENT_KILL)
|
||||
|
||||
def test_contains_odoo_bin_sh(self):
|
||||
self.assertIn("./odoo_bin.sh", STOP_PARENT_KILL)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
332
test/test_todo.py
Normal file
332
test/test_todo.py
Normal file
|
|
@ -0,0 +1,332 @@
|
|||
#!/usr/bin/env python3
|
||||
# © 2025 TechnoLibre (http://www.technolibre.ca)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from script.todo.todo import (
|
||||
ANDROID_DIR,
|
||||
CONFIG_FILE,
|
||||
CONFIG_OVERRIDE_FILE,
|
||||
ENABLE_CRASH,
|
||||
GRADLE_FILE,
|
||||
INSTALLED_ODOO_VERSION_FILE,
|
||||
LOGO_ASCII_FILE,
|
||||
MOBILE_HOME_PATH,
|
||||
ODOO_VERSION_FILE,
|
||||
STRINGS_FILE,
|
||||
TODO,
|
||||
VERSION_DATA_FILE,
|
||||
cst_venv_erplibre,
|
||||
file_error_path,
|
||||
)
|
||||
|
||||
|
||||
class TestTODOInit(unittest.TestCase):
|
||||
def test_initial_attributes(self):
|
||||
todo = TODO()
|
||||
self.assertIsNone(todo.dir_path)
|
||||
self.assertIsNone(todo.kdbx)
|
||||
self.assertIsNone(todo.file_path)
|
||||
self.assertIsNotNone(todo.config_file)
|
||||
self.assertIsNotNone(todo.execute)
|
||||
|
||||
|
||||
class TestFillHelpInfo(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.todo = TODO()
|
||||
|
||||
@patch("script.todo.todo.t")
|
||||
def test_basic_help_info(self, mock_t):
|
||||
mock_t.side_effect = lambda k: {
|
||||
"command": "Command:",
|
||||
"back": "Back",
|
||||
}.get(k, k)
|
||||
lst_choice = [
|
||||
{"prompt_description": "Option A"},
|
||||
{"prompt_description": "Option B"},
|
||||
]
|
||||
result = self.todo.fill_help_info(lst_choice)
|
||||
self.assertIn("[1] Option A", result)
|
||||
self.assertIn("[2] Option B", result)
|
||||
self.assertIn("[0] Back", result)
|
||||
|
||||
@patch("script.todo.todo.t")
|
||||
def test_with_prompt_description_key(self, mock_t):
|
||||
mock_t.side_effect = lambda k: {
|
||||
"command": "Command:",
|
||||
"back": "Back",
|
||||
"my_key": "Translated Description",
|
||||
}.get(k, k)
|
||||
lst_choice = [
|
||||
{
|
||||
"prompt_description": "fallback",
|
||||
"prompt_description_key": "my_key",
|
||||
},
|
||||
]
|
||||
result = self.todo.fill_help_info(lst_choice)
|
||||
self.assertIn("[1] Translated Description", result)
|
||||
|
||||
@patch("script.todo.todo.t")
|
||||
def test_empty_list(self, mock_t):
|
||||
mock_t.side_effect = lambda k: {
|
||||
"command": "Command:",
|
||||
"back": "Back",
|
||||
}.get(k, k)
|
||||
result = self.todo.fill_help_info([])
|
||||
self.assertIn("Command:", result)
|
||||
self.assertIn("[0] Back", result)
|
||||
self.assertNotIn("[1]", result)
|
||||
|
||||
|
||||
class TestGetOdooVersion(unittest.TestCase):
|
||||
def test_reads_version_data(self):
|
||||
version_data = {
|
||||
"odoo18.0_python3.12.10": {
|
||||
"odoo_version": "18.0",
|
||||
"python_version": "3.12.10",
|
||||
"default": True,
|
||||
"is_deprecated": False,
|
||||
},
|
||||
"odoo16.0_python3.10.18": {
|
||||
"odoo_version": "16.0",
|
||||
"python_version": "3.10.18",
|
||||
"default": False,
|
||||
"is_deprecated": False,
|
||||
},
|
||||
}
|
||||
todo = TODO()
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
version_file = os.path.join(tmpdir, "version.json")
|
||||
with open(version_file, "w") as f:
|
||||
json.dump(version_data, f)
|
||||
|
||||
odoo_version_file = os.path.join(tmpdir, ".odoo-version")
|
||||
with open(odoo_version_file, "w") as f:
|
||||
f.write("18.0")
|
||||
|
||||
with patch(
|
||||
"script.todo.todo.VERSION_DATA_FILE", version_file
|
||||
), patch(
|
||||
"script.todo.todo.INSTALLED_ODOO_VERSION_FILE",
|
||||
os.path.join(tmpdir, "nonexistent.txt"),
|
||||
), patch(
|
||||
"script.todo.todo.ODOO_VERSION_FILE",
|
||||
odoo_version_file,
|
||||
):
|
||||
lst_version, lst_installed, odoo_current = (
|
||||
todo.get_odoo_version()
|
||||
)
|
||||
|
||||
self.assertEqual(len(lst_version), 2)
|
||||
self.assertEqual(odoo_current, "odoo18.0")
|
||||
# Check erplibre_version was added
|
||||
names = [v["erplibre_version"] for v in lst_version]
|
||||
self.assertIn("odoo18.0_python3.12.10", names)
|
||||
self.assertIn("odoo16.0_python3.10.18", names)
|
||||
|
||||
def test_installed_versions_read(self):
|
||||
version_data = {
|
||||
"odoo18.0_python3.12.10": {
|
||||
"odoo_version": "18.0",
|
||||
"python_version": "3.12.10",
|
||||
"default": True,
|
||||
"is_deprecated": False,
|
||||
},
|
||||
}
|
||||
todo = TODO()
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
version_file = os.path.join(tmpdir, "version.json")
|
||||
with open(version_file, "w") as f:
|
||||
json.dump(version_data, f)
|
||||
|
||||
installed_file = os.path.join(tmpdir, "installed.txt")
|
||||
with open(installed_file, "w") as f:
|
||||
f.write("odoo18.0\nodoo16.0\n")
|
||||
|
||||
with patch(
|
||||
"script.todo.todo.VERSION_DATA_FILE", version_file
|
||||
), patch(
|
||||
"script.todo.todo.INSTALLED_ODOO_VERSION_FILE",
|
||||
installed_file,
|
||||
), patch(
|
||||
"script.todo.todo.ODOO_VERSION_FILE",
|
||||
os.path.join(tmpdir, "nonexistent"),
|
||||
):
|
||||
lst_version, lst_installed, odoo_current = (
|
||||
todo.get_odoo_version()
|
||||
)
|
||||
|
||||
self.assertEqual(lst_installed, ["odoo16.0", "odoo18.0"])
|
||||
self.assertIsNone(odoo_current)
|
||||
|
||||
def test_no_version_data_raises(self):
|
||||
todo = TODO()
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
version_file = os.path.join(tmpdir, "empty.json")
|
||||
with open(version_file, "w") as f:
|
||||
json.dump({}, f)
|
||||
|
||||
with patch("script.todo.todo.VERSION_DATA_FILE", version_file):
|
||||
with self.assertRaises(Exception):
|
||||
todo.get_odoo_version()
|
||||
|
||||
|
||||
class TestOnDirSelected(unittest.TestCase):
|
||||
@patch("script.todo.todo.todo_file_browser", create=True)
|
||||
def test_sets_dir_path(self, mock_browser):
|
||||
todo = TODO()
|
||||
todo.on_dir_selected("/some/path")
|
||||
self.assertEqual(todo.dir_path, "/some/path")
|
||||
|
||||
|
||||
class TestExecuteFromConfiguration(unittest.TestCase):
|
||||
def test_with_command(self):
|
||||
todo = TODO()
|
||||
todo.execute = MagicMock()
|
||||
dct = {"command": "./run.sh"}
|
||||
todo.execute_from_configuration(dct)
|
||||
todo.execute.exec_command_live.assert_called()
|
||||
|
||||
def test_with_makefile_cmd(self):
|
||||
todo = TODO()
|
||||
todo.execute = MagicMock()
|
||||
todo.execute.exec_command_live.return_value = 0
|
||||
dct = {"makefile_cmd": "run_test"}
|
||||
todo.execute_from_configuration(dct)
|
||||
call_args = todo.execute.exec_command_live.call_args
|
||||
self.assertIn("make run_test", call_args[0][0])
|
||||
|
||||
def test_makefile_cmd_ignored_when_flag(self):
|
||||
todo = TODO()
|
||||
todo.execute = MagicMock()
|
||||
dct = {"makefile_cmd": "run_test"}
|
||||
todo.execute_from_configuration(dct, ignore_makefile=True)
|
||||
todo.execute.exec_command_live.assert_not_called()
|
||||
|
||||
def test_with_callback(self):
|
||||
todo = TODO()
|
||||
todo.execute = MagicMock()
|
||||
callback = MagicMock()
|
||||
dct = {"callback": callback}
|
||||
todo.execute_from_configuration(dct)
|
||||
callback.assert_called_once_with(dct)
|
||||
|
||||
def test_makefile_error_stops_execution(self):
|
||||
todo = TODO()
|
||||
todo.execute = MagicMock()
|
||||
todo.execute.exec_command_live.return_value = 1
|
||||
callback = MagicMock()
|
||||
dct = {"makefile_cmd": "broken", "callback": callback}
|
||||
todo.execute_from_configuration(dct)
|
||||
callback.assert_not_called()
|
||||
|
||||
|
||||
class TestConstants(unittest.TestCase):
|
||||
def test_config_file_path(self):
|
||||
self.assertEqual(CONFIG_FILE, "./script/todo/todo.json")
|
||||
|
||||
def test_config_override_path(self):
|
||||
self.assertEqual(CONFIG_OVERRIDE_FILE, "./private/todo/todo.json")
|
||||
|
||||
def test_logo_path(self):
|
||||
self.assertEqual(LOGO_ASCII_FILE, "./script/todo/logo_ascii.txt")
|
||||
|
||||
def test_venv_erplibre(self):
|
||||
self.assertEqual(cst_venv_erplibre, ".venv.erplibre")
|
||||
|
||||
def test_file_error_path(self):
|
||||
self.assertEqual(file_error_path, ".erplibre.error.txt")
|
||||
|
||||
def test_version_data_file(self):
|
||||
self.assertEqual(
|
||||
VERSION_DATA_FILE,
|
||||
os.path.join("conf", "supported_version_erplibre.json"),
|
||||
)
|
||||
|
||||
def test_mobile_paths(self):
|
||||
self.assertEqual(ANDROID_DIR, "android")
|
||||
self.assertIn("mobile", MOBILE_HOME_PATH)
|
||||
|
||||
|
||||
class TestDeployGitServer(unittest.TestCase):
|
||||
def test_local_mode(self):
|
||||
todo = TODO()
|
||||
todo.execute = MagicMock()
|
||||
todo._deploy_git_server(production_ready=False, action="init")
|
||||
cmd = todo.execute.exec_command_live.call_args[0][0]
|
||||
self.assertIn("--action init", cmd)
|
||||
self.assertNotIn("--production-ready", cmd)
|
||||
|
||||
def test_production_mode(self):
|
||||
todo = TODO()
|
||||
todo.execute = MagicMock()
|
||||
todo._deploy_git_server(production_ready=True, action="all")
|
||||
cmd = todo.execute.exec_command_live.call_args[0][0]
|
||||
self.assertIn("--production-ready", cmd)
|
||||
self.assertIn("--action all", cmd)
|
||||
|
||||
|
||||
class TestProcessKillGitDaemon(unittest.TestCase):
|
||||
def test_calls_pkill(self):
|
||||
todo = TODO()
|
||||
todo.execute = MagicMock()
|
||||
todo.process_kill_git_daemon()
|
||||
cmd = todo.execute.exec_command_live.call_args[0][0]
|
||||
self.assertIn("pkill", cmd)
|
||||
self.assertIn("git daemon", cmd)
|
||||
|
||||
|
||||
class TestExecuteUnitTests(unittest.TestCase):
|
||||
def test_success_path(self):
|
||||
todo = TODO()
|
||||
todo.execute = MagicMock()
|
||||
todo.execute.exec_command_live.return_value = (0, ["OK"])
|
||||
with patch("builtins.print") as mock_print:
|
||||
todo.execute_unit_tests()
|
||||
cmd = todo.execute.exec_command_live.call_args[0][0]
|
||||
self.assertIn("unittest discover", cmd)
|
||||
|
||||
def test_failure_path(self):
|
||||
todo = TODO()
|
||||
todo.execute = MagicMock()
|
||||
todo.execute.exec_command_live.return_value = (1, ["FAIL"])
|
||||
with patch("builtins.print") as mock_print:
|
||||
todo.execute_unit_tests()
|
||||
# Verify it was called - error handling path
|
||||
|
||||
|
||||
class TestKdbxGetExtraCommandUser(unittest.TestCase):
|
||||
def test_empty_kdbx_key(self):
|
||||
todo = TODO()
|
||||
result = todo.kdbx_get_extra_command_user("")
|
||||
self.assertEqual(result, "")
|
||||
|
||||
def test_none_kdbx_key(self):
|
||||
todo = TODO()
|
||||
result = todo.kdbx_get_extra_command_user(None)
|
||||
self.assertEqual(result, "")
|
||||
|
||||
def test_kdbx_not_available(self):
|
||||
todo = TODO()
|
||||
todo.get_kdbx = MagicMock(return_value=None)
|
||||
result = todo.kdbx_get_extra_command_user("some_key")
|
||||
self.assertEqual(result, "")
|
||||
|
||||
|
||||
class TestSetupClaudeCommit(unittest.TestCase):
|
||||
def test_existing_file_skips(self):
|
||||
todo = TODO()
|
||||
with patch("os.path.exists", return_value=True), patch(
|
||||
"builtins.print"
|
||||
) as mock_print:
|
||||
todo._setup_claude_commit()
|
||||
# Should print exists message without asking for input
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
244
test/test_todo_i18n.py
Normal file
244
test/test_todo_i18n.py
Normal file
|
|
@ -0,0 +1,244 @@
|
|||
#!/usr/bin/env python3
|
||||
# © 2025 TechnoLibre (http://www.technolibre.ca)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from script.todo import todo_i18n
|
||||
|
||||
|
||||
class TestTranslations(unittest.TestCase):
|
||||
"""Test TRANSLATIONS dictionary integrity."""
|
||||
|
||||
def test_all_entries_have_fr_and_en(self):
|
||||
for key, entry in todo_i18n.TRANSLATIONS.items():
|
||||
self.assertIn("fr", entry, f"Key '{key}' missing 'fr' translation")
|
||||
self.assertIn("en", entry, f"Key '{key}' missing 'en' translation")
|
||||
|
||||
def test_no_empty_translations(self):
|
||||
for key, entry in todo_i18n.TRANSLATIONS.items():
|
||||
for lang in ("fr", "en"):
|
||||
self.assertTrue(
|
||||
len(entry[lang]) > 0,
|
||||
f"Key '{key}' has empty '{lang}' translation",
|
||||
)
|
||||
|
||||
def test_translations_not_empty(self):
|
||||
self.assertGreater(len(todo_i18n.TRANSLATIONS), 0)
|
||||
|
||||
|
||||
class TestT(unittest.TestCase):
|
||||
"""Test t() translation function."""
|
||||
|
||||
def setUp(self):
|
||||
todo_i18n._current_lang = None
|
||||
|
||||
def tearDown(self):
|
||||
todo_i18n._current_lang = None
|
||||
|
||||
def test_returns_french_when_lang_fr(self):
|
||||
todo_i18n.set_lang("fr")
|
||||
result = todo_i18n.t("menu_quit")
|
||||
self.assertEqual(result, "Quitter")
|
||||
|
||||
def test_returns_english_when_lang_en(self):
|
||||
todo_i18n.set_lang("en")
|
||||
result = todo_i18n.t("menu_quit")
|
||||
self.assertEqual(result, "Quit")
|
||||
|
||||
def test_unknown_key_returns_key(self):
|
||||
todo_i18n.set_lang("fr")
|
||||
result = todo_i18n.t("nonexistent_key_xyz")
|
||||
self.assertEqual(result, "nonexistent_key_xyz")
|
||||
|
||||
def test_fallback_to_fr_if_lang_missing(self):
|
||||
todo_i18n.set_lang("de")
|
||||
result = todo_i18n.t("menu_quit")
|
||||
self.assertEqual(result, "Quitter")
|
||||
|
||||
|
||||
class TestGetLang(unittest.TestCase):
|
||||
"""Test get_lang() function."""
|
||||
|
||||
def setUp(self):
|
||||
todo_i18n._current_lang = None
|
||||
|
||||
def tearDown(self):
|
||||
todo_i18n._current_lang = None
|
||||
|
||||
def test_returns_cached_lang(self):
|
||||
todo_i18n._current_lang = "en"
|
||||
result = todo_i18n.get_lang()
|
||||
self.assertEqual(result, "en")
|
||||
|
||||
def test_reads_from_env_var_sh(self):
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".sh", delete=False
|
||||
) as f:
|
||||
f.write('EL_LANG="en"\n')
|
||||
f.flush()
|
||||
try:
|
||||
with patch.object(
|
||||
todo_i18n, "CONFIG_OVERRIDE_PRIVATE_FILE", f.name
|
||||
):
|
||||
result = todo_i18n.get_lang()
|
||||
self.assertEqual(result, "en")
|
||||
finally:
|
||||
os.unlink(f.name)
|
||||
|
||||
def test_reads_unquoted_lang(self):
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".sh", delete=False
|
||||
) as f:
|
||||
f.write("EL_LANG=fr\n")
|
||||
f.flush()
|
||||
try:
|
||||
with patch.object(
|
||||
todo_i18n, "CONFIG_OVERRIDE_PRIVATE_FILE", f.name
|
||||
):
|
||||
result = todo_i18n.get_lang()
|
||||
self.assertEqual(result, "fr")
|
||||
finally:
|
||||
os.unlink(f.name)
|
||||
|
||||
def test_env_variable_fallback(self):
|
||||
with patch.object(
|
||||
todo_i18n,
|
||||
"CONFIG_OVERRIDE_PRIVATE_FILE",
|
||||
"/nonexistent/path",
|
||||
), patch.dict(os.environ, {"EL_LANG": "en"}):
|
||||
result = todo_i18n.get_lang()
|
||||
self.assertEqual(result, "en")
|
||||
|
||||
def test_default_is_fr(self):
|
||||
with patch.object(
|
||||
todo_i18n,
|
||||
"CONFIG_OVERRIDE_PRIVATE_FILE",
|
||||
"/nonexistent/path",
|
||||
), patch.dict(os.environ, {}, clear=True):
|
||||
result = todo_i18n.get_lang()
|
||||
self.assertEqual(result, "fr")
|
||||
|
||||
def test_invalid_lang_in_file_falls_through(self):
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".sh", delete=False
|
||||
) as f:
|
||||
f.write('EL_LANG="de"\n')
|
||||
f.flush()
|
||||
try:
|
||||
with patch.object(
|
||||
todo_i18n, "CONFIG_OVERRIDE_PRIVATE_FILE", f.name
|
||||
), patch.dict(os.environ, {}, clear=True):
|
||||
result = todo_i18n.get_lang()
|
||||
self.assertEqual(result, "fr")
|
||||
finally:
|
||||
os.unlink(f.name)
|
||||
|
||||
|
||||
class TestSetLang(unittest.TestCase):
|
||||
"""Test set_lang() function."""
|
||||
|
||||
def setUp(self):
|
||||
todo_i18n._current_lang = None
|
||||
|
||||
def tearDown(self):
|
||||
todo_i18n._current_lang = None
|
||||
|
||||
def test_sets_current_lang(self):
|
||||
todo_i18n.set_lang("en")
|
||||
self.assertEqual(todo_i18n._current_lang, "en")
|
||||
|
||||
def test_persists_to_file_update(self):
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".sh", delete=False
|
||||
) as f:
|
||||
f.write('EL_LANG="fr"\nOTHER=value\n')
|
||||
f.flush()
|
||||
try:
|
||||
with patch.object(
|
||||
todo_i18n, "CONFIG_OVERRIDE_PRIVATE_FILE", f.name
|
||||
):
|
||||
todo_i18n.set_lang("en")
|
||||
with open(f.name) as rf:
|
||||
content = rf.read()
|
||||
self.assertIn('EL_LANG="en"', content)
|
||||
self.assertIn("OTHER=value", content)
|
||||
finally:
|
||||
os.unlink(f.name)
|
||||
|
||||
def test_persists_to_file_append(self):
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".sh", delete=False
|
||||
) as f:
|
||||
f.write("SOME_VAR=123\n")
|
||||
f.flush()
|
||||
try:
|
||||
with patch.object(
|
||||
todo_i18n, "CONFIG_OVERRIDE_PRIVATE_FILE", f.name
|
||||
):
|
||||
todo_i18n.set_lang("en")
|
||||
with open(f.name) as rf:
|
||||
content = rf.read()
|
||||
self.assertIn('EL_LANG="en"', content)
|
||||
self.assertIn("SOME_VAR=123", content)
|
||||
finally:
|
||||
os.unlink(f.name)
|
||||
|
||||
def test_nonexistent_file_no_crash(self):
|
||||
with patch.object(
|
||||
todo_i18n,
|
||||
"CONFIG_OVERRIDE_PRIVATE_FILE",
|
||||
"/nonexistent/path",
|
||||
):
|
||||
todo_i18n.set_lang("en")
|
||||
self.assertEqual(todo_i18n._current_lang, "en")
|
||||
|
||||
|
||||
class TestLangIsConfigured(unittest.TestCase):
|
||||
"""Test lang_is_configured() function."""
|
||||
|
||||
def test_returns_true_when_configured(self):
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".sh", delete=False
|
||||
) as f:
|
||||
f.write('EL_LANG="fr"\n')
|
||||
f.flush()
|
||||
try:
|
||||
with patch.object(
|
||||
todo_i18n, "CONFIG_OVERRIDE_PRIVATE_FILE", f.name
|
||||
):
|
||||
result = todo_i18n.lang_is_configured()
|
||||
self.assertTrue(result)
|
||||
finally:
|
||||
os.unlink(f.name)
|
||||
|
||||
def test_returns_false_when_not_configured(self):
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".sh", delete=False
|
||||
) as f:
|
||||
f.write("SOME_VAR=123\n")
|
||||
f.flush()
|
||||
try:
|
||||
with patch.object(
|
||||
todo_i18n, "CONFIG_OVERRIDE_PRIVATE_FILE", f.name
|
||||
):
|
||||
result = todo_i18n.lang_is_configured()
|
||||
self.assertFalse(result)
|
||||
finally:
|
||||
os.unlink(f.name)
|
||||
|
||||
def test_returns_false_when_file_missing(self):
|
||||
with patch.object(
|
||||
todo_i18n,
|
||||
"CONFIG_OVERRIDE_PRIVATE_FILE",
|
||||
"/nonexistent/path",
|
||||
):
|
||||
result = todo_i18n.lang_is_configured()
|
||||
self.assertFalse(result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Loading…
Reference in a new issue