From 37f6fad4eda9d7971bfe260c8321ce03f675aeb3 Mon Sep 17 00:00:00 2001 From: "d.kovalenko" Date: Thu, 2 Jul 2026 13:16:53 +0300 Subject: [PATCH 1/2] [new] OsOperations::get_abs_path is added It is a replacement for os.path.abs. --- src/local_ops.py | 14 ++++ src/os_ops.py | 4 ++ src/remote_ops.py | 39 +++++++++++ tests/test_os_ops_common.py | 125 ++++++++++++++++++++++++++++++++++++ 4 files changed, 182 insertions(+) diff --git a/src/local_ops.py b/src/local_ops.py index 5be4fc4..925690f 100644 --- a/src/local_ops.py +++ b/src/local_ops.py @@ -732,3 +732,17 @@ def is_abs_path(self, path: str) -> bool: def get_basename(self, path: str) -> str: assert type(path) is str return os.path.basename(path) + + def get_abs_path(self, path: str) -> str: + assert type(path) is str + + normalized = os.path.normpath(path) + assert type(normalized) is str + + # We expand the tilde locally so that the behavior matches the server + expanded = os.path.expanduser(normalized) + assert type(expanded) is str + + r = os.path.abspath(expanded) + assert type(r) is str + return r diff --git a/src/os_ops.py b/src/os_ops.py index d2af413..ae0432c 100644 --- a/src/os_ops.py +++ b/src/os_ops.py @@ -279,3 +279,7 @@ def is_abs_path(self, path: str) -> bool: def get_basename(self, path: str) -> str: assert type(path) is str raise NotImplementedError() + + def get_abs_path(self, path: str) -> str: + assert type(path) is str + raise NotImplementedError() diff --git a/src/remote_ops.py b/src/remote_ops.py index 66fa2ba..0c910fa 100644 --- a/src/remote_ops.py +++ b/src/remote_ops.py @@ -45,6 +45,8 @@ def cmdline(self): class RemoteOperations(OsOperations): + _C_EOL = "\n" + # # Target system is Linux only. # @@ -992,6 +994,24 @@ def get_basename(self, path: str) -> str: assert type(path) is str return __class__._get_basename(path) + def get_abs_path(self, path: str) -> str: + assert type(path) is str + + cleaned_path = __class__._normpath(path) + assert type(cleaned_path) is str + + # + # "-m" is used to ignore not exist parts of path + # + r = self.exec_command( + ["realpath", "-m", cleaned_path], + encoding=get_default_encoding(), + ) + assert type(r) is str + r = __class__._strip_last_eol(r) + assert type(r) is str + return r + @staticmethod def _build_cmdline( cmd, @@ -1104,6 +1124,25 @@ def _get_basename(path: str) -> str: assert type(path) is str return posixpath.basename(path) + @staticmethod + def _normpath(path: str) -> str: + assert type(path) is str + return posixpath.normpath(path) + + @staticmethod + def _strip_last_eol(text: str) -> str: + assert type(text) is str + assert type(__class__._C_EOL) is str + assert __class__._C_EOL == "\n" + + if not text.endswith(__class__._C_EOL): + return text + + r = text[:-(len(__class__._C_EOL))] + assert type(r) is str + assert r + __class__._C_EOL == text + return r + def normalize_error(error): if isinstance(error, bytes): diff --git a/tests/test_os_ops_common.py b/tests/test_os_ops_common.py index 6877d8d..62e8b8c 100644 --- a/tests/test_os_ops_common.py +++ b/tests/test_os_ops_common.py @@ -1548,6 +1548,131 @@ def test_is_abs_path__no(self, os_ops: OsOperations): assert actual_value is False return + # -------------------------------------------------------------------- + def test_get_abs_path(self, os_ops: OsOperations): + assert isinstance(os_ops, OsOperations) + + def LOCAL__check(value, expected) -> bool: + logging.info("Source path: [{}]".format(value)) + actual = os_ops.get_abs_path(value) + if actual == expected: + logging.info("Result is OK: [{}].".format( + actual, + )) + else: + logging.error("Result is BAD: [{}]. Expected: [{}].".format( + actual, + expected, + )) + logging.info("") + return False + + logging.info("------------- test empty string") + cwd = os_ops.cwd() + LOCAL__check("", cwd) + + logging.info("------------- test cwd") + LOCAL__check(".", cwd) + + path = os_ops.build_path(cwd, ".") + LOCAL__check(path, cwd) + + cwd = os_ops.cwd() + expected_r = os_ops.build_path(cwd, "abc") + LOCAL__check("abc", expected_r) + + cwd = os_ops.cwd() + expected_r = os_ops.build_path(cwd, "abc") + LOCAL__check("./abc", expected_r) + + cwd = os_ops.cwd() + expected_r = os_ops.build_path(os_ops.get_dirname(cwd), "abc") + LOCAL__check("../abc", expected_r) + + cwd = os_ops.cwd() + expected_r = os_ops.build_path(cwd, "abc1.txt") + LOCAL__check("abc1.txt", expected_r) + + logging.info("------------- test cwd parent") + cwd = os_ops.cwd() + expected_r = os_ops.get_dirname(cwd) + LOCAL__check("..", expected_r) + + logging.info("------------- test file") + file = os_ops.mkstemp() + LOCAL__check(file, file) + os_ops.remove_file(file) + + logging.info("------------- test dir") + dir = os_ops.mkdtemp() + LOCAL__check(dir, dir) + + dirname = os_ops.get_basename(dir) + path = os_ops.build_path(dir, "..", dirname) + LOCAL__check(path, dir) + + dirname = os_ops.get_basename(dir) + path = os_ops.build_path(dir, "..", dirname, "abc.txt") + expected_r = os_ops.build_path(dir, "abc.txt") + LOCAL__check(path, expected_r) + + os_ops.rmdir(dir) + + logging.info("------------- unknown path") + expected_r = os_ops.build_path(cwd, "abc/file.txt") + LOCAL__check("./abc/file.txt", expected_r) + + logging.info("------------- home dir") + LOCAL__check("/~", "/~") + + logging.info("------------- test root over-traversal") + # Из любого места системы (даже глубокого) 15 переходов вверх выведут в корень + many_dots = os_ops.build_path(*([".."] * 15)) + LOCAL__check(many_dots, "/") + + # Корень + еще раз вверх + папка + path = os_ops.build_path("/", "..", "abc") + LOCAL__check(path, "/abc") + + logging.info("------------- test multiple slashes") + + # TODO: Double slash at the beginning. In POSIX it sometimes + # has a special meaning, let's check it out. + # os.path.abs returns "//abc" + # r = os_ops.get_abs_path("//abc") + # LOCAL__check("//abc", "/abc") + + # Slashes in the middle of a relative path + expected_r = os_ops.build_path(cwd, "abc", "def") + LOCAL__check("abc///def", expected_r) + + # Relative path ending with a slash + expected_r = os_ops.build_path(cwd, "abc") + LOCAL__check("abc/", expected_r) + + logging.info("------------- test raw tilde") + exec_r = os_ops.exec_command(["sh", "-c", "cd ~;pwd"], encoding="utf-8") + assert type(exec_r) is str + expected_r = exec_r.rstrip() + LOCAL__check("~", expected_r) + + # Tilda with a ROOT user + LOCAL__check("~root/abc", "/root/abc") + + logging.info("------------- test spaces and special chars") + # Folder with quotes, and spaces. + weird_name = "my folder VAR 'single' \"double\"" + expected_r = os_ops.build_path(cwd, weird_name) + LOCAL__check(weird_name, expected_r) + + # TODO: Folder with dollar signs, quotes, and spaces. + # weird_name = "my folder $VAR 'single' \"double\"" + # expected_r = os_ops.build_path(cwd, weird_name) + # LOCAL__check(weird_name, expected_r) + + logging.info("OK. GO HOME!") + return + # -------------------------------------------------------------------- @dataclasses.dataclass class tagGetBaseNameData: From cad73800ce645d7d1d4c651f7112709316eec3bf Mon Sep 17 00:00:00 2001 From: "d.kovalenko" Date: Thu, 2 Jul 2026 13:40:16 +0300 Subject: [PATCH 2/2] [CI] alpine: coreutils is installed To a support "-m" in realpath utility (remote_ops::get_abs_path). --- Dockerfile--alpine.tmpl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Dockerfile--alpine.tmpl b/Dockerfile--alpine.tmpl index bf83aa0..f78af01 100644 --- a/Dockerfile--alpine.tmpl +++ b/Dockerfile--alpine.tmpl @@ -3,7 +3,9 @@ ARG PYTHON_VERSION=3.12 # --------------------------------------------- base1 FROM python:${PYTHON_VERSION}-alpine AS base1 -# --------------------------------------------- base1 +RUN apk add --no-cache coreutils + +# --------------------------------------------- base2 FROM base1 AS base2 RUN apk add python3-dev build-base musl-dev linux-headers