[1/4] dts: constrain DPDK source flag

Message ID 20240122182611.1904974-2-luca.vizzarro@arm.com (mailing list archive)
State Superseded, archived
Delegated to: Thomas Monjalon
Headers
Series dts: error and usage improvements |

Checks

Context Check Description
ci/checkpatch success coding style OK

Commit Message

Luca Vizzarro Jan. 22, 2024, 6:26 p.m. UTC
  DTS needs an input to gather the DPDK source code from. This is then
built on the remote target. This commit makes sure that this input is
more constrained, separating the Git revision ID – used to create a
tarball using Git – and providing tarballed source code directly, while
retaining mutual exclusion.

This makes the code more readable and easier to handle for input
validation, of which this commit introduces a basic one based on the
pre-existing code.

Moreover it ensures that these flags are explicitly required to be set
by the user, dropping a default value. It also aids the user understand
how to use the DTS in the scenario it is ran without any arguments set.

Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
---
 doc/guides/tools/dts.rst  |  8 +++--
 dts/framework/settings.py | 64 ++++++++++++++++++++++++++++-----------
 dts/framework/utils.py    | 43 ++++++++++++++++----------
 3 files changed, 79 insertions(+), 36 deletions(-)
  

Comments

Juraj Linkeš Jan. 29, 2024, 11:47 a.m. UTC | #1
Unaddressed
On Mon, Jan 22, 2024 at 7:26 PM Luca Vizzarro <luca.vizzarro@arm.com> wrote:
>
> DTS needs an input to gather the DPDK source code from. This is then
> built on the remote target. This commit makes sure that this input is
> more constrained, separating the Git revision ID – used to create a
> tarball using Git – and providing tarballed source code directly, while
> retaining mutual exclusion.
>

I didn't see the mutual exclusion being enforced in the code. From
what I can tell, I could pass both --tarball FILEPATH and --revision
and the former would be used without checking the latter.

> This makes the code more readable and easier to handle for input
> validation, of which this commit introduces a basic one based on the
> pre-existing code.
>

I wanted to have just one argument for basically the same thing, but
the input is pretty different so a two argument solution actually
sounds better.

> Moreover it ensures that these flags are explicitly required to be set
> by the user, dropping a default value. It also aids the user understand
> how to use the DTS in the scenario it is ran without any arguments set.
>
> Reviewed-by: Paul Szczepanek <paul.szczepanek@arm.com>
> Signed-off-by: Luca Vizzarro <luca.vizzarro@arm.com>
> ---
>  doc/guides/tools/dts.rst  |  8 +++--
>  dts/framework/settings.py | 64 ++++++++++++++++++++++++++++-----------
>  dts/framework/utils.py    | 43 ++++++++++++++++----------
>  3 files changed, 79 insertions(+), 36 deletions(-)
>
> diff --git a/doc/guides/tools/dts.rst b/doc/guides/tools/dts.rst
> index 846696e14e..6e2da317e8 100644
> --- a/doc/guides/tools/dts.rst
> +++ b/doc/guides/tools/dts.rst
> @@ -215,12 +215,16 @@ DTS is run with ``main.py`` located in the ``dts`` directory after entering Poet
>  .. code-block:: console
>
>     (dts-py3.10) $ ./main.py --help
> -   usage: main.py [-h] [--config-file CONFIG_FILE] [--output-dir OUTPUT_DIR] [-t TIMEOUT] [-v] [-s] [--tarball TARBALL] [--compile-timeout COMPILE_TIMEOUT] [--test-cases TEST_CASES] [--re-run RE_RUN]
> +   usage: main.py [-h] (--tarball FILEPATH | --revision ID) [--config-file CONFIG_FILE] [--output-dir OUTPUT_DIR] [-t TIMEOUT] [-v] [-s] [--compile-timeout COMPILE_TIMEOUT] [--test-cases TEST_CASES] [--re-run RE_RUN]
>
>     Run DPDK test suites. All options may be specified with the environment variables provided in brackets. Command line arguments have higher priority.
>
>     options:
>     -h, --help            show this help message and exit
> +   --tarball FILEPATH, --snapshot FILEPATH
> +                         Path to DPDK source code tarball to test. (default: None)
> +   --revision ID, --rev ID, --git-ref ID
> +                         Git revision ID to test. Could be commit, tag, tree ID and vice versa. To test local changes, first commit them, then use their commit ID (default: None)
>     --config-file CONFIG_FILE
>                           [DTS_CFG_FILE] configuration file that describes the test cases, SUTs and targets. (default: ./conf.yaml)
>     --output-dir OUTPUT_DIR, --output OUTPUT_DIR
> @@ -229,8 +233,6 @@ DTS is run with ``main.py`` located in the ``dts`` directory after entering Poet
>                           [DTS_TIMEOUT] The default timeout for all DTS operations except for compiling DPDK. (default: 15)
>     -v, --verbose         [DTS_VERBOSE] Specify to enable verbose output, logging all messages to the console. (default: False)
>     -s, --skip-setup      [DTS_SKIP_SETUP] Specify to skip all setup steps on SUT and TG nodes. (default: None)
> -   --tarball TARBALL, --snapshot TARBALL, --git-ref TARBALL
> -                         [DTS_DPDK_TARBALL] Path to DPDK source code tarball or a git commit ID, tag ID or tree ID to test. To test local changes, first commit them, then use the commit ID with this option. (default: dpdk.tar.xz)
>     --compile-timeout COMPILE_TIMEOUT
>                           [DTS_COMPILE_TIMEOUT] The timeout for compiling DPDK. (default: 1200)
>     --test-cases TEST_CASES
> diff --git a/dts/framework/settings.py b/dts/framework/settings.py
> index 609c8d0e62..2d0365e763 100644
> --- a/dts/framework/settings.py
> +++ b/dts/framework/settings.py
> @@ -76,7 +76,8 @@
>  from pathlib import Path
>  from typing import Any, TypeVar
>
> -from .utils import DPDKGitTarball
> +from .exception import ConfigurationError
> +from .utils import DPDKGitTarball, get_commit_id
>
>  _T = TypeVar("_T")
>
> @@ -149,6 +150,26 @@ def __call__(
>      return _EnvironmentArgument
>
>
> +def _parse_tarball(filepath: str) -> Path:
> +    """Validate if the filepath is valid and return a Path object."""

whether `filepath` is valid

Even though private methods don't get included in the API docs, I like
to be consistent. In this case, it doesn't really detract (but maybe
some disability would prove this wrong) while adding a bit (the fact
that we're referencing the argument).

I think the name should either be _validate_tarball or
_parse_tarball_path. The argument name is two words, so let's put an
underscore between them.

> +    path = Path(filepath)
> +    if not path.exists() or not path.is_file():
> +        raise argparse.ArgumentTypeError(
> +            "the file path provided is not a valid file")

Typo: capital T

> +    return path
> +
> +
> +def _parse_revision_id(rev_id: str) -> str:
> +    """Retrieve effective commit ID from a revision ID. While validating it."""

I think this would read better as one sentence.

> +
> +    try:
> +        return get_commit_id(rev_id)
> +    except ConfigurationError:
> +        raise argparse.ArgumentTypeError(
> +            "the Git revision ID supplied is invalid or ambiguous"
> +        )
> +
> +
>  @dataclass(slots=True)
>  class Settings:
>      """Default framework-wide user settings.
> @@ -167,7 +188,7 @@ class Settings:
>      #:
>      skip_setup: bool = False
>      #:
> -    dpdk_tarball_path: Path | str = "dpdk.tar.xz"
> +    dpdk_tarball_path: Path | str = ""
>      #:
>      compile_timeout: float = 1200
>      #:
> @@ -186,6 +207,28 @@ def _get_parser() -> argparse.ArgumentParser:
>          formatter_class=argparse.ArgumentDefaultsHelpFormatter,
>      )
>
> +    dpdk_source = parser.add_mutually_exclusive_group(required=True)
> +
> +    dpdk_source.add_argument(
> +        "--tarball",
> +        "--snapshot",
> +        action='store',
> +        type=_parse_tarball,
> +        help="Path to DPDK source code tarball to test.",
> +        metavar="FILEPATH",
> +    )
> +
> +    dpdk_source.add_argument(
> +        "--revision",
> +        "--rev",
> +        "--git-ref",
> +        type=_parse_revision_id,
> +        metavar="ID",

Since this is a patch with improvements, maybe we could add metavars
to other arguments as well. It looks pretty good.

> +        help="Git revision ID to test. Could be commit, tag, tree ID and "
> +        "vice versa. To test local changes, first commit them, then use their "
> +        "commit ID",
> +    )
> +

This removes the support for environment variables. It's possible we
don't want the support for these two arguments. Maybe we don't need
the support for variables at all. I'm leaning towards supporting the
env variables, but we probably should refactor how they're done. The
easiest would be to not do them with action, but just modifying the
default value if set. That would be a worthwhile improvement.

>      parser.add_argument(
>          "--config-file",
>          action=_env_arg("DTS_CFG_FILE"),
> @@ -229,18 +272,6 @@ def _get_parser() -> argparse.ArgumentParser:
>          help="[DTS_SKIP_SETUP] Specify to skip all setup steps on SUT and TG nodes.",
>      )
>
> -    parser.add_argument(
> -        "--tarball",
> -        "--snapshot",
> -        "--git-ref",
> -        action=_env_arg("DTS_DPDK_TARBALL"),
> -        default=SETTINGS.dpdk_tarball_path,
> -        type=Path,
> -        help="[DTS_DPDK_TARBALL] Path to DPDK source code tarball or a git commit ID, "
> -        "tag ID or tree ID to test. To test local changes, first commit them, "
> -        "then use the commit ID with this option.",
> -    )
> -
>      parser.add_argument(
>          "--compile-timeout",
>          action=_env_arg("DTS_COMPILE_TIMEOUT"),
> @@ -283,9 +314,8 @@ def get_settings() -> Settings:
>          verbose=parsed_args.verbose,
>          skip_setup=parsed_args.skip_setup,
>          dpdk_tarball_path=Path(
> -            Path(DPDKGitTarball(parsed_args.tarball, parsed_args.output_dir))
> -            if not os.path.exists(parsed_args.tarball)
> -            else Path(parsed_args.tarball)
> +            parsed_args.tarball or
> +            DPDKGitTarball(parsed_args.revision, parsed_args.output_dir)
>          ),
>          compile_timeout=parsed_args.compile_timeout,
>          test_cases=(parsed_args.test_cases.split(",") if parsed_args.test_cases else []),
> diff --git a/dts/framework/utils.py b/dts/framework/utils.py
> index cc5e458cc8..dbbec8b00a 100644
> --- a/dts/framework/utils.py
> +++ b/dts/framework/utils.py
> @@ -70,6 +70,32 @@ def get_packet_summaries(packets: list[Packet]) -> str:
>      return f"Packet contents: \n{packet_summaries}"
>
>
> +def get_commit_id(rev_id: str) -> str:
> +    """Given a Git revision ID, return the corresponding commit ID.
> +
> +    Args:
> +        rev_id: The Git revision ID.
> +
> +    Raises:
> +        ConfigurationError: The ``git rev-parse`` command failed. Suggesting
> +            an invalid or ambiguous revision ID was supplied.

This would also probably read better as one sentence.

> +    """
> +    result = subprocess.run(
> +        ["git", "rev-parse", "--verify", rev_id],
> +        text=True,
> +        capture_output=True,
> +    )
> +    if result.returncode != 0:
> +        raise ConfigurationError(
> +            f"{rev_id} is neither a path to an existing DPDK "
> +            "archive nor a valid git reference.\n"
> +            f"Command: {result.args}\n"
> +            f"Stdout: {result.stdout}\n"
> +            f"Stderr: {result.stderr}"
> +        )

We shuffled the order of operations a bit and now the error message
doesn't correspond.

> +    return result.stdout.strip()
> +
> +
>  class StrEnum(Enum):
>      """Enum with members stored as strings."""
>
> @@ -170,7 +196,6 @@ def __init__(
>
>          self._tarball_dir = Path(output_dir, "tarball")
>
> -        self._get_commit_id()

Makes sense to move this outside.

>          self._create_tarball_dir()
>
>          self._tarball_name = (
> @@ -180,21 +205,7 @@ def __init__(
>          if not self._tarball_path:
>              self._create_tarball()
>
> -    def _get_commit_id(self) -> None:
> -        result = subprocess.run(
> -            ["git", "rev-parse", "--verify", self._git_ref],
> -            text=True,
> -            capture_output=True,
> -        )
> -        if result.returncode != 0:
> -            raise ConfigurationError(
> -                f"{self._git_ref} is neither a path to an existing DPDK "
> -                "archive nor a valid git reference.\n"
> -                f"Command: {result.args}\n"
> -                f"Stdout: {result.stdout}\n"
> -                f"Stderr: {result.stderr}"
> -            )
> -        self._git_ref = result.stdout.strip()
> +
>
>      def _create_tarball_dir(self) -> None:
>          os.makedirs(self._tarball_dir, exist_ok=True)
> --
> 2.34.1
>
  
Luca Vizzarro Feb. 23, 2024, 7:09 p.m. UTC | #2
Hi Juraj,

Thank you for your review!

On 29/01/2024 11:47, Juraj Linkeš wrote:
> I didn't see the mutual exclusion being enforced in the code. From
> what I can tell, I could pass both --tarball FILEPATH and --revision
> and the former would be used without checking the latter.

Yep, it is enforced in the code, you may have missed it. The two 
arguments are under the same mutual exclusion group in line 220:

dpdk_source = parser.add_mutually_exclusive_group(required=True)

When using both arguments `argparse` will automatically complain that 
you can only use one or the other.

> whether `filepath` is valid
> Even though private methods don't get included in the API docs, I like
> to be consistent. In this case, it doesn't really detract (but maybe
> some disability would prove this wrong) while adding a bit (the fact
> that we're referencing the argument).

Yes, it is a good idea. Especially since this will work within IDEs.

> I think the name should either be _validate_tarball or
> _parse_tarball_path. The argument name is two words, so let's put an
> underscore between them.

Ack.

> I think this would read better as one sentence.

Ack.

> Since this is a patch with improvements, maybe we could add metavars
> to other arguments as well. It looks pretty good.

Sure, no problem!

> This removes the support for environment variables. It's possible we
> don't want the support for these two arguments. Maybe we don't need
> the support for variables at all. I'm leaning towards supporting the
> env variables, but we probably should refactor how they're done. The
> easiest would be to not do them with action, but just modifying the
> default value if set. That would be a worthwhile improvement.

I've tried to find a way to still keep them. But with arguments done 
this way, it is somewhat hard to understand the provenance of the value, 
whether it's set in the arguments, an environment variable or just the 
default value. Therefore, give a useful error message to the user when 
using something invalid. I'll try to come up with something as you 
suggested, although I am not entirely convinced it'll be ideal.

> This would also probably read better as one sentence.

Ack.

> We shuffled the order of operations a bit and now the error message
> doesn't correspond.

Sorry, I don't think I am understanding what you are referring to 
exactly. What do you mean?

Best,
Luca
  
Juraj Linkeš March 1, 2024, 10:22 a.m. UTC | #3
On Fri, Feb 23, 2024 at 8:09 PM Luca Vizzarro <Luca.Vizzarro@arm.com> wrote:
>
> Hi Juraj,
>
> Thank you for your review!
>
> On 29/01/2024 11:47, Juraj Linkeš wrote:
> > I didn't see the mutual exclusion being enforced in the code. From
> > what I can tell, I could pass both --tarball FILEPATH and --revision
> > and the former would be used without checking the latter.
>
> Yep, it is enforced in the code, you may have missed it. The two
> arguments are under the same mutual exclusion group in line 220:
>
> dpdk_source = parser.add_mutually_exclusive_group(required=True)
>
> When using both arguments `argparse` will automatically complain that
> you can only use one or the other.
>

Yes, I missed this. This looks great.

> > whether `filepath` is valid
> > Even though private methods don't get included in the API docs, I like
> > to be consistent. In this case, it doesn't really detract (but maybe
> > some disability would prove this wrong) while adding a bit (the fact
> > that we're referencing the argument).
>
> Yes, it is a good idea. Especially since this will work within IDEs.
>
> > I think the name should either be _validate_tarball or
> > _parse_tarball_path. The argument name is two words, so let's put an
> > underscore between them.
>
> Ack.
>
> > I think this would read better as one sentence.
>
> Ack.
>
> > Since this is a patch with improvements, maybe we could add metavars
> > to other arguments as well. It looks pretty good.
>
> Sure, no problem!
>
> > This removes the support for environment variables. It's possible we
> > don't want the support for these two arguments. Maybe we don't need
> > the support for variables at all. I'm leaning towards supporting the
> > env variables, but we probably should refactor how they're done. The
> > easiest would be to not do them with action, but just modifying the
> > default value if set. That would be a worthwhile improvement.
>
> I've tried to find a way to still keep them. But with arguments done
> this way, it is somewhat hard to understand the provenance of the value,
> whether it's set in the arguments, an environment variable or just the
> default value. Therefore, give a useful error message to the user when
> using something invalid. I'll try to come up with something as you
> suggested, although I am not entirely convinced it'll be ideal.
>

For reference, my test case blocking patch implements an alternative
approach: https://patches.dpdk.org/project/dpdk/patch/20240223075502.60485-8-juraj.linkes@pantheon.tech/

It's the same thing, significantly simplified. Looks like it could work here.

> > This would also probably read better as one sentence.
>
> Ack.
>
> > We shuffled the order of operations a bit and now the error message
> > doesn't correspond.
>
> Sorry, I don't think I am understanding what you are referring to
> exactly. What do you mean?
>

You removed what I commented on (in the future, please keep that so
what we know what the original comment relates to), so here's what I
commented on:

+    if result.returncode != 0:
+        raise ConfigurationError(
+            f"{rev_id} is neither a path to an existing DPDK "
+            "archive nor a valid git reference.\n"
+            f"Command: {result.args}\n"
+            f"Stdout: {result.stdout}\n"
+            f"Stderr: {result.stderr}"
+        )

Previously, this was inside DPDKGitTarball which assumed it was passed
git ref because the command line arg is not a path. In this patch, the
git ref arg is decoupled from the tarball path, so the function
shouldn't reference the path.

> Best,
> Luca
  

Patch

diff --git a/doc/guides/tools/dts.rst b/doc/guides/tools/dts.rst
index 846696e14e..6e2da317e8 100644
--- a/doc/guides/tools/dts.rst
+++ b/doc/guides/tools/dts.rst
@@ -215,12 +215,16 @@  DTS is run with ``main.py`` located in the ``dts`` directory after entering Poet
 .. code-block:: console
 
    (dts-py3.10) $ ./main.py --help
-   usage: main.py [-h] [--config-file CONFIG_FILE] [--output-dir OUTPUT_DIR] [-t TIMEOUT] [-v] [-s] [--tarball TARBALL] [--compile-timeout COMPILE_TIMEOUT] [--test-cases TEST_CASES] [--re-run RE_RUN]
+   usage: main.py [-h] (--tarball FILEPATH | --revision ID) [--config-file CONFIG_FILE] [--output-dir OUTPUT_DIR] [-t TIMEOUT] [-v] [-s] [--compile-timeout COMPILE_TIMEOUT] [--test-cases TEST_CASES] [--re-run RE_RUN]
 
    Run DPDK test suites. All options may be specified with the environment variables provided in brackets. Command line arguments have higher priority.
 
    options:
    -h, --help            show this help message and exit
+   --tarball FILEPATH, --snapshot FILEPATH
+                         Path to DPDK source code tarball to test. (default: None)
+   --revision ID, --rev ID, --git-ref ID
+                         Git revision ID to test. Could be commit, tag, tree ID and vice versa. To test local changes, first commit them, then use their commit ID (default: None)
    --config-file CONFIG_FILE
                          [DTS_CFG_FILE] configuration file that describes the test cases, SUTs and targets. (default: ./conf.yaml)
    --output-dir OUTPUT_DIR, --output OUTPUT_DIR
@@ -229,8 +233,6 @@  DTS is run with ``main.py`` located in the ``dts`` directory after entering Poet
                          [DTS_TIMEOUT] The default timeout for all DTS operations except for compiling DPDK. (default: 15)
    -v, --verbose         [DTS_VERBOSE] Specify to enable verbose output, logging all messages to the console. (default: False)
    -s, --skip-setup      [DTS_SKIP_SETUP] Specify to skip all setup steps on SUT and TG nodes. (default: None)
-   --tarball TARBALL, --snapshot TARBALL, --git-ref TARBALL
-                         [DTS_DPDK_TARBALL] Path to DPDK source code tarball or a git commit ID, tag ID or tree ID to test. To test local changes, first commit them, then use the commit ID with this option. (default: dpdk.tar.xz)
    --compile-timeout COMPILE_TIMEOUT
                          [DTS_COMPILE_TIMEOUT] The timeout for compiling DPDK. (default: 1200)
    --test-cases TEST_CASES
diff --git a/dts/framework/settings.py b/dts/framework/settings.py
index 609c8d0e62..2d0365e763 100644
--- a/dts/framework/settings.py
+++ b/dts/framework/settings.py
@@ -76,7 +76,8 @@ 
 from pathlib import Path
 from typing import Any, TypeVar
 
-from .utils import DPDKGitTarball
+from .exception import ConfigurationError
+from .utils import DPDKGitTarball, get_commit_id
 
 _T = TypeVar("_T")
 
@@ -149,6 +150,26 @@  def __call__(
     return _EnvironmentArgument
 
 
+def _parse_tarball(filepath: str) -> Path:
+    """Validate if the filepath is valid and return a Path object."""
+    path = Path(filepath)
+    if not path.exists() or not path.is_file():
+        raise argparse.ArgumentTypeError(
+            "the file path provided is not a valid file")
+    return path
+
+
+def _parse_revision_id(rev_id: str) -> str:
+    """Retrieve effective commit ID from a revision ID. While validating it."""
+
+    try:
+        return get_commit_id(rev_id)
+    except ConfigurationError:
+        raise argparse.ArgumentTypeError(
+            "the Git revision ID supplied is invalid or ambiguous"
+        )
+
+
 @dataclass(slots=True)
 class Settings:
     """Default framework-wide user settings.
@@ -167,7 +188,7 @@  class Settings:
     #:
     skip_setup: bool = False
     #:
-    dpdk_tarball_path: Path | str = "dpdk.tar.xz"
+    dpdk_tarball_path: Path | str = ""
     #:
     compile_timeout: float = 1200
     #:
@@ -186,6 +207,28 @@  def _get_parser() -> argparse.ArgumentParser:
         formatter_class=argparse.ArgumentDefaultsHelpFormatter,
     )
 
+    dpdk_source = parser.add_mutually_exclusive_group(required=True)
+
+    dpdk_source.add_argument(
+        "--tarball",
+        "--snapshot",
+        action='store',
+        type=_parse_tarball,
+        help="Path to DPDK source code tarball to test.",
+        metavar="FILEPATH",
+    )
+
+    dpdk_source.add_argument(
+        "--revision",
+        "--rev",
+        "--git-ref",
+        type=_parse_revision_id,
+        metavar="ID",
+        help="Git revision ID to test. Could be commit, tag, tree ID and "
+        "vice versa. To test local changes, first commit them, then use their "
+        "commit ID",
+    )
+
     parser.add_argument(
         "--config-file",
         action=_env_arg("DTS_CFG_FILE"),
@@ -229,18 +272,6 @@  def _get_parser() -> argparse.ArgumentParser:
         help="[DTS_SKIP_SETUP] Specify to skip all setup steps on SUT and TG nodes.",
     )
 
-    parser.add_argument(
-        "--tarball",
-        "--snapshot",
-        "--git-ref",
-        action=_env_arg("DTS_DPDK_TARBALL"),
-        default=SETTINGS.dpdk_tarball_path,
-        type=Path,
-        help="[DTS_DPDK_TARBALL] Path to DPDK source code tarball or a git commit ID, "
-        "tag ID or tree ID to test. To test local changes, first commit them, "
-        "then use the commit ID with this option.",
-    )
-
     parser.add_argument(
         "--compile-timeout",
         action=_env_arg("DTS_COMPILE_TIMEOUT"),
@@ -283,9 +314,8 @@  def get_settings() -> Settings:
         verbose=parsed_args.verbose,
         skip_setup=parsed_args.skip_setup,
         dpdk_tarball_path=Path(
-            Path(DPDKGitTarball(parsed_args.tarball, parsed_args.output_dir))
-            if not os.path.exists(parsed_args.tarball)
-            else Path(parsed_args.tarball)
+            parsed_args.tarball or
+            DPDKGitTarball(parsed_args.revision, parsed_args.output_dir)
         ),
         compile_timeout=parsed_args.compile_timeout,
         test_cases=(parsed_args.test_cases.split(",") if parsed_args.test_cases else []),
diff --git a/dts/framework/utils.py b/dts/framework/utils.py
index cc5e458cc8..dbbec8b00a 100644
--- a/dts/framework/utils.py
+++ b/dts/framework/utils.py
@@ -70,6 +70,32 @@  def get_packet_summaries(packets: list[Packet]) -> str:
     return f"Packet contents: \n{packet_summaries}"
 
 
+def get_commit_id(rev_id: str) -> str:
+    """Given a Git revision ID, return the corresponding commit ID.
+
+    Args:
+        rev_id: The Git revision ID.
+
+    Raises:
+        ConfigurationError: The ``git rev-parse`` command failed. Suggesting
+            an invalid or ambiguous revision ID was supplied.
+    """
+    result = subprocess.run(
+        ["git", "rev-parse", "--verify", rev_id],
+        text=True,
+        capture_output=True,
+    )
+    if result.returncode != 0:
+        raise ConfigurationError(
+            f"{rev_id} is neither a path to an existing DPDK "
+            "archive nor a valid git reference.\n"
+            f"Command: {result.args}\n"
+            f"Stdout: {result.stdout}\n"
+            f"Stderr: {result.stderr}"
+        )
+    return result.stdout.strip()
+
+
 class StrEnum(Enum):
     """Enum with members stored as strings."""
 
@@ -170,7 +196,6 @@  def __init__(
 
         self._tarball_dir = Path(output_dir, "tarball")
 
-        self._get_commit_id()
         self._create_tarball_dir()
 
         self._tarball_name = (
@@ -180,21 +205,7 @@  def __init__(
         if not self._tarball_path:
             self._create_tarball()
 
-    def _get_commit_id(self) -> None:
-        result = subprocess.run(
-            ["git", "rev-parse", "--verify", self._git_ref],
-            text=True,
-            capture_output=True,
-        )
-        if result.returncode != 0:
-            raise ConfigurationError(
-                f"{self._git_ref} is neither a path to an existing DPDK "
-                "archive nor a valid git reference.\n"
-                f"Command: {result.args}\n"
-                f"Stdout: {result.stdout}\n"
-                f"Stderr: {result.stderr}"
-            )
-        self._git_ref = result.stdout.strip()
+
 
     def _create_tarball_dir(self) -> None:
         os.makedirs(self._tarball_dir, exist_ok=True)