erplibre/test/test_git_tool.py
Mathieu Benoit cfbdd9406e [REF] format : passer l'outillage et les tests sous ruff
Le formateur de ce dépôt est ruff depuis qu'il remplace black, qui ne connaît
aucune cible au-delà de py313 ; ce passage applique sa norme à l'arbre entier,
d'un coup, pour qu'aucun commit de fond n'ait à porter du style. L'écart tient
presque entièrement aux chaînes coupées à la main que ruff recolle quand elles
tiennent sur une ligne, et aux « with » multiples qu'il regroupe : aucune
valeur ne change, et les clés de traduction non plus.
Vérifié : la suite unitaire reste verte après le passage, et le contrôle de
syntaxe ne signale rien.

--- EN ---

This repository's formatter is ruff since it replaced black, which knows no
target beyond py313; this pass applies its standard to the whole tree at once,
so that no substantive commit has to carry style. The difference is almost
entirely the hand-split strings ruff joins back when they fit on one line, and
the multiple "with" it merges: no value changes, nor do the translation keys.
Checked: the unit suite stays green after the pass, and the syntax check
reports nothing.

Assisted-by: Claude Opus 5
2026-09-24 14:40:38 -04:00

402 lines
14 KiB
Python

#!/usr/bin/env python3
# © 2026 TechnoLibre (http://www.technolibre.ca)
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl)
import io
import os
import subprocess
import tempfile
import unittest
from collections import OrderedDict
from contextlib import redirect_stdout
from unittest.mock import MagicMock, mock_open, patch
from script.git.git_tool import (
DEFAULT_PROJECT_NAME,
DEFAULT_REMOTE_URL,
DEFAULT_WEBSITE,
EL_GITHUB_TOKEN,
SOURCE_REPO_ADDONS_FILE,
GitTool,
RepoAttrs,
)
class TestRepoAttrs(unittest.TestCase):
def test_basic_attributes(self):
s = RepoAttrs(a=1, b="hello")
self.assertEqual(s.a, 1)
self.assertEqual(s.b, "hello")
def test_empty_struct(self):
s = RepoAttrs()
self.assertEqual(s.__dict__, {})
def test_override_existing(self):
s = RepoAttrs(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, RepoAttrs)
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\nEL_GITHUB_TOKEN="my_token_123"\nOTHER_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[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 TestTheRootRepoSurvivesAMissingManifest(unittest.TestCase):
"""Le dépôt racine ne dépend ni du manifeste ni des sous-modules.
Une liste vide là où la racine était demandée fait conclure à
l'appelant qu'il n'a rien à faire : sur un checkout sans `.repo`, le
remote du dépôt principal n'était jamais réécrit, sans message.
"""
def _depot(self, chemin, origine="https://github.com/ERPLibre/ERPLibre"):
"""Un vrai dépôt git, sans manifeste. Rien n'est bouchonné : la
lecture de l'origine passe par git, et c'est elle qu'on vérifie."""
subprocess.run(["git", "init", "-q", chemin], check=True)
if origine:
subprocess.run(
["git", "-C", chemin, "remote", "add", "origin", origine],
check=True,
)
return chemin
def test_without_a_manifest_the_root_is_still_returned(self):
gt = GitTool()
with tempfile.TemporaryDirectory() as tmpdir:
self._depot(tmpdir)
result = gt.get_repo_info(tmpdir, add_root=True)
self.assertEqual(len(result), 1)
self.assertEqual(result[0]["name"], "")
self.assertEqual(
result[0]["url_git"], "git@github.com:ERPLibre/ERPLibre"
)
def test_without_the_root_asked_nothing_is_returned(self):
"""Deux appelants internes lisent le manifeste SANS la racine : le
correctif ne doit rien rendre à qui n'en veut pas."""
gt = GitTool()
with tempfile.TemporaryDirectory() as tmpdir:
self._depot(tmpdir)
self.assertEqual(gt.get_repo_info(tmpdir), [])
def test_a_root_without_origin_falls_back_instead_of_raising(self):
"""La liste sert à RÉÉCRIRE les remotes : un dépôt sans origine est
précisément un de ceux qu'on vient corriger."""
gt = GitTool()
with tempfile.TemporaryDirectory() as tmpdir:
self._depot(tmpdir, origine="")
with redirect_stdout(io.StringIO()):
result = gt.get_repo_info(tmpdir, add_root=True)
self.assertEqual(len(result), 1)
self.assertTrue(result[0]["url_git"].startswith("git@"))
def test_the_root_comes_first_alongside_the_manifest_projects(self):
"""La racine porte un nom vide : le tri la met en tête, et le
script qui réécrit les remotes commence donc par elle."""
xml_content = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
"<manifest>\n"
' <remote name="OCA" fetch="https://github.com/OCA/"/>\n'
' <default remote="OCA" revision="16.0"/>\n'
' <project name="web.git" path="addons/OCA_web"'
' remote="OCA" groups="odoo16.0"/>\n'
"</manifest>\n"
)
gt = GitTool()
with tempfile.TemporaryDirectory() as tmpdir:
self._depot(tmpdir)
manifeste = os.path.join(tmpdir, "manifest_test.xml")
with open(manifeste, "w") as fh:
fh.write(xml_content)
with patch.object(
GitTool, "get_manifest_file", return_value=manifeste
):
result = gt.get_repo_info(tmpdir, add_root=True)
self.assertEqual([r["name"] for r in result], ["", "addons/OCA_web"])
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:
remotes, projects, default_remote = gt.get_manifest_xml_info(
filename=f.name
)
self.assertIn("OCA", remotes)
self.assertIn("server-tools.git", projects)
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:
remotes, projects, default_remote = gt.get_manifest_xml_info(
filename=f.name
)
self.assertEqual(remotes, {})
self.assertEqual(projects, {})
self.assertIsNone(default_remote)
finally:
os.unlink(f.name)
class TestConstants(unittest.TestCase):
def test_file_source_repo_addons(self):
self.assertEqual(SOURCE_REPO_ADDONS_FILE, "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()