From b2f6462520192857eac93bcadfe8faeda6e3b38d Mon Sep 17 00:00:00 2001 From: diegoferigo Date: Fri, 23 Feb 2024 15:58:46 +0100 Subject: [PATCH 01/20] Update comment in code --- .../resolve_robotics_uri_py.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py b/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py index 234e1d7..0d072f9 100644 --- a/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py +++ b/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py @@ -39,11 +39,13 @@ def resolve_robotics_uri(uri: str) -> pathlib.Path: from urllib.parse import urlparse parsed_uri = urlparse(uri) - # We only support at the moment: - # file:// scheme: to pass a file path directly - # package:// : ROS-style package URI - # model:// : SDF-style model URI - if parsed_uri.scheme not in ["file", "package", "model"]: + # We only support the following URI schemes at the moment: + # + # * file://: to pass an absolute file path directly + # * model://: SDF-style model URI + # * package://: ROS-style package URI + # + if parsed_uri.scheme not in {"file", "package", "model"}: raise FileNotFoundError(f"Passed URI \"{uri}\" use non-supported scheme {parsed_uri.scheme}") model_filenames = [] From c51ed743f4740599eafa8f91b14cb8cf13b7ba8a Mon Sep 17 00:00:00 2001 From: diegoferigo Date: Fri, 23 Feb 2024 16:00:08 +0100 Subject: [PATCH 02/20] Refactor logic and raise exception if file:// URI does not exists --- .../resolve_robotics_uri_py.py | 51 +++++++++++++------ 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py b/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py index 0d072f9..ed37176 100644 --- a/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py +++ b/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py @@ -48,26 +48,45 @@ def resolve_robotics_uri(uri: str) -> pathlib.Path: if parsed_uri.scheme not in {"file", "package", "model"}: raise FileNotFoundError(f"Passed URI \"{uri}\" use non-supported scheme {parsed_uri.scheme}") + if parsed_uri.scheme == "file": + # Strip the URI scheme, keep the absolute path to the file (with trailing /) + uri_path = pathlib.Path(uri.replace(f"{parsed_uri.scheme}:/", "")) + + if not uri_path.is_file(): + msg = "resolve-robotics-uri-py: No file corresponding to uri '{}' found" + raise FileNotFoundError(msg.format(uri)) + + return uri_path + + # Strip the URI scheme + uri_path = uri.replace(f"{parsed_uri.scheme}://", "") + + # List of matching resources found model_filenames = [] - if parsed_uri.scheme == "file": - model_filenames.append(uri.replace("file:/", "")) - - if parsed_uri.scheme == "package" or parsed_uri.scheme == "model": - uri_path = uri.replace(f"{parsed_uri.scheme}://","") - for folder in get_search_paths_from_envs(env_list): - candidate_file_name = folder / pathlib.Path(uri_path) - if (candidate_file_name.is_file()): - if candidate_file_name not in model_filenames: - model_filenames.append(candidate_file_name) - - if model_filenames: - if (len(model_filenames) > 1): - warnings.warn(f"resolve-robotics-uri-py: Multiple files ({pathlist_list_to_string(model_filenames)}) found for uri \"{uri}\", returning the first one.") + for folder in set(get_search_paths_from_envs(env_list)): + + candidate_file_name = folder / uri_path + + if not candidate_file_name.is_file(): + continue + + if candidate_file_name not in model_filenames: + model_filenames.append(candidate_file_name) + + if len(model_filenames) == 0: + msg = "resolve-robotics-uri-py: No file corresponding to uri '{}' found" + raise FileNotFoundError(msg.format(uri)) + + if len(model_filenames) > 1: + msg = "resolve-robotics-uri-py: " + msg += "Multiple files ({}) found for URI '{}', returning the first one." + warnings.warn(msg.format(pathlist_list_to_string(model_filenames), uri)) + + if len(model_filenames) >= 1: + assert model_filenames[0].exists() return pathlib.Path(model_filenames[0]) - # If no file was found raise error - raise FileNotFoundError(f"resolve-robotics-uri-py: No file corresponding to uri \"{uri}\" found") def main(): parser = argparse.ArgumentParser(description="Utility resolve a robotics URI (file://, model://, package://) to an absolute filename.") From c4cc68f701e1b9a6550eea796b06b05902214049 Mon Sep 17 00:00:00 2001 From: diegoferigo Date: Fri, 23 Feb 2024 16:04:11 +0100 Subject: [PATCH 03/20] Assume file:// if the URI has no scheme --- src/resolve_robotics_uri_py/resolve_robotics_uri_py.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py b/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py index ed37176..f7f1cc5 100644 --- a/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py +++ b/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py @@ -31,9 +31,9 @@ def resolve_robotics_uri(uri: str) -> pathlib.Path: # * GZ_SIM_RESOURCE_PATH: Used in Gazebo Sim >= 7 env_list = ["GAZEBO_MODEL_PATH", "ROS_PACKAGE_PATH", "AMENT_PREFIX_PATH", "SDF_PATH", "IGN_GAZEBO_RESOURCE_PATH", "GZ_SIM_RESOURCE_PATH"] - # Preliminary step: if there is no scheme, we just consider this a path and we return it as it is + # If the URI has no scheme, use by default the file:// if "://" not in uri: - return pathlib.Path(uri) + uri = f"file://{uri}" # Get scheme from URI from urllib.parse import urlparse From 7bbe6c037fc39b6d6eff73ba1c1749c9ed3711fd Mon Sep 17 00:00:00 2001 From: diegoferigo Date: Fri, 23 Feb 2024 16:10:34 +0100 Subject: [PATCH 04/20] Format with black and isort --- .../resolve_robotics_uri_py.py | 29 +++++++++++++++---- test/test_resolve_robotics_uri_py.py | 6 +++- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py b/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py index f7f1cc5..fc78e86 100644 --- a/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py +++ b/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py @@ -2,19 +2,22 @@ import os import pathlib import sys -from typing import List import warnings + # Function inspired from https://github.com/ami-iit/robot-log-visualizer/pull/51 def get_search_paths_from_envs(env_list): return [ pathlib.Path(f) if (env != "AMENT_PREFIX_PATH") else pathlib.Path(f) / "share" - for env in env_list if os.getenv(env) is not None + for env in env_list + if os.getenv(env) is not None for f in os.getenv(env).split(os.pathsep) ] + def pathlist_list_to_string(path_list): - return ' '.join(str(path) for path in path_list) + return " ".join(str(path) for path in path_list) + def resolve_robotics_uri(uri: str) -> pathlib.Path: # List of environment variables to consider, see: @@ -29,7 +32,14 @@ def resolve_robotics_uri(uri: str) -> pathlib.Path: # * SDF_PATH: Used in sdformat # * IGN_GAZEBO_RESOURCE_PATH: Used in Ignition Gazebo <= 7 # * GZ_SIM_RESOURCE_PATH: Used in Gazebo Sim >= 7 - env_list = ["GAZEBO_MODEL_PATH", "ROS_PACKAGE_PATH", "AMENT_PREFIX_PATH", "SDF_PATH", "IGN_GAZEBO_RESOURCE_PATH", "GZ_SIM_RESOURCE_PATH"] + env_list = [ + "GAZEBO_MODEL_PATH", + "ROS_PACKAGE_PATH", + "AMENT_PREFIX_PATH", + "SDF_PATH", + "IGN_GAZEBO_RESOURCE_PATH", + "GZ_SIM_RESOURCE_PATH", + ] # If the URI has no scheme, use by default the file:// if "://" not in uri: @@ -37,6 +47,8 @@ def resolve_robotics_uri(uri: str) -> pathlib.Path: # Get scheme from URI from urllib.parse import urlparse + + # Parse the URI parsed_uri = urlparse(uri) # We only support the following URI schemes at the moment: @@ -46,7 +58,9 @@ def resolve_robotics_uri(uri: str) -> pathlib.Path: # * package://: ROS-style package URI # if parsed_uri.scheme not in {"file", "package", "model"}: - raise FileNotFoundError(f"Passed URI \"{uri}\" use non-supported scheme {parsed_uri.scheme}") + raise FileNotFoundError( + f'Passed URI "{uri}" use non-supported scheme {parsed_uri.scheme}' + ) if parsed_uri.scheme == "file": # Strip the URI scheme, keep the absolute path to the file (with trailing /) @@ -89,7 +103,9 @@ def resolve_robotics_uri(uri: str) -> pathlib.Path: def main(): - parser = argparse.ArgumentParser(description="Utility resolve a robotics URI (file://, model://, package://) to an absolute filename.") + parser = argparse.ArgumentParser( + description="Utility resolve a robotics URI (file://, model://, package://) to an absolute filename." + ) parser.add_argument("uri", metavar="uri", type=str, help="URI to resolve") args = parser.parse_args() @@ -98,5 +114,6 @@ def main(): print(result) sys.exit(0) + if __name__ == "__main__": main() diff --git a/test/test_resolve_robotics_uri_py.py b/test/test_resolve_robotics_uri_py.py index 589ac3b..5a12480 100644 --- a/test/test_resolve_robotics_uri_py.py +++ b/test/test_resolve_robotics_uri_py.py @@ -1,6 +1,10 @@ import resolve_robotics_uri_py import pytest + def test_non_existing_file(): + with pytest.raises(FileNotFoundError): - resolve_robotics_uri_py.resolve_robotics_uri("package://this/package/and/file/does/not.exist") + resolve_robotics_uri_py.resolve_robotics_uri( + "package://this/package/and/file/does/not.exist" + ) From ec415253ed1aa4e68b7f14afe6b60c95ae3396c3 Mon Sep 17 00:00:00 2001 From: diegoferigo Date: Fri, 23 Feb 2024 16:34:01 +0100 Subject: [PATCH 05/20] Update command-line logic --- .../resolve_robotics_uri_py.py | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py b/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py index fc78e86..45cd2ff 100644 --- a/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py +++ b/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py @@ -4,6 +4,9 @@ import sys import warnings +# Supported URI schemes +SupportedSchemes = {"file", "package", "model"} + # Function inspired from https://github.com/ami-iit/robot-log-visualizer/pull/51 def get_search_paths_from_envs(env_list): @@ -57,7 +60,7 @@ def resolve_robotics_uri(uri: str) -> pathlib.Path: # * model://: SDF-style model URI # * package://: ROS-style package URI # - if parsed_uri.scheme not in {"file", "package", "model"}: + if parsed_uri.scheme not in SupportedSchemes: raise FileNotFoundError( f'Passed URI "{uri}" use non-supported scheme {parsed_uri.scheme}' ) @@ -104,14 +107,21 @@ def resolve_robotics_uri(uri: str) -> pathlib.Path: def main(): parser = argparse.ArgumentParser( - description="Utility resolve a robotics URI (file://, model://, package://) to an absolute filename." + description="Utility resolve a robotics URI ({}) to an absolute filename.".format( + ", ".join(f"{scheme}://" for scheme in SupportedSchemes) + ) ) - parser.add_argument("uri", metavar="uri", type=str, help="URI to resolve") + parser.add_argument("uri", metavar="URI", type=str, help="URI to resolve") args = parser.parse_args() - result = resolve_robotics_uri(args.uri) - print(result) + try: + result = resolve_robotics_uri(args.uri) + except FileNotFoundError as e: + print(e, file=sys.stderr) + sys.exit(1) + + print(result, file=sys.stdout) sys.exit(0) From b1c23cb2e424a83bb0adff027d2b9750cd52bb96 Mon Sep 17 00:00:00 2001 From: diegoferigo Date: Fri, 23 Feb 2024 16:25:35 +0100 Subject: [PATCH 06/20] Handle both absolute and relative paths of file:// scheme --- .../resolve_robotics_uri_py.py | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py b/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py index 45cd2ff..9cc03a0 100644 --- a/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py +++ b/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py @@ -61,22 +61,25 @@ def resolve_robotics_uri(uri: str) -> pathlib.Path: # * package://: ROS-style package URI # if parsed_uri.scheme not in SupportedSchemes: - raise FileNotFoundError( - f'Passed URI "{uri}" use non-supported scheme {parsed_uri.scheme}' - ) + msg = "resolve-robotics-uri-py: Passed URI '{}' use non-supported scheme '{}'" + raise FileNotFoundError(msg.format(uri, parsed_uri.scheme)) + + # Strip the URI scheme + uri_path = uri.replace(f"{parsed_uri.scheme}://", "") if parsed_uri.scheme == "file": - # Strip the URI scheme, keep the absolute path to the file (with trailing /) - uri_path = pathlib.Path(uri.replace(f"{parsed_uri.scheme}:/", "")) + # Ensure the URI path is absolute + uri_path = uri_path if uri_path.startswith("/") else f"/{uri_path}" - if not uri_path.is_file(): - msg = "resolve-robotics-uri-py: No file corresponding to uri '{}' found" - raise FileNotFoundError(msg.format(uri)) + # Create the file path + uri_file_path = pathlib.Path(uri_path) - return uri_path + # Check that the file exists + if not uri_file_path.is_file(): + msg = "resolve-robotics-uri-py: No file corresponding to URI '{}' found" + raise FileNotFoundError(msg.format(uri)) - # Strip the URI scheme - uri_path = uri.replace(f"{parsed_uri.scheme}://", "") + return uri_file_path # List of matching resources found model_filenames = [] From 2b3d57b4bd2b5e8e1b02140406f11a9d4b9ab589 Mon Sep 17 00:00:00 2001 From: diegoferigo Date: Fri, 23 Feb 2024 16:25:47 +0100 Subject: [PATCH 07/20] Update documentation and comments --- .../resolve_robotics_uri_py.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py b/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py index 9cc03a0..98db0d0 100644 --- a/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py +++ b/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py @@ -23,6 +23,19 @@ def pathlist_list_to_string(path_list): def resolve_robotics_uri(uri: str) -> pathlib.Path: + """ + Resolve a robotics URI to an absolute filename. + + Args: + uri: The URI to resolve. + + Returns: + The absolute filename corresponding to the URI. + + Raises: + FileNotFoundError: If no file corresponding to the URI is found. + """ + # List of environment variables to consider, see: # * https://github.com/robotology/idyntree/issues/291 # * https://github.com/gazebosim/sdformat/issues/1234 @@ -86,11 +99,13 @@ def resolve_robotics_uri(uri: str) -> pathlib.Path: for folder in set(get_search_paths_from_envs(env_list)): + # Join the folder from environment variable and the URI path candidate_file_name = folder / uri_path if not candidate_file_name.is_file(): continue + # Skip if the file is already in the list if candidate_file_name not in model_filenames: model_filenames.append(candidate_file_name) From 9efd7a76197e8a6df9746010c7457e6450de0d00 Mon Sep 17 00:00:00 2001 From: diegoferigo Date: Fri, 23 Feb 2024 17:04:58 +0100 Subject: [PATCH 08/20] Expose the supported environment variables --- .../resolve_robotics_uri_py.py | 49 ++++++++++--------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py b/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py index 98db0d0..114cd7e 100644 --- a/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py +++ b/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py @@ -7,6 +7,32 @@ # Supported URI schemes SupportedSchemes = {"file", "package", "model"} +# Environment variables in the search path. +# +# * https://github.com/robotology/idyntree/issues/291 +# * https://github.com/gazebosim/sdformat/issues/1234 +# +# AMENT_PREFIX_PATH is the only "special" as we need to add +# "share" after each value, see https://github.com/stack-of-tasks/pinocchio/issues/1520 +# +# This list specify the origin of each env variable: +# +# * AMENT_PREFIX_PATH: Used in ROS2 +# * GAZEBO_MODEL_PATH: Used in Gazebo Classic +# * GZ_SIM_RESOURCE_PATH: Used in Gazebo Sim >= 7 +# * IGN_GAZEBO_RESOURCE_PATH: Used in Ignition Gazebo <= 7 +# * ROS_PACKAGE_PATH: Used in ROS1 +# * SDF_PATH: Used in sdformat +# +SupportedEnvVars = { + "AMENT_PREFIX_PATH", + "GAZEBO_MODEL_PATH", + "GZ_SIM_RESOURCE_PATH", + "IGN_GAZEBO_RESOURCE_PATH", + "ROS_PACKAGE_PATH", + "SDF_PATH", +} + # Function inspired from https://github.com/ami-iit/robot-log-visualizer/pull/51 def get_search_paths_from_envs(env_list): @@ -36,27 +62,6 @@ def resolve_robotics_uri(uri: str) -> pathlib.Path: FileNotFoundError: If no file corresponding to the URI is found. """ - # List of environment variables to consider, see: - # * https://github.com/robotology/idyntree/issues/291 - # * https://github.com/gazebosim/sdformat/issues/1234 - # AMENT_PREFIX_PATH is the only "special" as we need to add - # "share" after each value, see https://github.com/stack-of-tasks/pinocchio/issues/1520 - # This list specify the origin of each env variable: - # * GAZEBO_MODEL_PATH: Used in Gazebo Classic - # * ROS_PACKAGE_PATH: Used in ROS1 - # * AMENT_PREFIX_PATH: Used in ROS2 - # * SDF_PATH: Used in sdformat - # * IGN_GAZEBO_RESOURCE_PATH: Used in Ignition Gazebo <= 7 - # * GZ_SIM_RESOURCE_PATH: Used in Gazebo Sim >= 7 - env_list = [ - "GAZEBO_MODEL_PATH", - "ROS_PACKAGE_PATH", - "AMENT_PREFIX_PATH", - "SDF_PATH", - "IGN_GAZEBO_RESOURCE_PATH", - "GZ_SIM_RESOURCE_PATH", - ] - # If the URI has no scheme, use by default the file:// if "://" not in uri: uri = f"file://{uri}" @@ -97,7 +102,7 @@ def resolve_robotics_uri(uri: str) -> pathlib.Path: # List of matching resources found model_filenames = [] - for folder in set(get_search_paths_from_envs(env_list)): + for folder in set(get_search_paths_from_envs(SupportedEnvVars)): # Join the folder from environment variable and the URI path candidate_file_name = folder / uri_path From 8e9ef4e8482c5ee4b98013d013ac27335d168561 Mon Sep 17 00:00:00 2001 From: diegoferigo Date: Fri, 23 Feb 2024 17:05:14 +0100 Subject: [PATCH 09/20] Add new tests --- test/test_resolve_robotics_uri_py.py | 136 ++++++++++++++++++++++++++- 1 file changed, 131 insertions(+), 5 deletions(-) diff --git a/test/test_resolve_robotics_uri_py.py b/test/test_resolve_robotics_uri_py.py index 5a12480..b4affba 100644 --- a/test/test_resolve_robotics_uri_py.py +++ b/test/test_resolve_robotics_uri_py.py @@ -1,10 +1,136 @@ -import resolve_robotics_uri_py +import contextlib +import os +import pathlib +import tempfile +from typing import ContextManager + import pytest +import resolve_robotics_uri_py + + +def clear_env_vars(): + for env_var in resolve_robotics_uri_py.resolve_robotics_uri_py.SupportedEnvVars: + _ = os.environ.pop(env_var, None) + + +@contextlib.contextmanager +def export_env_var(name: str, value: str) -> ContextManager[None]: + + os.environ[name] = value + yield + del os.environ[name] + + +# ======================================= +# Test the resolve_robotics_uri functions +# ======================================= + + +@pytest.mark.parametrize("scheme", ["model://", "package://"]) +def test_scoped_uri(scheme: str, env_var_name="GZ_SIM_RESOURCE_PATH"): + + clear_env_vars() + + # Non-existing relative URI path + with pytest.raises(FileNotFoundError): + uri = f"{scheme}this/package/and/file/does/not.exist" + resolve_robotics_uri_py.resolve_robotics_uri(uri) + + # Non-existing absolute URI path + with pytest.raises(FileNotFoundError): + uri = f"{scheme}/this/package/and/file/does/not.exist" + resolve_robotics_uri_py.resolve_robotics_uri(uri) + + # Existing URI path not in search dirs + with pytest.raises(FileNotFoundError): + with tempfile.NamedTemporaryFile() as temp: + uri = f"{scheme}{temp.name}" + resolve_robotics_uri_py.resolve_robotics_uri(uri) + + # URI path in top-level search dir + with tempfile.TemporaryDirectory() as temp_dir: + pathlib.Path(temp_dir).mkdir(exist_ok=True) + top_level = pathlib.Path(temp_dir) / "top_level.txt" + top_level.touch(exist_ok=True) + + # Existing relative URI path not in search dirs + with pytest.raises(FileNotFoundError): + uri = f"{scheme}top_level.txt" + resolve_robotics_uri_py.resolve_robotics_uri(uri) + + # Existing absolute URI path in search dirs + with pytest.raises(FileNotFoundError): + with export_env_var(name=env_var_name, value=temp_dir): + uri = f"{scheme}/top_level.txt" + resolve_robotics_uri_py.resolve_robotics_uri(uri) + + # Existing relative URI path in search dirs + with export_env_var(name=env_var_name, value=temp_dir): + uri = f"{scheme}top_level.txt" + resolve_robotics_uri_py.resolve_robotics_uri(uri) + + # Existing relative URI path in search dirs with multiple paths + with export_env_var(name=env_var_name, value=f"/another/dir:{temp_dir}"): + uri = f"{scheme}top_level.txt" + resolve_robotics_uri_py.resolve_robotics_uri(uri) + + # URI path in sub-level search dir + with tempfile.TemporaryDirectory() as temp_dir: + (pathlib.Path(temp_dir) / "sub").mkdir(exist_ok=True) + level1 = pathlib.Path(temp_dir) / "sub" / "level1.txt" + level1.touch(exist_ok=True) + + # Existing relative URI path not in search dirs + with pytest.raises(FileNotFoundError): + uri = f"{scheme}sub/level1.txt" + resolve_robotics_uri_py.resolve_robotics_uri(uri) + + # Existing absolute URI path in search dirs + with pytest.raises(FileNotFoundError): + with export_env_var(name=env_var_name, value=temp_dir): + uri = f"{scheme}/sub/level1.txt" + resolve_robotics_uri_py.resolve_robotics_uri(uri) -def test_non_existing_file(): + # Existing relative URI path in search dirs + with export_env_var(name=env_var_name, value=temp_dir): + uri = f"{scheme}sub/level1.txt" + resolve_robotics_uri_py.resolve_robotics_uri(uri) + # Existing relative URI path in search dirs with multiple paths + with export_env_var(name=env_var_name, value=f"/another/dir:{temp_dir}"): + uri = f"{scheme}sub/level1.txt" + resolve_robotics_uri_py.resolve_robotics_uri(uri) + + +def test_scheme_file(): + + clear_env_vars() + + # Non-existing relative URI path with pytest.raises(FileNotFoundError): - resolve_robotics_uri_py.resolve_robotics_uri( - "package://this/package/and/file/does/not.exist" - ) + uri_file = "file://this/file/does/not.exist" + resolve_robotics_uri_py.resolve_robotics_uri(uri_file) + + # Non-existing absolute URI path + with pytest.raises(FileNotFoundError): + uri_file = "file:///this/file/does/not.exist" + resolve_robotics_uri_py.resolve_robotics_uri(uri_file) + + # Existing absolute URI path + with tempfile.NamedTemporaryFile() as temp: + uri_file = f"file://{temp.name}" + path_of_file = resolve_robotics_uri_py.resolve_robotics_uri(uri_file) + assert path_of_file == pathlib.Path(temp.name) + + # Existing relative URI path (automatic conversion to absolute) + with tempfile.NamedTemporaryFile() as temp: + uri_file = f"file:/{temp.name}" + path_of_file = resolve_robotics_uri_py.resolve_robotics_uri(uri_file) + assert path_of_file == pathlib.Path(temp.name) + + # Fallback to file:// with no scheme + with tempfile.NamedTemporaryFile() as temp: + uri_file = f"{temp.name}" + path_of_file = resolve_robotics_uri_py.resolve_robotics_uri(uri_file) + assert path_of_file == pathlib.Path(temp.name) From 5253ab69856de5e38f52dcdad710b426b75237ae Mon Sep 17 00:00:00 2001 From: diegoferigo Date: Wed, 28 Feb 2024 13:43:14 +0100 Subject: [PATCH 10/20] Enhance search path detection logic from env vars --- .../resolve_robotics_uri_py.py | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py b/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py index 114cd7e..25d96a2 100644 --- a/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py +++ b/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py @@ -3,6 +3,11 @@ import pathlib import sys import warnings +from typing import Iterable + +# ===================== +# URI resolving helpers +# ===================== # Supported URI schemes SupportedSchemes = {"file", "package", "model"} @@ -35,19 +40,42 @@ # Function inspired from https://github.com/ami-iit/robot-log-visualizer/pull/51 -def get_search_paths_from_envs(env_list): - return [ +def get_search_paths_from_envs(env_list: Iterable[str]) -> list[pathlib.Path]: + # Read the searched paths from all the environment variables + search_paths = [ pathlib.Path(f) if (env != "AMENT_PREFIX_PATH") else pathlib.Path(f) / "share" for env in env_list if os.getenv(env) is not None for f in os.getenv(env).split(os.pathsep) ] + # Resolve and remove duplicate paths + search_paths = list({path.resolve() for path in search_paths}) + + # Keep only existing paths + existing_search_paths = [path for path in search_paths if path.is_dir()] + + # Notify the user of non-existing paths + if len(set(search_paths) - set(existing_search_paths)) > 0: + msg = "resolve-robotics-uri-py: Ignoring non-existing paths from env vars: {}." + warnings.warn( + msg.format( + pathlist_list_to_string(set(search_paths) - set(existing_search_paths)) + ) + ) + + return existing_search_paths + def pathlist_list_to_string(path_list): return " ".join(str(path) for path in path_list) +# =================== +# URI resolving logic +# =================== + + def resolve_robotics_uri(uri: str) -> pathlib.Path: """ Resolve a robotics URI to an absolute filename. From 0f98b0490d13eecbb0a4b7a4eea52d81031757e6 Mon Sep 17 00:00:00 2001 From: diegoferigo Date: Wed, 28 Feb 2024 14:44:25 +0100 Subject: [PATCH 11/20] Resolve symlinks and ".." in paths --- .../resolve_robotics_uri_py.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py b/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py index 25d96a2..916b6cc 100644 --- a/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py +++ b/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py @@ -113,19 +113,20 @@ def resolve_robotics_uri(uri: str) -> pathlib.Path: # Strip the URI scheme uri_path = uri.replace(f"{parsed_uri.scheme}://", "") + # Process file:// separately from the other schemes if parsed_uri.scheme == "file": # Ensure the URI path is absolute uri_path = uri_path if uri_path.startswith("/") else f"/{uri_path}" - # Create the file path - uri_file_path = pathlib.Path(uri_path) + # Create the file path, resolving symlinks and '..' + uri_file_path = pathlib.Path(uri_path).resolve() # Check that the file exists if not uri_file_path.is_file(): msg = "resolve-robotics-uri-py: No file corresponding to URI '{}' found" raise FileNotFoundError(msg.format(uri)) - return uri_file_path + return uri_file_path.resolve() # List of matching resources found model_filenames = [] @@ -135,6 +136,9 @@ def resolve_robotics_uri(uri: str) -> pathlib.Path: # Join the folder from environment variable and the URI path candidate_file_name = folder / uri_path + # Expand or resolve the file path (symlinks and ..) + candidate_file_name = candidate_file_name.resolve() + if not candidate_file_name.is_file(): continue @@ -153,7 +157,7 @@ def resolve_robotics_uri(uri: str) -> pathlib.Path: if len(model_filenames) >= 1: assert model_filenames[0].exists() - return pathlib.Path(model_filenames[0]) + return pathlib.Path(model_filenames[0]).resolve() def main(): From 7895155e3ce842ef2c536b7ed6055dbd75f65568 Mon Sep 17 00:00:00 2001 From: diegoferigo Date: Wed, 28 Feb 2024 12:34:29 +0100 Subject: [PATCH 12/20] Configure pytest with the right folder containing tests --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index d849aa1..bbe6bb8 100644 --- a/setup.cfg +++ b/setup.cfg @@ -59,4 +59,4 @@ all = [tool:pytest] addopts = -rsxX -v --strict-markers -testpaths = tests +testpaths = test From c761fd8f54f4be0395fb30e6868df15dae22be93 Mon Sep 17 00:00:00 2001 From: diegoferigo Date: Wed, 28 Feb 2024 13:43:58 +0100 Subject: [PATCH 13/20] Print output generated by pytest in CI Useful for debugging in CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c58e20..a81abe9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,7 +40,7 @@ jobs: - name: Run the Python tests shell: bash -l {0} - run: pytest + run: pytest --capture=no # This test requires the conda-forge::icub-models, # robotology::ergocub-software and From 90f028545d7995c2e4bebfe6868ba2fc313235ce Mon Sep 17 00:00:00 2001 From: diegoferigo Date: Wed, 28 Feb 2024 13:44:16 +0100 Subject: [PATCH 14/20] Enhance type hints --- src/resolve_robotics_uri_py/resolve_robotics_uri_py.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py b/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py index 916b6cc..91a7443 100644 --- a/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py +++ b/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py @@ -67,7 +67,7 @@ def get_search_paths_from_envs(env_list: Iterable[str]) -> list[pathlib.Path]: return existing_search_paths -def pathlist_list_to_string(path_list): +def pathlist_list_to_string(path_list: Iterable[str | pathlib.Path]) -> str: return " ".join(str(path) for path in path_list) From 929e8601e55141582f9a29a332512081113edfb2 Mon Sep 17 00:00:00 2001 From: diegoferigo Date: Wed, 28 Feb 2024 14:46:39 +0100 Subject: [PATCH 15/20] Fix tests on windows and macos --- test/test_resolve_robotics_uri_py.py | 68 +++++++++++++++++++--------- 1 file changed, 46 insertions(+), 22 deletions(-) diff --git a/test/test_resolve_robotics_uri_py.py b/test/test_resolve_robotics_uri_py.py index b4affba..60cfbcf 100644 --- a/test/test_resolve_robotics_uri_py.py +++ b/test/test_resolve_robotics_uri_py.py @@ -50,8 +50,10 @@ def test_scoped_uri(scheme: str, env_var_name="GZ_SIM_RESOURCE_PATH"): # URI path in top-level search dir with tempfile.TemporaryDirectory() as temp_dir: - pathlib.Path(temp_dir).mkdir(exist_ok=True) - top_level = pathlib.Path(temp_dir) / "top_level.txt" + + temp_dir_path = pathlib.Path(temp_dir).resolve() + temp_dir_path.mkdir(exist_ok=True) + top_level = temp_dir_path / "top_level.txt" top_level.touch(exist_ok=True) # Existing relative URI path not in search dirs @@ -61,24 +63,34 @@ def test_scoped_uri(scheme: str, env_var_name="GZ_SIM_RESOURCE_PATH"): # Existing absolute URI path in search dirs with pytest.raises(FileNotFoundError): - with export_env_var(name=env_var_name, value=temp_dir): + with export_env_var(name=env_var_name, value=str(temp_dir_path)): uri = f"{scheme}/top_level.txt" resolve_robotics_uri_py.resolve_robotics_uri(uri) # Existing relative URI path in search dirs - with export_env_var(name=env_var_name, value=temp_dir): - uri = f"{scheme}top_level.txt" - resolve_robotics_uri_py.resolve_robotics_uri(uri) + with export_env_var(name=env_var_name, value=str(temp_dir_path)): + relative_path = "top_level.txt" + uri = f"{scheme}{relative_path}" + path_of_file = resolve_robotics_uri_py.resolve_robotics_uri(uri) + assert path_of_file == path_of_file.resolve() + assert path_of_file == temp_dir_path / relative_path # Existing relative URI path in search dirs with multiple paths - with export_env_var(name=env_var_name, value=f"/another/dir:{temp_dir}"): - uri = f"{scheme}top_level.txt" - resolve_robotics_uri_py.resolve_robotics_uri(uri) + with export_env_var( + name=env_var_name, value=f"/another/dir{os.pathsep}{str(temp_dir_path)}" + ): + relative_path = "top_level.txt" + uri = f"{scheme}{relative_path}" + path_of_file = resolve_robotics_uri_py.resolve_robotics_uri(uri) + assert path_of_file == path_of_file.resolve() + assert path_of_file == temp_dir_path / relative_path # URI path in sub-level search dir with tempfile.TemporaryDirectory() as temp_dir: - (pathlib.Path(temp_dir) / "sub").mkdir(exist_ok=True) - level1 = pathlib.Path(temp_dir) / "sub" / "level1.txt" + + temp_dir_path = pathlib.Path(temp_dir).resolve() + level1 = temp_dir_path / "sub" / "level1.txt" + level1.parent.mkdir(exist_ok=True, parents=True) level1.touch(exist_ok=True) # Existing relative URI path not in search dirs @@ -88,19 +100,25 @@ def test_scoped_uri(scheme: str, env_var_name="GZ_SIM_RESOURCE_PATH"): # Existing absolute URI path in search dirs with pytest.raises(FileNotFoundError): - with export_env_var(name=env_var_name, value=temp_dir): + with export_env_var(name=env_var_name, value=str(temp_dir_path)): uri = f"{scheme}/sub/level1.txt" resolve_robotics_uri_py.resolve_robotics_uri(uri) # Existing relative URI path in search dirs - with export_env_var(name=env_var_name, value=temp_dir): - uri = f"{scheme}sub/level1.txt" - resolve_robotics_uri_py.resolve_robotics_uri(uri) + with export_env_var(name=env_var_name, value=str(temp_dir_path)): + relative_path = "sub/level1.txt" + uri = f"{scheme}{relative_path}" + path_of_file = resolve_robotics_uri_py.resolve_robotics_uri(uri) + assert path_of_file == temp_dir_path / relative_path # Existing relative URI path in search dirs with multiple paths - with export_env_var(name=env_var_name, value=f"/another/dir:{temp_dir}"): - uri = f"{scheme}sub/level1.txt" - resolve_robotics_uri_py.resolve_robotics_uri(uri) + with export_env_var( + name=env_var_name, value=f"/another/dir{os.pathsep}{str(temp_dir_path)}" + ): + relative_path = "sub/level1.txt" + uri = f"{scheme}{relative_path}" + path_of_file = resolve_robotics_uri_py.resolve_robotics_uri(uri) + assert path_of_file == temp_dir_path / relative_path def test_scheme_file(): @@ -119,18 +137,24 @@ def test_scheme_file(): # Existing absolute URI path with tempfile.NamedTemporaryFile() as temp: + temp_name = pathlib.Path(temp.name).resolve(strict=True) uri_file = f"file://{temp.name}" path_of_file = resolve_robotics_uri_py.resolve_robotics_uri(uri_file) - assert path_of_file == pathlib.Path(temp.name) + assert path_of_file == path_of_file.resolve() + assert path_of_file == temp_name # Existing relative URI path (automatic conversion to absolute) with tempfile.NamedTemporaryFile() as temp: + temp_name = pathlib.Path(temp.name).resolve(strict=True) uri_file = f"file:/{temp.name}" path_of_file = resolve_robotics_uri_py.resolve_robotics_uri(uri_file) - assert path_of_file == pathlib.Path(temp.name) + assert path_of_file == path_of_file.resolve() + assert path_of_file == temp_name # Fallback to file:// with no scheme with tempfile.NamedTemporaryFile() as temp: - uri_file = f"{temp.name}" + temp_name = pathlib.Path(temp.name).resolve(strict=True) + uri_file = f"{temp_name}" path_of_file = resolve_robotics_uri_py.resolve_robotics_uri(uri_file) - assert path_of_file == pathlib.Path(temp.name) + assert path_of_file == path_of_file.resolve() + assert path_of_file == temp_name From a5ae14af848b79ea73920168c36fc588895e1ebc Mon Sep 17 00:00:00 2001 From: diegoferigo Date: Wed, 28 Feb 2024 19:24:53 +0100 Subject: [PATCH 16/20] Update file scheme logic --- .../resolve_robotics_uri_py.py | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py b/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py index 91a7443..967fbb7 100644 --- a/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py +++ b/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py @@ -90,9 +90,10 @@ def resolve_robotics_uri(uri: str) -> pathlib.Path: FileNotFoundError: If no file corresponding to the URI is found. """ - # If the URI has no scheme, use by default the file:// - if "://" not in uri: - uri = f"file://{uri}" + # If the URI has no scheme, use by default file:// which maps the resolved input + # path to a URI with empty authority + if not any(uri.startswith(scheme) for scheme in SupportedSchemes): + uri = f"file://{pathlib.Path(uri).resolve()}" # Get scheme from URI from urllib.parse import urlparse @@ -102,22 +103,22 @@ def resolve_robotics_uri(uri: str) -> pathlib.Path: # We only support the following URI schemes at the moment: # - # * file://: to pass an absolute file path directly - # * model://: SDF-style model URI - # * package://: ROS-style package URI + # * file:/ to pass an absolute file path directly + # * model:// SDF-style model URI + # * package:// ROS-style package URI + # + # Note that file has only one trailing '/' as per RFC8089: + # https://datatracker.ietf.org/doc/html/rfc8089 # if parsed_uri.scheme not in SupportedSchemes: msg = "resolve-robotics-uri-py: Passed URI '{}' use non-supported scheme '{}'" raise FileNotFoundError(msg.format(uri, parsed_uri.scheme)) - # Strip the URI scheme - uri_path = uri.replace(f"{parsed_uri.scheme}://", "") + # Strip the scheme from the URI + uri_path = uri.replace(f"{parsed_uri.scheme}//", "") - # Process file:// separately from the other schemes + # Process file:/ separately from the other schemes if parsed_uri.scheme == "file": - # Ensure the URI path is absolute - uri_path = uri_path if uri_path.startswith("/") else f"/{uri_path}" - # Create the file path, resolving symlinks and '..' uri_file_path = pathlib.Path(uri_path).resolve() From 81908de23dc3f9398a18bacc25477cd3063d5992 Mon Sep 17 00:00:00 2001 From: diegoferigo Date: Wed, 28 Feb 2024 19:25:40 +0100 Subject: [PATCH 17/20] Update tests --- test/test_resolve_robotics_uri_py.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/test/test_resolve_robotics_uri_py.py b/test/test_resolve_robotics_uri_py.py index 60cfbcf..bd82d64 100644 --- a/test/test_resolve_robotics_uri_py.py +++ b/test/test_resolve_robotics_uri_py.py @@ -125,28 +125,23 @@ def test_scheme_file(): clear_env_vars() - # Non-existing relative URI path - with pytest.raises(FileNotFoundError): - uri_file = "file://this/file/does/not.exist" - resolve_robotics_uri_py.resolve_robotics_uri(uri_file) - # Non-existing absolute URI path with pytest.raises(FileNotFoundError): - uri_file = "file:///this/file/does/not.exist" + uri_file = "file://" + "/this/file/does/not.exist" resolve_robotics_uri_py.resolve_robotics_uri(uri_file) - # Existing absolute URI path + # Existing absolute URI path with empty authority with tempfile.NamedTemporaryFile() as temp: temp_name = pathlib.Path(temp.name).resolve(strict=True) - uri_file = f"file://{temp.name}" + uri_file = "file://" + temp.name path_of_file = resolve_robotics_uri_py.resolve_robotics_uri(uri_file) assert path_of_file == path_of_file.resolve() assert path_of_file == temp_name - # Existing relative URI path (automatic conversion to absolute) + # Existing absolute URI path without authority with tempfile.NamedTemporaryFile() as temp: temp_name = pathlib.Path(temp.name).resolve(strict=True) - uri_file = f"file:/{temp.name}" + uri_file = "file:" + temp.name path_of_file = resolve_robotics_uri_py.resolve_robotics_uri(uri_file) assert path_of_file == path_of_file.resolve() assert path_of_file == temp_name From a31df11ca74d35fa9bd22104e5d45c5408869e05 Mon Sep 17 00:00:00 2001 From: Silvio Traversaro Date: Wed, 28 Feb 2024 20:15:47 +0100 Subject: [PATCH 18/20] Add test for resolving fine an existing file without file:/ scheme --- test/test_resolve_robotics_uri_py.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/test_resolve_robotics_uri_py.py b/test/test_resolve_robotics_uri_py.py index bd82d64..58f5736 100644 --- a/test/test_resolve_robotics_uri_py.py +++ b/test/test_resolve_robotics_uri_py.py @@ -1,6 +1,7 @@ import contextlib import os import pathlib +import sys import tempfile from typing import ContextManager @@ -153,3 +154,9 @@ def test_scheme_file(): path_of_file = resolve_robotics_uri_py.resolve_robotics_uri(uri_file) assert path_of_file == path_of_file.resolve() assert path_of_file == temp_name + + # Try to find an existing file (the Python executable) without any file:/ scheme + path_of_python_executable = resolve_robotics_uri_py.resolve_robotics_uri(sys.executable) + assert path_of_python_executable == pathlib.Path(sys.executable) + + From a52927ee18bbe7165ff54ffeea25c247c1c92c06 Mon Sep 17 00:00:00 2001 From: diegoferigo Date: Thu, 29 Feb 2024 08:44:55 +0100 Subject: [PATCH 19/20] Move file:/ logic --- .../resolve_robotics_uri_py.py | 47 ++++++++++++------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py b/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py index 967fbb7..d85d0ab 100644 --- a/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py +++ b/src/resolve_robotics_uri_py/resolve_robotics_uri_py.py @@ -95,6 +95,32 @@ def resolve_robotics_uri(uri: str) -> pathlib.Path: if not any(uri.startswith(scheme) for scheme in SupportedSchemes): uri = f"file://{pathlib.Path(uri).resolve()}" + # ================================================ + # Process file:/ separately from the other schemes + # ================================================ + + # This is the file URI scheme as per RFC8089: + # https://datatracker.ietf.org/doc/html/rfc8089 + + if uri.startswith("file:"): + # Strip the scheme from the URI + uri = uri.replace(f"file://", "") + uri = uri.replace(f"file:", "") + + # Create the file path, resolving symlinks and '..' + uri_file_path = pathlib.Path(uri).resolve() + + # Check that the file exists + if not uri_file_path.is_file(): + msg = "resolve-robotics-uri-py: No file corresponding to URI '{}' found" + raise FileNotFoundError(msg.format(uri)) + + return uri_file_path.resolve() + + # ========================= + # Process the other schemes + # ========================= + # Get scheme from URI from urllib.parse import urlparse @@ -107,31 +133,18 @@ def resolve_robotics_uri(uri: str) -> pathlib.Path: # * model:// SDF-style model URI # * package:// ROS-style package URI # - # Note that file has only one trailing '/' as per RFC8089: - # https://datatracker.ietf.org/doc/html/rfc8089 - # if parsed_uri.scheme not in SupportedSchemes: msg = "resolve-robotics-uri-py: Passed URI '{}' use non-supported scheme '{}'" raise FileNotFoundError(msg.format(uri, parsed_uri.scheme)) # Strip the scheme from the URI - uri_path = uri.replace(f"{parsed_uri.scheme}//", "") - - # Process file:/ separately from the other schemes - if parsed_uri.scheme == "file": - # Create the file path, resolving symlinks and '..' - uri_file_path = pathlib.Path(uri_path).resolve() - - # Check that the file exists - if not uri_file_path.is_file(): - msg = "resolve-robotics-uri-py: No file corresponding to URI '{}' found" - raise FileNotFoundError(msg.format(uri)) - - return uri_file_path.resolve() + uri_path = uri + uri_path = uri_path.replace(f"{parsed_uri.scheme}://", "") # List of matching resources found model_filenames = [] + # Search the resource in the path from the env variables for folder in set(get_search_paths_from_envs(SupportedEnvVars)): # Join the folder from environment variable and the URI path @@ -148,7 +161,7 @@ def resolve_robotics_uri(uri: str) -> pathlib.Path: model_filenames.append(candidate_file_name) if len(model_filenames) == 0: - msg = "resolve-robotics-uri-py: No file corresponding to uri '{}' found" + msg = "resolve-robotics-uri-py: No file corresponding to URI '{}' found" raise FileNotFoundError(msg.format(uri)) if len(model_filenames) > 1: From e6ef710d165a04767e6c75c6bef79f480ec5fe05 Mon Sep 17 00:00:00 2001 From: diegoferigo Date: Thu, 29 Feb 2024 08:58:29 +0100 Subject: [PATCH 20/20] Test file URIs in CI --- .github/workflows/ci.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a81abe9..3d4852f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -53,3 +53,13 @@ jobs: resolve-robotics-uri-py package://ergoCub/robots/ergoCubSN000/model.urdf resolve-robotics-uri-py package://moveit_resources_panda_description/urdf/panda.urdf ! resolve-robotics-uri-py package://this/file/does/not/exist + - name: Check command line helper (Ubuntu and macOS) + if: startsWith(matrix.os, 'ubuntu') || startsWith(matrix.os, 'macos') + shell: bash -l {0} + run: | + mkdir -p /tmp/folder/ && touch "/tmp/folder/file with spaces.txt" + mkdir -p /tmp/folder/ && touch "/tmp/folder/file_without_spaces.txt" + resolve-robotics-uri-py "file:///tmp/folder/file_without_spaces.txt" + resolve-robotics-uri-py "file:///tmp/folder/file with spaces.txt" + resolve-robotics-uri-py "file:/tmp/folder/file_without_spaces.txt" + resolve-robotics-uri-py "file:/tmp/folder/file with spaces.txt"