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
402 lines
14 KiB
Python
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()
|