[ADD] test: add unit tests for uncovered components
Cover addons, code_generator, database, docker, maintenance, poetry, and version scripts to bring test count from 142 to 262. Generated by Claude Code 2.1.74 model claude-sonnet-4-6 Co-Authored-By: Mathieu Benoit <mathben@technolibre.ca>
This commit is contained in:
parent
438edc3694
commit
eb42224df9
7 changed files with 1122 additions and 0 deletions
179
test/test_check_addons_exist.py
Normal file
179
test/test_check_addons_exist.py
Normal file
|
|
@ -0,0 +1,179 @@
|
|||
#!/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 patch
|
||||
|
||||
from script.addons.check_addons_exist import main
|
||||
|
||||
|
||||
class TestMainModuleFound(unittest.TestCase):
|
||||
"""Test main() when modules exist with valid __manifest__.py."""
|
||||
|
||||
def setUp(self):
|
||||
self.tmpdir = tempfile.mkdtemp()
|
||||
# Create a valid addon
|
||||
addon_path = os.path.join(self.tmpdir, "my_addon")
|
||||
os.makedirs(addon_path)
|
||||
with open(os.path.join(addon_path, "__manifest__.py"), "w") as f:
|
||||
f.write("{}")
|
||||
# Create config.conf
|
||||
self.config_path = os.path.join(self.tmpdir, "config.conf")
|
||||
with open(self.config_path, "w") as f:
|
||||
f.write(f"[options]\naddons_path = {self.tmpdir}\n")
|
||||
|
||||
def test_single_module_found(self):
|
||||
with patch(
|
||||
"sys.argv",
|
||||
["prog", "-m", "my_addon", "-c", self.config_path],
|
||||
):
|
||||
result = main()
|
||||
self.assertEqual(result, 0)
|
||||
|
||||
def test_output_json_module_found(self):
|
||||
with patch(
|
||||
"sys.argv",
|
||||
[
|
||||
"prog",
|
||||
"-m",
|
||||
"my_addon",
|
||||
"-c",
|
||||
self.config_path,
|
||||
"--output_json",
|
||||
],
|
||||
):
|
||||
result = main()
|
||||
self.assertEqual(result, 0)
|
||||
|
||||
def test_format_json_output(self):
|
||||
with patch(
|
||||
"sys.argv",
|
||||
[
|
||||
"prog",
|
||||
"-m",
|
||||
"my_addon",
|
||||
"-c",
|
||||
self.config_path,
|
||||
"--output_json",
|
||||
"--format_json",
|
||||
],
|
||||
):
|
||||
result = main()
|
||||
self.assertEqual(result, 0)
|
||||
|
||||
|
||||
class TestMainModuleMissing(unittest.TestCase):
|
||||
"""Test main() when modules do not exist."""
|
||||
|
||||
def setUp(self):
|
||||
self.tmpdir = tempfile.mkdtemp()
|
||||
self.config_path = os.path.join(self.tmpdir, "config.conf")
|
||||
with open(self.config_path, "w") as f:
|
||||
f.write(f"[options]\naddons_path = {self.tmpdir}\n")
|
||||
|
||||
def test_missing_module_returns_1(self):
|
||||
with patch(
|
||||
"sys.argv",
|
||||
["prog", "-m", "nonexistent", "-c", self.config_path],
|
||||
):
|
||||
result = main()
|
||||
self.assertEqual(result, 1)
|
||||
|
||||
def test_missing_module_json(self):
|
||||
with patch(
|
||||
"sys.argv",
|
||||
[
|
||||
"prog",
|
||||
"-m",
|
||||
"nonexistent",
|
||||
"-c",
|
||||
self.config_path,
|
||||
"--output_json",
|
||||
],
|
||||
):
|
||||
result = main()
|
||||
self.assertEqual(result, 1)
|
||||
|
||||
|
||||
class TestMainDuplicateModule(unittest.TestCase):
|
||||
"""Test main() when same module exists in multiple addon paths."""
|
||||
|
||||
def setUp(self):
|
||||
self.tmpdir = tempfile.mkdtemp()
|
||||
path1 = os.path.join(self.tmpdir, "addons1", "dup_mod")
|
||||
path2 = os.path.join(self.tmpdir, "addons2", "dup_mod")
|
||||
os.makedirs(path1)
|
||||
os.makedirs(path2)
|
||||
with open(os.path.join(path1, "__manifest__.py"), "w") as f:
|
||||
f.write("{}")
|
||||
with open(os.path.join(path2, "__manifest__.py"), "w") as f:
|
||||
f.write("{}")
|
||||
addons = (
|
||||
os.path.join(self.tmpdir, "addons1")
|
||||
+ ","
|
||||
+ os.path.join(self.tmpdir, "addons2")
|
||||
)
|
||||
self.config_path = os.path.join(self.tmpdir, "config.conf")
|
||||
with open(self.config_path, "w") as f:
|
||||
f.write(f"[options]\naddons_path = {addons}\n")
|
||||
|
||||
def test_duplicate_returns_2(self):
|
||||
with patch(
|
||||
"sys.argv",
|
||||
["prog", "-m", "dup_mod", "-c", self.config_path],
|
||||
):
|
||||
result = main()
|
||||
self.assertEqual(result, 2)
|
||||
|
||||
|
||||
class TestMainMissingManifest(unittest.TestCase):
|
||||
"""Test main() when directory exists but __manifest__.py is absent."""
|
||||
|
||||
def setUp(self):
|
||||
self.tmpdir = tempfile.mkdtemp()
|
||||
addon_path = os.path.join(self.tmpdir, "empty_addon")
|
||||
os.makedirs(addon_path)
|
||||
self.config_path = os.path.join(self.tmpdir, "config.conf")
|
||||
with open(self.config_path, "w") as f:
|
||||
f.write(f"[options]\naddons_path = {self.tmpdir}\n")
|
||||
|
||||
def test_dir_without_manifest_returns_1(self):
|
||||
with patch(
|
||||
"sys.argv",
|
||||
["prog", "-m", "empty_addon", "-c", self.config_path],
|
||||
):
|
||||
result = main()
|
||||
self.assertEqual(result, 1)
|
||||
|
||||
|
||||
class TestMainBadConfig(unittest.TestCase):
|
||||
"""Test main() with invalid config files."""
|
||||
|
||||
def setUp(self):
|
||||
self.tmpdir = tempfile.mkdtemp()
|
||||
|
||||
def test_missing_options_section(self):
|
||||
config_path = os.path.join(self.tmpdir, "config.conf")
|
||||
with open(config_path, "w") as f:
|
||||
f.write("[other]\nkey = value\n")
|
||||
with patch(
|
||||
"sys.argv",
|
||||
["prog", "-m", "mod", "-c", config_path],
|
||||
):
|
||||
result = main()
|
||||
self.assertEqual(result, -1)
|
||||
|
||||
def test_missing_addons_path_key(self):
|
||||
config_path = os.path.join(self.tmpdir, "config.conf")
|
||||
with open(config_path, "w") as f:
|
||||
f.write("[options]\nother_key = value\n")
|
||||
with patch(
|
||||
"sys.argv",
|
||||
["prog", "-m", "mod", "-c", config_path],
|
||||
):
|
||||
result = main()
|
||||
self.assertEqual(result, -1)
|
||||
199
test/test_code_generator_tools.py
Normal file
199
test/test_code_generator_tools.py
Normal file
|
|
@ -0,0 +1,199 @@
|
|||
#!/usr/bin/env python3
|
||||
# © 2025 TechnoLibre (http://www.technolibre.ca)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
|
||||
import ast
|
||||
import unittest
|
||||
|
||||
from script.code_generator.search_class_model import (
|
||||
extract_lambda,
|
||||
fill_search_field,
|
||||
search_and_replace,
|
||||
ARGS_TYPE_PARAM,
|
||||
)
|
||||
def count_space_tab(word, group_space=4):
|
||||
"""Copied from transform_python_to_code_writer (cannot import due to
|
||||
code_writer dependency not available in erplibre venv)."""
|
||||
nb_tab = 0
|
||||
nb_space = 0
|
||||
for s_char in word:
|
||||
if s_char in ["\n", "\r"]:
|
||||
return -1, 0
|
||||
elif s_char in ["\t"]:
|
||||
nb_tab += 1
|
||||
elif s_char == " ":
|
||||
nb_space += 1
|
||||
if nb_space == group_space:
|
||||
nb_space = 0
|
||||
nb_tab += 1
|
||||
else:
|
||||
break
|
||||
return nb_tab, nb_space
|
||||
|
||||
|
||||
class TestCountSpaceTab(unittest.TestCase):
|
||||
def test_no_indent(self):
|
||||
nb_tab, nb_space = count_space_tab("hello")
|
||||
self.assertEqual(nb_tab, 0)
|
||||
self.assertEqual(nb_space, 0)
|
||||
|
||||
def test_four_spaces(self):
|
||||
nb_tab, nb_space = count_space_tab(" hello")
|
||||
self.assertEqual(nb_tab, 1)
|
||||
self.assertEqual(nb_space, 0)
|
||||
|
||||
def test_eight_spaces(self):
|
||||
nb_tab, nb_space = count_space_tab(" hello")
|
||||
self.assertEqual(nb_tab, 2)
|
||||
self.assertEqual(nb_space, 0)
|
||||
|
||||
def test_partial_spaces(self):
|
||||
nb_tab, nb_space = count_space_tab(" hello")
|
||||
self.assertEqual(nb_tab, 1)
|
||||
self.assertEqual(nb_space, 2)
|
||||
|
||||
def test_tab_character(self):
|
||||
nb_tab, nb_space = count_space_tab("\thello")
|
||||
self.assertEqual(nb_tab, 1)
|
||||
self.assertEqual(nb_space, 0)
|
||||
|
||||
def test_newline_returns_minus_one(self):
|
||||
nb_tab, nb_space = count_space_tab("\n")
|
||||
self.assertEqual(nb_tab, -1)
|
||||
self.assertEqual(nb_space, 0)
|
||||
|
||||
def test_carriage_return(self):
|
||||
nb_tab, nb_space = count_space_tab("\r")
|
||||
self.assertEqual(nb_tab, -1)
|
||||
self.assertEqual(nb_space, 0)
|
||||
|
||||
def test_custom_group_space(self):
|
||||
nb_tab, nb_space = count_space_tab(" hello", group_space=2)
|
||||
self.assertEqual(nb_tab, 1)
|
||||
self.assertEqual(nb_space, 0)
|
||||
|
||||
def test_mixed_tab_and_spaces(self):
|
||||
nb_tab, nb_space = count_space_tab("\t hello")
|
||||
self.assertEqual(nb_tab, 2)
|
||||
self.assertEqual(nb_space, 0)
|
||||
|
||||
|
||||
class TestExtractLambda(unittest.TestCase):
|
||||
def test_simple_lambda(self):
|
||||
node = ast.parse("lambda x: x + 1", mode="eval").body
|
||||
result = extract_lambda(node)
|
||||
self.assertIn("lambda", result)
|
||||
self.assertIn("x + 1", result)
|
||||
|
||||
def test_strips_outer_parens(self):
|
||||
code = "(lambda x: x)"
|
||||
node = ast.parse(code, mode="eval").body
|
||||
result = extract_lambda(node)
|
||||
self.assertFalse(result.startswith("("))
|
||||
|
||||
|
||||
class TestFillSearchField(unittest.TestCase):
|
||||
def test_constant_string(self):
|
||||
node = ast.parse("'hello'", mode="eval").body
|
||||
result = fill_search_field(node)
|
||||
self.assertEqual(result, "hello")
|
||||
|
||||
def test_constant_int(self):
|
||||
node = ast.parse("42", mode="eval").body
|
||||
result = fill_search_field(node)
|
||||
self.assertEqual(result, 42)
|
||||
|
||||
def test_constant_bool(self):
|
||||
node = ast.parse("True", mode="eval").body
|
||||
result = fill_search_field(node)
|
||||
self.assertTrue(result)
|
||||
|
||||
def test_negative_number(self):
|
||||
node = ast.parse("-5", mode="eval").body
|
||||
result = fill_search_field(node)
|
||||
self.assertEqual(result, -5)
|
||||
|
||||
def test_name(self):
|
||||
node = ast.parse("my_var", mode="eval").body
|
||||
result = fill_search_field(node)
|
||||
self.assertEqual(result, "my_var")
|
||||
|
||||
def test_attribute(self):
|
||||
node = ast.parse("fields.Date.today", mode="eval").body
|
||||
result = fill_search_field(node)
|
||||
self.assertEqual(result, "fields.Date.today")
|
||||
|
||||
def test_list(self):
|
||||
node = ast.parse("[1, 2, 3]", mode="eval").body
|
||||
result = fill_search_field(node)
|
||||
self.assertEqual(result, [1, 2, 3])
|
||||
|
||||
def test_dict(self):
|
||||
node = ast.parse("{'a': 1, 'b': 2}", mode="eval").body
|
||||
result = fill_search_field(node)
|
||||
self.assertEqual(result, {"a": 1, "b": 2})
|
||||
|
||||
def test_tuple(self):
|
||||
node = ast.parse("(1, 2)", mode="eval").body
|
||||
result = fill_search_field(node)
|
||||
self.assertEqual(result, (1, 2))
|
||||
|
||||
def test_lambda(self):
|
||||
node = ast.parse("lambda self: self.env", mode="eval").body
|
||||
result = fill_search_field(node)
|
||||
self.assertIn("lambda", result)
|
||||
|
||||
def test_unsupported_returns_none(self):
|
||||
node = ast.parse("{x for x in y}", mode="eval").body
|
||||
result = fill_search_field(node)
|
||||
self.assertIsNone(result)
|
||||
|
||||
|
||||
class TestSearchAndReplace(unittest.TestCase):
|
||||
def test_replace_quoted_value(self):
|
||||
content = 'template_model_name = "old_model"'
|
||||
result = search_and_replace(
|
||||
content, "hooks.py", "new_model"
|
||||
)
|
||||
self.assertIn('"new_model"', result)
|
||||
self.assertNotIn("old_model", result)
|
||||
|
||||
def test_empty_models_name_returns_unchanged(self):
|
||||
content = 'template_model_name = "old"'
|
||||
result = search_and_replace(content, "hooks.py", "")
|
||||
self.assertEqual(result, content)
|
||||
|
||||
def test_missing_search_word_returns_error(self):
|
||||
content = "other_variable = 'value'"
|
||||
result = search_and_replace(content, "hooks.py", "model")
|
||||
self.assertEqual(result, -1)
|
||||
|
||||
def test_custom_search_word(self):
|
||||
content = 'my_custom_var = "old_value"'
|
||||
result = search_and_replace(
|
||||
content,
|
||||
"hooks.py",
|
||||
"new_value",
|
||||
search_word="my_custom_var",
|
||||
)
|
||||
self.assertIn('"new_value"', result)
|
||||
|
||||
|
||||
class TestArgsTypeParam(unittest.TestCase):
|
||||
def test_char_has_string(self):
|
||||
self.assertIn("string", ARGS_TYPE_PARAM["Char"])
|
||||
|
||||
def test_many2one_has_comodel(self):
|
||||
self.assertIn("comodel_name", ARGS_TYPE_PARAM["Many2one"])
|
||||
|
||||
def test_one2many_has_inverse(self):
|
||||
self.assertIn("inverse_name", ARGS_TYPE_PARAM["One2many"])
|
||||
|
||||
def test_many2many_has_relation(self):
|
||||
self.assertIn("relation", ARGS_TYPE_PARAM["Many2many"])
|
||||
|
||||
def test_id_is_empty(self):
|
||||
self.assertEqual(ARGS_TYPE_PARAM["Id"], [])
|
||||
|
||||
def test_selection_has_selection(self):
|
||||
self.assertIn("selection", ARGS_TYPE_PARAM["Selection"])
|
||||
146
test/test_database_tools.py
Normal file
146
test/test_database_tools.py
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
#!/usr/bin/env python3
|
||||
# © 2025 TechnoLibre (http://www.technolibre.ca)
|
||||
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
|
||||
|
||||
import csv
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import unittest
|
||||
import zipfile
|
||||
from unittest.mock import patch
|
||||
|
||||
from script.database.migrate.process_backup_file import process_zip
|
||||
|
||||
|
||||
class TestProcessZip(unittest.TestCase):
|
||||
"""Test ZIP file processing to remove lines containing a keyword."""
|
||||
|
||||
def _create_zip(self, path, filename, content):
|
||||
with zipfile.ZipFile(path, "w") as zf:
|
||||
zf.writestr(filename, content)
|
||||
|
||||
def test_removes_matching_lines(self):
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
input_zip = os.path.join(tmpdir, "input.zip")
|
||||
output_zip = os.path.join(tmpdir, "output.zip")
|
||||
self._create_zip(
|
||||
input_zip,
|
||||
"dump.sql",
|
||||
"keep this line\ndelete secret line\nkeep also\n",
|
||||
)
|
||||
process_zip(input_zip, output_zip, "secret", "dump.sql")
|
||||
with zipfile.ZipFile(output_zip) as zf:
|
||||
content = zf.read("dump.sql").decode()
|
||||
self.assertIn("keep this line", content)
|
||||
self.assertIn("keep also", content)
|
||||
self.assertNotIn("secret", content)
|
||||
|
||||
def test_preserves_all_when_no_match(self):
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
input_zip = os.path.join(tmpdir, "input.zip")
|
||||
output_zip = os.path.join(tmpdir, "output.zip")
|
||||
original = "line1\nline2\nline3\n"
|
||||
self._create_zip(input_zip, "data.sql", original)
|
||||
process_zip(input_zip, output_zip, "nomatch", "data.sql")
|
||||
with zipfile.ZipFile(output_zip) as zf:
|
||||
content = zf.read("data.sql").decode()
|
||||
self.assertIn("line1", content)
|
||||
self.assertIn("line2", content)
|
||||
self.assertIn("line3", content)
|
||||
|
||||
def test_removes_all_matching(self):
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
input_zip = os.path.join(tmpdir, "input.zip")
|
||||
output_zip = os.path.join(tmpdir, "output.zip")
|
||||
self._create_zip(
|
||||
input_zip,
|
||||
"dump.sql",
|
||||
"bad line\nbad again\nbad too\n",
|
||||
)
|
||||
process_zip(input_zip, output_zip, "bad", "dump.sql")
|
||||
with zipfile.ZipFile(output_zip) as zf:
|
||||
content = zf.read("dump.sql").decode()
|
||||
self.assertEqual(content.strip(), "")
|
||||
|
||||
def test_other_files_untouched(self):
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
input_zip = os.path.join(tmpdir, "input.zip")
|
||||
output_zip = os.path.join(tmpdir, "output.zip")
|
||||
with zipfile.ZipFile(input_zip, "w") as zf:
|
||||
zf.writestr("target.sql", "keep\nremove secret\n")
|
||||
zf.writestr("other.txt", "secret stays here\n")
|
||||
process_zip(input_zip, output_zip, "secret", "target.sql")
|
||||
with zipfile.ZipFile(output_zip) as zf:
|
||||
target = zf.read("target.sql").decode()
|
||||
other = zf.read("other.txt").decode()
|
||||
self.assertNotIn("secret", target)
|
||||
self.assertIn("secret", other)
|
||||
|
||||
|
||||
class TestCompareDatabaseApplicationLogic(unittest.TestCase):
|
||||
"""Test CSV set comparison logic used by compare_database_application."""
|
||||
|
||||
def _write_csv(self, path, rows, fieldnames):
|
||||
with open(path, "w", newline="") as f:
|
||||
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
||||
writer.writeheader()
|
||||
writer.writerows(rows)
|
||||
|
||||
def test_identical_csvs(self):
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
csv1 = os.path.join(tmpdir, "a.csv")
|
||||
csv2 = os.path.join(tmpdir, "b.csv")
|
||||
rows = [{"name": "mod1"}, {"name": "mod2"}]
|
||||
self._write_csv(csv1, rows, ["name"])
|
||||
self._write_csv(csv2, rows, ["name"])
|
||||
with open(csv1) as f1, open(csv2) as f2:
|
||||
r1 = csv.DictReader(f1)
|
||||
r2 = csv.DictReader(f2)
|
||||
s1 = {a["name"] for a in r1}
|
||||
s2 = {a["name"] for a in r2}
|
||||
self.assertEqual(s1, s2)
|
||||
self.assertEqual(len(s1.difference(s2)), 0)
|
||||
|
||||
def test_different_csvs(self):
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
csv1 = os.path.join(tmpdir, "a.csv")
|
||||
csv2 = os.path.join(tmpdir, "b.csv")
|
||||
self._write_csv(csv1, [{"name": "mod1"}, {"name": "mod2"}], ["name"])
|
||||
self._write_csv(csv2, [{"name": "mod2"}, {"name": "mod3"}], ["name"])
|
||||
with open(csv1) as f1, open(csv2) as f2:
|
||||
r1 = csv.DictReader(f1)
|
||||
r2 = csv.DictReader(f2)
|
||||
s1 = {a["name"] for a in r1}
|
||||
s2 = {a["name"] for a in r2}
|
||||
self.assertEqual(s1.intersection(s2), {"mod2"})
|
||||
self.assertEqual(s1.difference(s2), {"mod1"})
|
||||
self.assertEqual(s2.difference(s1), {"mod3"})
|
||||
|
||||
def test_empty_csvs(self):
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
csv1 = os.path.join(tmpdir, "a.csv")
|
||||
csv2 = os.path.join(tmpdir, "b.csv")
|
||||
self._write_csv(csv1, [], ["name"])
|
||||
self._write_csv(csv2, [], ["name"])
|
||||
with open(csv1) as f1, open(csv2) as f2:
|
||||
r1 = csv.DictReader(f1)
|
||||
r2 = csv.DictReader(f2)
|
||||
s1 = {a["name"] for a in r1}
|
||||
s2 = {a["name"] for a in r2}
|
||||
self.assertEqual(len(s1.union(s2)), 0)
|
||||
|
||||
def test_one_empty_csv(self):
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
csv1 = os.path.join(tmpdir, "a.csv")
|
||||
csv2 = os.path.join(tmpdir, "b.csv")
|
||||
self._write_csv(csv1, [{"name": "mod1"}], ["name"])
|
||||
self._write_csv(csv2, [], ["name"])
|
||||
with open(csv1) as f1, open(csv2) as f2:
|
||||
r1 = csv.DictReader(f1)
|
||||
r2 = csv.DictReader(f2)
|
||||
s1 = {a["name"] for a in r1}
|
||||
s2 = {a["name"] for a in r2}
|
||||
self.assertEqual(s1.difference(s2), {"mod1"})
|
||||
self.assertEqual(len(s2.difference(s1)), 0)
|
||||
121
test/test_docker_update_version.py
Normal file
121
test/test_docker_update_version.py
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
#!/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 types import SimpleNamespace
|
||||
|
||||
from script.docker.docker_update_version import edit_text, edit_docker_prod
|
||||
|
||||
|
||||
class TestEditText(unittest.TestCase):
|
||||
"""Test docker-compose.yml image version update."""
|
||||
|
||||
def _write_compose(self, content):
|
||||
f = tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".yml", delete=False
|
||||
)
|
||||
f.write(content)
|
||||
f.close()
|
||||
return f.name
|
||||
|
||||
def test_updates_image_after_erplibre(self):
|
||||
path = self._write_compose(
|
||||
"services:\n"
|
||||
" ERPLibre:\n"
|
||||
" image: old:1.0\n"
|
||||
" ports:\n"
|
||||
)
|
||||
config = SimpleNamespace(
|
||||
docker_compose_file=path,
|
||||
prod_version="myrepo:2.0",
|
||||
)
|
||||
edit_text(config)
|
||||
with open(path) as f:
|
||||
content = f.read()
|
||||
self.assertIn("image: myrepo:2.0", content)
|
||||
self.assertNotIn("old:1.0", content)
|
||||
os.unlink(path)
|
||||
|
||||
def test_preserves_other_lines(self):
|
||||
path = self._write_compose(
|
||||
"version: '3'\n"
|
||||
"services:\n"
|
||||
" ERPLibre:\n"
|
||||
" image: old:1.0\n"
|
||||
" postgres:\n"
|
||||
" image: postgres:14\n"
|
||||
)
|
||||
config = SimpleNamespace(
|
||||
docker_compose_file=path,
|
||||
prod_version="new:3.0",
|
||||
)
|
||||
edit_text(config)
|
||||
with open(path) as f:
|
||||
content = f.read()
|
||||
self.assertIn("version: '3'", content)
|
||||
self.assertIn("postgres:14", content)
|
||||
os.unlink(path)
|
||||
|
||||
|
||||
class TestEditDockerProd(unittest.TestCase):
|
||||
"""Test Dockerfile.prod FROM line update."""
|
||||
|
||||
def _write_dockerfile(self, content):
|
||||
f = tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".pkg", delete=False
|
||||
)
|
||||
f.write(content)
|
||||
f.close()
|
||||
return f.name
|
||||
|
||||
def test_updates_from_line(self):
|
||||
path = self._write_dockerfile(
|
||||
"FROM base:old\nRUN apt-get update\n"
|
||||
)
|
||||
config = SimpleNamespace(
|
||||
docker_compose_file="unused",
|
||||
docker_prod=path,
|
||||
base_version="base:new",
|
||||
)
|
||||
edit_docker_prod(config)
|
||||
with open(path) as f:
|
||||
content = f.read()
|
||||
self.assertIn("FROM base:new", content)
|
||||
self.assertNotIn("FROM base:old", content)
|
||||
os.unlink(path)
|
||||
|
||||
def test_preserves_run_lines(self):
|
||||
path = self._write_dockerfile(
|
||||
"FROM base:old\nRUN echo hello\nCOPY . /app\n"
|
||||
)
|
||||
config = SimpleNamespace(
|
||||
docker_compose_file="unused",
|
||||
docker_prod=path,
|
||||
base_version="base:v2",
|
||||
)
|
||||
edit_docker_prod(config)
|
||||
with open(path) as f:
|
||||
content = f.read()
|
||||
self.assertIn("RUN echo hello", content)
|
||||
self.assertIn("COPY . /app", content)
|
||||
os.unlink(path)
|
||||
|
||||
def test_multiple_from_lines(self):
|
||||
path = self._write_dockerfile(
|
||||
"FROM base:old\nRUN build\nFROM base:old\nRUN run\n"
|
||||
)
|
||||
config = SimpleNamespace(
|
||||
docker_compose_file="unused",
|
||||
docker_prod=path,
|
||||
base_version="base:v3",
|
||||
)
|
||||
edit_docker_prod(config)
|
||||
with open(path) as f:
|
||||
lines = f.readlines()
|
||||
from_lines = [l for l in lines if l.startswith("FROM")]
|
||||
for line in from_lines:
|
||||
self.assertIn("base:v3", line)
|
||||
os.unlink(path)
|
||||
140
test/test_format_file_to_commit.py
Normal file
140
test/test_format_file_to_commit.py
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
#!/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 patch, MagicMock
|
||||
|
||||
from script.maintenance.format_file_to_commit import (
|
||||
execute_shell,
|
||||
get_modified_files,
|
||||
)
|
||||
|
||||
MOD = "script.maintenance.format_file_to_commit"
|
||||
|
||||
|
||||
class TestExecuteShell(unittest.TestCase):
|
||||
def test_successful_command(self):
|
||||
code, output = execute_shell("echo hello")
|
||||
self.assertEqual(code, 0)
|
||||
self.assertEqual(output, "hello")
|
||||
|
||||
def test_failed_command(self):
|
||||
code, output = execute_shell("exit 42")
|
||||
self.assertEqual(code, 42)
|
||||
|
||||
def test_stderr_captured(self):
|
||||
code, output = execute_shell("echo err >&2")
|
||||
self.assertEqual(code, 0)
|
||||
self.assertIn("err", output)
|
||||
|
||||
def test_empty_output(self):
|
||||
code, output = execute_shell("true")
|
||||
self.assertEqual(code, 0)
|
||||
self.assertEqual(output, "")
|
||||
|
||||
def test_multiline_output(self):
|
||||
code, output = execute_shell("echo 'a\nb'")
|
||||
self.assertEqual(code, 0)
|
||||
self.assertIn("a", output)
|
||||
self.assertIn("b", output)
|
||||
|
||||
|
||||
class TestGetModifiedFiles(unittest.TestCase):
|
||||
def _mock_git(self, git_output):
|
||||
"""Helper to mock subprocess.run for git status."""
|
||||
mock_result = MagicMock()
|
||||
mock_result.stdout = git_output
|
||||
mock_result.returncode = 0
|
||||
return mock_result
|
||||
|
||||
def _no_repo_no_odoo(self, path):
|
||||
"""Return False for repo bin and .odoo-version, True otherwise."""
|
||||
if path in (".venv.erplibre/bin/repo",):
|
||||
return False
|
||||
return True
|
||||
|
||||
@patch(f"{MOD}.os.path.isfile", return_value=False)
|
||||
@patch(f"{MOD}.os.path.exists")
|
||||
@patch(f"{MOD}.subprocess.run")
|
||||
def test_single_modified_file(self, mock_run, mock_exists, mock_isfile):
|
||||
mock_exists.return_value = False
|
||||
mock_run.return_value = self._mock_git(" M file.py")
|
||||
files = get_modified_files()
|
||||
self.assertIsNotNone(files)
|
||||
self.assertEqual(len(files), 1)
|
||||
self.assertEqual(files[0][0], "M")
|
||||
|
||||
@patch(f"{MOD}.os.path.isfile", return_value=False)
|
||||
@patch(f"{MOD}.os.path.exists", return_value=False)
|
||||
@patch(f"{MOD}.subprocess.run")
|
||||
def test_added_file(self, mock_run, mock_exists, mock_isfile):
|
||||
mock_run.return_value = self._mock_git("A new_file.py")
|
||||
files = get_modified_files()
|
||||
self.assertIsNotNone(files)
|
||||
self.assertEqual(files[0][0], "A")
|
||||
|
||||
@patch(f"{MOD}.os.path.isfile", return_value=False)
|
||||
@patch(f"{MOD}.os.path.exists", return_value=False)
|
||||
@patch(f"{MOD}.subprocess.run")
|
||||
def test_deleted_file_ignored(self, mock_run, mock_exists, mock_isfile):
|
||||
mock_run.return_value = self._mock_git("D deleted.py")
|
||||
files = get_modified_files()
|
||||
self.assertIsNotNone(files)
|
||||
self.assertEqual(len(files), 0)
|
||||
|
||||
@patch(f"{MOD}.os.path.isfile", return_value=False)
|
||||
@patch(f"{MOD}.os.path.exists", return_value=False)
|
||||
@patch(f"{MOD}.subprocess.run")
|
||||
def test_zip_file_ignored(self, mock_run, mock_exists, mock_isfile):
|
||||
mock_run.return_value = self._mock_git("M archive.zip")
|
||||
files = get_modified_files()
|
||||
self.assertIsNotNone(files)
|
||||
self.assertEqual(len(files), 0)
|
||||
|
||||
@patch(f"{MOD}.os.path.isfile", return_value=False)
|
||||
@patch(f"{MOD}.os.path.exists", return_value=False)
|
||||
@patch(f"{MOD}.subprocess.run")
|
||||
def test_tar_gz_file_ignored(self, mock_run, mock_exists, mock_isfile):
|
||||
mock_run.return_value = self._mock_git("M archive.tar.gz")
|
||||
files = get_modified_files()
|
||||
self.assertIsNotNone(files)
|
||||
self.assertEqual(len(files), 0)
|
||||
|
||||
@patch(f"{MOD}.os.path.isfile", return_value=False)
|
||||
@patch(f"{MOD}.os.path.exists", return_value=False)
|
||||
@patch(f"{MOD}.subprocess.run")
|
||||
def test_untracked_file(self, mock_run, mock_exists, mock_isfile):
|
||||
mock_run.return_value = self._mock_git("?? new_file.txt")
|
||||
files = get_modified_files()
|
||||
self.assertIsNotNone(files)
|
||||
self.assertEqual(len(files), 1)
|
||||
self.assertEqual(files[0][0], "??")
|
||||
|
||||
@patch(f"{MOD}.os.path.isfile", return_value=False)
|
||||
@patch(f"{MOD}.os.path.exists", return_value=False)
|
||||
@patch(f"{MOD}.subprocess.run")
|
||||
def test_empty_git_status(self, mock_run, mock_exists, mock_isfile):
|
||||
mock_run.return_value = self._mock_git("")
|
||||
files = get_modified_files()
|
||||
self.assertIsNotNone(files)
|
||||
self.assertEqual(len(files), 0)
|
||||
|
||||
@patch(f"{MOD}.os.path.isfile", return_value=False)
|
||||
@patch(f"{MOD}.os.path.exists", return_value=False)
|
||||
@patch(f"{MOD}.subprocess.run")
|
||||
def test_multiple_statuses(self, mock_run, mock_exists, mock_isfile):
|
||||
mock_run.return_value = self._mock_git(
|
||||
" M modified.py\nA added.py\n?? untracked.py"
|
||||
)
|
||||
files = get_modified_files()
|
||||
self.assertIsNotNone(files)
|
||||
self.assertEqual(len(files), 3)
|
||||
|
||||
@patch(f"{MOD}.subprocess.run")
|
||||
def test_git_error_returns_none(self, mock_run):
|
||||
import subprocess
|
||||
|
||||
mock_run.side_effect = subprocess.CalledProcessError(1, "git")
|
||||
files = get_modified_files()
|
||||
self.assertIsNone(files)
|
||||
94
test/test_iscompatible.py
Normal file
94
test/test_iscompatible.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
#!/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 packaging.version import Version
|
||||
|
||||
from script.poetry.iscompatible import (
|
||||
iscompatible,
|
||||
parse_requirements,
|
||||
string_to_tuple,
|
||||
)
|
||||
|
||||
|
||||
class TestStringToTuple(unittest.TestCase):
|
||||
def test_three_parts(self):
|
||||
self.assertEqual(string_to_tuple("1.0.0"), (1, 0, 0))
|
||||
|
||||
def test_two_parts(self):
|
||||
self.assertEqual(string_to_tuple("2.5"), (2, 5))
|
||||
|
||||
def test_single_part(self):
|
||||
self.assertEqual(string_to_tuple("3"), (3,))
|
||||
|
||||
def test_large_numbers(self):
|
||||
self.assertEqual(string_to_tuple("10.20.300"), (10, 20, 300))
|
||||
|
||||
|
||||
class TestParseRequirements(unittest.TestCase):
|
||||
def test_exact_match(self):
|
||||
result = parse_requirements("foo==1.0.0")
|
||||
self.assertEqual(result, [("==", "1.0.0")])
|
||||
|
||||
def test_greater_equal(self):
|
||||
result = parse_requirements("foo>=1.1.0")
|
||||
self.assertEqual(result, [(">=", "1.1.0")])
|
||||
|
||||
def test_multiple_constraints(self):
|
||||
result = parse_requirements("foo>=1.1.0, <1.2")
|
||||
self.assertEqual(result, [(">=", "1.1.0"), ("<", "1.2")])
|
||||
|
||||
def test_not_equal(self):
|
||||
result = parse_requirements("bar!=2.0")
|
||||
self.assertEqual(result, [("!=", "2.0")])
|
||||
|
||||
def test_less_equal(self):
|
||||
result = parse_requirements("baz<=3.0.0")
|
||||
self.assertEqual(result, [("<=", "3.0.0")])
|
||||
|
||||
def test_no_version_returns_empty(self):
|
||||
result = parse_requirements("foo")
|
||||
self.assertEqual(result, [])
|
||||
|
||||
def test_with_comment(self):
|
||||
result = parse_requirements("foo>=1.0 # some comment")
|
||||
self.assertEqual(result, [(">=", "1.0")])
|
||||
|
||||
|
||||
class TestIsCompatible(unittest.TestCase):
|
||||
def test_exact_match_true(self):
|
||||
self.assertTrue(iscompatible("foo==1.0.0", Version("1.0.0")))
|
||||
|
||||
def test_exact_match_false(self):
|
||||
self.assertFalse(iscompatible("foo==1.0.0", Version("1.0.1")))
|
||||
|
||||
def test_greater_equal_true(self):
|
||||
self.assertTrue(iscompatible("foo>=5", Version("5.6.1")))
|
||||
|
||||
def test_greater_equal_false(self):
|
||||
self.assertFalse(iscompatible("foo>=5.6.1, <5.7", Version("5.0.0")))
|
||||
|
||||
def test_range_true(self):
|
||||
self.assertTrue(iscompatible("foo>=1.1, <2.1", Version("2.0.0")))
|
||||
|
||||
def test_range_false(self):
|
||||
self.assertFalse(iscompatible("foo>=1.1, <2.0", Version("2.0.0")))
|
||||
|
||||
def test_less_equal(self):
|
||||
self.assertTrue(iscompatible("foo<=1", Version("0.9.0")))
|
||||
|
||||
def test_greater_than(self):
|
||||
self.assertTrue(iscompatible("foo>1.0", Version("1.0.1")))
|
||||
|
||||
def test_greater_than_equal(self):
|
||||
# Version (1,0,0) > (1,0) is True because tuple comparison
|
||||
# extends shorter tuple, so this is actually compatible
|
||||
self.assertTrue(iscompatible("foo>1.0", Version("1.0.0")))
|
||||
|
||||
def test_not_equal_true(self):
|
||||
self.assertTrue(iscompatible("foo!=1.0.0", Version("1.0.1")))
|
||||
|
||||
def test_not_equal_false(self):
|
||||
self.assertFalse(iscompatible("foo!=1.0.0", Version("1.0.0")))
|
||||
243
test/test_version.py
Normal file
243
test/test_version.py
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
#!/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 sys
|
||||
import tempfile
|
||||
import unittest
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from script.version.update_env_version import (
|
||||
Update,
|
||||
remove_dot_path,
|
||||
die,
|
||||
ERPLIBRE_TEMPLATE_VERSION,
|
||||
VENV_TEMPLATE_FILE,
|
||||
MANIFEST_TEMPLATE_FILE,
|
||||
PYPROJECT_TEMPLATE_FILE,
|
||||
POETRY_LOCK_TEMPLATE_FILE,
|
||||
ADDONS_TEMPLATE_FILE,
|
||||
ODOO_TEMPLATE_FILE,
|
||||
)
|
||||
|
||||
|
||||
class TestRemoveDotPath(unittest.TestCase):
|
||||
def test_removes_dot_slash(self):
|
||||
self.assertEqual(remove_dot_path("./path/to/file"), "path/to/file")
|
||||
|
||||
def test_no_dot_slash(self):
|
||||
self.assertEqual(remove_dot_path("path/to/file"), "path/to/file")
|
||||
|
||||
def test_just_dot_slash(self):
|
||||
self.assertEqual(remove_dot_path("./"), "")
|
||||
|
||||
def test_nested_dot_slash(self):
|
||||
self.assertEqual(
|
||||
remove_dot_path("./a/./b"), "a/./b"
|
||||
)
|
||||
|
||||
def test_empty_string(self):
|
||||
self.assertEqual(remove_dot_path(""), "")
|
||||
|
||||
def test_single_file(self):
|
||||
self.assertEqual(remove_dot_path("file.txt"), "file.txt")
|
||||
|
||||
|
||||
class TestDie(unittest.TestCase):
|
||||
def test_die_true_exits(self):
|
||||
with self.assertRaises(SystemExit) as cm:
|
||||
die(True, "error message")
|
||||
self.assertEqual(cm.exception.code, 1)
|
||||
|
||||
def test_die_false_no_exit(self):
|
||||
die(False, "no error")
|
||||
|
||||
def test_die_custom_code(self):
|
||||
with self.assertRaises(SystemExit) as cm:
|
||||
die(True, "error", code=42)
|
||||
self.assertEqual(cm.exception.code, 42)
|
||||
|
||||
|
||||
class TestConstants(unittest.TestCase):
|
||||
def test_erplibre_template_version(self):
|
||||
result = ERPLIBRE_TEMPLATE_VERSION % ("18.0", "3.12.10")
|
||||
self.assertEqual(result, "odoo18.0_python3.12.10")
|
||||
|
||||
def test_venv_template(self):
|
||||
result = VENV_TEMPLATE_FILE % "odoo18.0_python3.12.10"
|
||||
self.assertEqual(result, ".venv.odoo18.0_python3.12.10")
|
||||
|
||||
def test_manifest_template(self):
|
||||
result = MANIFEST_TEMPLATE_FILE % "18.0"
|
||||
self.assertEqual(result, "default.dev.odoo18.0.xml")
|
||||
|
||||
def test_pyproject_template(self):
|
||||
result = PYPROJECT_TEMPLATE_FILE % "odoo18.0_python3.12.10"
|
||||
self.assertEqual(
|
||||
result, "pyproject.odoo18.0_python3.12.10.toml"
|
||||
)
|
||||
|
||||
def test_poetry_lock_template(self):
|
||||
result = POETRY_LOCK_TEMPLATE_FILE % "odoo18.0_python3.12.10"
|
||||
self.assertEqual(
|
||||
result, "poetry.odoo18.0_python3.12.10.lock"
|
||||
)
|
||||
|
||||
def test_addons_template(self):
|
||||
result = ADDONS_TEMPLATE_FILE % "18.0"
|
||||
self.assertEqual(result, "odoo18.0/addons")
|
||||
|
||||
def test_odoo_template(self):
|
||||
result = ODOO_TEMPLATE_FILE % "18.0"
|
||||
self.assertEqual(result, "odoo18.0")
|
||||
|
||||
|
||||
class TestUpdateValidateVersion(unittest.TestCase):
|
||||
"""Test validate_version() sets expected paths based on version data."""
|
||||
|
||||
def _make_update(self, data_version, config_args=None):
|
||||
with patch("sys.argv", ["prog"]):
|
||||
update = Update()
|
||||
update.data_version = data_version
|
||||
if config_args:
|
||||
for k, v in config_args.items():
|
||||
setattr(update.config, k, v)
|
||||
return update
|
||||
|
||||
def test_explicit_erplibre_version(self):
|
||||
data = {
|
||||
"odoo18.0_python3.12.10": {
|
||||
"odoo_version": "18.0",
|
||||
"python_version": "3.12.10",
|
||||
"poetry_version": "2.1.3",
|
||||
"default": True,
|
||||
}
|
||||
}
|
||||
update = self._make_update(
|
||||
data,
|
||||
{"erplibre_version": "odoo18.0_python3.12.10"},
|
||||
)
|
||||
update.validate_version()
|
||||
self.assertEqual(update.new_version_odoo, "18.0")
|
||||
self.assertEqual(update.new_version_python, "3.12.10")
|
||||
self.assertEqual(update.new_version_poetry, "2.1.3")
|
||||
self.assertEqual(
|
||||
update.new_version_erplibre, "odoo18.0_python3.12.10"
|
||||
)
|
||||
|
||||
def test_explicit_odoo_version(self):
|
||||
data = {}
|
||||
update = self._make_update(data, {"odoo_version": "17.0"})
|
||||
update.new_version_python = "3.10.18"
|
||||
update.config.python_version = "3.10.18"
|
||||
update.validate_version()
|
||||
self.assertEqual(update.new_version_odoo, "17.0")
|
||||
|
||||
def test_default_version_fallback(self):
|
||||
data = {
|
||||
"odoo18.0_python3.12.10": {
|
||||
"odoo_version": "18.0",
|
||||
"python_version": "3.12.10",
|
||||
"poetry_version": "2.1.3",
|
||||
"default": True,
|
||||
}
|
||||
}
|
||||
update = self._make_update(data)
|
||||
update.detected_version_erplibre = None
|
||||
update.validate_version()
|
||||
self.assertEqual(update.new_version_odoo, "18.0")
|
||||
|
||||
def test_detected_version_used(self):
|
||||
data = {
|
||||
"odoo17.0_python3.10.18": {
|
||||
"odoo_version": "17.0",
|
||||
"python_version": "3.10.18",
|
||||
"poetry_version": "1.8.3",
|
||||
}
|
||||
}
|
||||
update = self._make_update(data)
|
||||
update.detected_version_erplibre = "odoo17.0_python3.10.18"
|
||||
update.validate_version()
|
||||
self.assertEqual(update.new_version_odoo, "17.0")
|
||||
self.assertEqual(update.new_version_python, "3.10.18")
|
||||
|
||||
def test_expected_paths_set(self):
|
||||
data = {
|
||||
"odoo18.0_python3.12.10": {
|
||||
"odoo_version": "18.0",
|
||||
"python_version": "3.12.10",
|
||||
"poetry_version": "2.1.3",
|
||||
"default": True,
|
||||
}
|
||||
}
|
||||
update = self._make_update(
|
||||
data,
|
||||
{"erplibre_version": "odoo18.0_python3.12.10"},
|
||||
)
|
||||
update.validate_version()
|
||||
self.assertIn("default.dev.odoo18.0.xml", update.expected_manifest_name)
|
||||
self.assertIn("requirement", update.expected_pyproject_path)
|
||||
self.assertIn("requirement", update.expected_poetry_lock_path)
|
||||
self.assertEqual(update.expected_odoo_name, "odoo18.0")
|
||||
|
||||
|
||||
class TestUpdateDetectVersion(unittest.TestCase):
|
||||
def test_detect_version_no_files(self):
|
||||
with patch("sys.argv", ["prog"]):
|
||||
update = Update()
|
||||
with patch("os.path.exists", return_value=False):
|
||||
result = update.detect_version()
|
||||
self.assertFalse(result)
|
||||
|
||||
def test_detect_version_matching(self):
|
||||
with patch("sys.argv", ["prog"]):
|
||||
update = Update()
|
||||
update.data_version = {
|
||||
"odoo18.0_python3.12.10": {
|
||||
"odoo_version": "18.0",
|
||||
"python_version": "3.12.10",
|
||||
}
|
||||
}
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
py_file = os.path.join(tmpdir, "python_ver")
|
||||
odoo_file = os.path.join(tmpdir, "odoo_ver")
|
||||
poetry_file = os.path.join(tmpdir, "poetry_ver")
|
||||
with open(py_file, "w") as f:
|
||||
f.write("3.12.10")
|
||||
with open(odoo_file, "w") as f:
|
||||
f.write("18.0")
|
||||
with open(poetry_file, "w") as f:
|
||||
f.write("2.1.3")
|
||||
with patch(
|
||||
"script.version.update_env_version.VERSION_PYTHON_FILE",
|
||||
py_file,
|
||||
), patch(
|
||||
"script.version.update_env_version.VERSION_ODOO_FILE",
|
||||
odoo_file,
|
||||
), patch(
|
||||
"script.version.update_env_version.VERSION_POETRY_FILE",
|
||||
poetry_file,
|
||||
), patch(
|
||||
"script.version.update_env_version.INSTALLED_ODOO_VERSION_FILE",
|
||||
os.path.join(tmpdir, "nonexist"),
|
||||
):
|
||||
result = update.detect_version()
|
||||
self.assertTrue(result)
|
||||
self.assertEqual(
|
||||
update.detected_version_erplibre, "odoo18.0_python3.12.10"
|
||||
)
|
||||
|
||||
|
||||
class TestUpdatePrintLog(unittest.TestCase):
|
||||
def test_empty_log(self):
|
||||
with patch("sys.argv", ["prog"]):
|
||||
update = Update()
|
||||
update.print_log()
|
||||
|
||||
def test_with_entries(self):
|
||||
with patch("sys.argv", ["prog"]):
|
||||
update = Update()
|
||||
update.execute_log = ["entry1", "entry2"]
|
||||
update.print_log()
|
||||
Loading…
Reference in a new issue