On Wed, Jun 24, 2026 at 10:34 PM Patrick Robb <[email protected]>
wrote:

>
>
> On Mon, Jun 22, 2026 at 1:02 PM Koushik Bhargav Nimoji <
> [email protected]> wrote:
>
>> Previously, DTS had no code coverage. This patch adds a command line
>> argument in order to build DPDK with code coverage enabled. This allows
>> users to create and view code coverage reports of what code and functions
>> were called during a DTS run.
>>
>> Signed-off-by: Koushik Bhargav Nimoji <[email protected]>
>> ---
>> v2:
>>     *Fixed error in lcov/gcov tool detection
>> v3:
>>     *Fixed type hints and error message typos
>> ---
>>  .mailmap                                      |  1 +
>>  doc/guides/tools/dts.rst                      | 15 +++++++++++++
>>  dts/README.md                                 |  5 +++++
>>  dts/framework/remote_session/dpdk.py          | 19 ++++++++++++++++
>>  .../remote_session/remote_session.py          |  5 ++++-
>>  dts/framework/settings.py                     | 10 +++++++++
>>  dts/framework/testbed_model/os_session.py     | 10 +++++++++
>>  dts/framework/testbed_model/posix_session.py  | 22 +++++++++++++++++++
>>  dts/framework/utils.py                        |  8 +++++++
>>  9 files changed, 94 insertions(+), 1 deletion(-)
>>
>> diff --git a/.mailmap b/.mailmap
>> index e052b85213..a1209150ad 100644
>> --- a/.mailmap
>> +++ b/.mailmap
>> @@ -877,6 +877,7 @@ Klaus Degner <[email protected]>
>>  Kommula Shiva Shankar <[email protected]>
>>  Konstantin Ananyev <[email protected]> <
>> [email protected]>
>>  Konstantin Ananyev <[email protected]> <
>> [email protected]>
>> +Koushik Bhargav Nimoji <[email protected]>
>>  Krishna Murthy <[email protected]>
>>  Krzysztof Galazka <[email protected]>
>>  Krzysztof Kanas <[email protected]> <[email protected]
>> >
>> diff --git a/doc/guides/tools/dts.rst b/doc/guides/tools/dts.rst
>> index 5b9a348016..a838a317ee 100644
>> --- a/doc/guides/tools/dts.rst
>> +++ b/doc/guides/tools/dts.rst
>> @@ -352,6 +352,10 @@ DTS is run with ``main.py`` located in the ``dts``
>> directory using the ``poetry
>>       --precompiled-build-dir DIR_NAME
>>                             [DTS_PRECOMPILED_BUILD_DIR] Define the
>> subdirectory under the DPDK tree root directory or tarball where the pre-
>>                             compiled binaries are located. (default: None)
>> +     --code-coverage       Builds DPDK on the SUT node with code
>> coverage enabled. Generates a code coverage report which can be found on
>> +                           the local filesystem at
>> dts/output/coverage_reports/meson-logs/coveragereport/index.html, or the
>> specified output
>>
>
> at the DTS execution host's local filesystem
>
> I realize you are presumably concating what gcov/lcov gives you but can
> the dts/output/coverage_reports/meson-logs/coveragereport/index.html,
> path be shortened? Seems like 2-3 of those middle dir names can be dropped
> hah. Not a big deal if left as is for any reason.
>

I looked into shortening the path initially, but based on the structure of
the report and its components I believe it would be most simple to keep it
as is.


>
>> +                           directory. To use code coverage, please
>> ensure lcov v1.15 and gcov v8.0 or higher (included in gcc package) are
>> +                           installed on the SUT node.
>>
>>
>>  The brackets contain the names of environment variables that set the
>> same thing.
>> @@ -367,6 +371,17 @@ Results are stored in the output dir by default
>>  which be changed with the ``--output-dir`` command line argument.
>>  The results contain basic statistics of passed/failed test cases and
>> DPDK version.
>>
>> +Code Coverage
>> +~~~~~~~~~~~~~
>> +
>> +DTS has the ablilty to track code usage during test runs, and generate
>> an HTML
>>
>
> I'm sure it's obvious to most readers what coverage we are talking about
> here, but why not just explicitly say DTS can generate coverage reports
> which show the code coverage % for DPDK libraries and drivers touched
> during the testsuite(s) execution? It never hurts to be extra clear. :)
>
> VERY IMPORTANT: You need to explain what the code coverage behavior is. Is
> it a code coverage report per testrun? or per testsuite?
>
> If it is per testrun, what happens if we use a prebuilt DPDK dir? Then do
> coverage stats bleed over between runs because the build dir is preserved?
> (happy to talk about this tomorrow if I'm not phrasing it clearly).
>
> +coverage report with that data. This can be done by using the
>> "--code-coverage"
>> +CLI parameter when running DTS.
>> +
>> +To use code coverage, please make sure the following dependencies are
>> available
>> +on the SUT node:
>> +- lcov v1.15
>>
>
> code says 1.15 or greater
>
>
>> +- gcov v8.0 or greater (included in gcc package)
>>
>>  Contributing to DTS
>>  -------------------
>> diff --git a/dts/README.md b/dts/README.md
>> index d257b7a167..51f824e077 100644
>> --- a/dts/README.md
>> +++ b/dts/README.md
>> @@ -64,6 +64,11 @@ $ poetry run ./main.py
>>  These commands will give you a bash shell inside a docker container
>>  with all DTS Python dependencies installed.
>>
>> +# Code Coverage
>> +
>> +To generate code coverage reports, ensure the SUT has lcov v1.15 and
>> gcov v8.0 or greater
>> +installed, and that DTS is run using the '--code-coverage' argument.
>>
>
> Not that I'm opposed, but it is interesting to me that we are exposing
> this toggle as a flag but not as a test_run.yaml option. I was about to
> suggest adding a test_run.yaml boolean field for it but... maybe we need to
> relax on the amount of fields we put in there. It might be better for some
> of the more "infrequently used" options to be flag only, for
> readability reasons. Happy to defer to your judgement here.
>

I agree, as code coverage is more so an "add-on" to a DTS run. The
components of the test_run.yaml are more so required for the test run, so
it would be better to keep it as a flag.


> +
>>  ## Visual Studio Code
>>
>>  Usage of VScode devcontainers is NOT required for developing on DTS and
>> running DTS,
>> diff --git a/dts/framework/remote_session/dpdk.py
>> b/dts/framework/remote_session/dpdk.py
>> index c3575cfcaf..865f97f6ca 100644
>> --- a/dts/framework/remote_session/dpdk.py
>> +++ b/dts/framework/remote_session/dpdk.py
>> @@ -29,6 +29,7 @@
>>  from framework.logger import DTSLogger, get_dts_logger
>>  from framework.params.eal import EalParams
>>  from framework.remote_session.remote_session import CommandResult
>> +from framework.settings import SETTINGS
>>  from framework.testbed_model.cpu import LogicalCore, LogicalCoreCount,
>> LogicalCoreList, lcore_filter
>>  from framework.testbed_model.node import Node
>>  from framework.testbed_model.os_session import OSSession
>> @@ -107,7 +108,22 @@ def teardown(self) -> None:
>>          """Teardown the DPDK build on the target node.
>>
>>          Removes the DPDK tree and/or build directory/tarball depending
>> on the configuration.
>> +        If code coverage is enabled, the coverage report and .info file
>> are generated and
>> +        copied onto the local filesystem before teardown.
>>          """
>> +        if SETTINGS.code_coverage:
>> +            report_folder = PurePath(self.remote_dpdk_build_dir /
>> "meson-logs")
>> +            output_dir = SETTINGS.output_dir
>> +            Path(output_dir).mkdir(parents=True, exist_ok=True)
>> +
>> +            coverage_status =
>> self._session.generate_coverage_report(self.remote_dpdk_build_dir)
>> +            if coverage_status:
>> +                self._session.copy_dir_from(report_folder, output_dir)
>> +                self._logger.info(
>> +                    "Coverage HTML report generated, "
>> +                    f"available at
>> {output_dir}/meson-logs/coveragereports/index.html"
>> +                )
>> +
>>          match self.config.dpdk_location:
>>              case LocalDPDKTreeLocation():
>>
>>  self._node.main_session.remove_remote_dir(self.remote_dpdk_tree_path)
>> @@ -272,6 +288,9 @@ def _build_dpdk(self) -> None:
>>          else:
>>              meson_args = MesonArgs(default_library="static",
>> libdir="lib")
>>
>> +        if SETTINGS.code_coverage:
>> +            meson_args._add_arg("-Db_coverage=true")
>> +
>>          self._session.build_dpdk(
>>              self._env_vars,
>>              meson_args,
>> diff --git a/dts/framework/remote_session/remote_session.py
>> b/dts/framework/remote_session/remote_session.py
>> index 158325bb7f..d2440dc2d8 100644
>> --- a/dts/framework/remote_session/remote_session.py
>> +++ b/dts/framework/remote_session/remote_session.py
>> @@ -252,7 +252,10 @@ def copy_from(self, source_file: str | PurePath,
>> destination_dir: str | Path) ->
>>              destination_dir: The directory path on the local filesystem
>> where the `source_file`
>>                  will be saved.
>>          """
>> -        self.session.get(str(source_file), str(destination_dir))
>> +        source_file = PurePath(source_file)
>> +        destination_dir = Path(destination_dir)
>> +        local_path = destination_dir / source_file.name
>> +        self.session.get(str(source_file), str(local_path))
>>
>>      def copy_to(self, source_file: str | Path, destination_dir: str |
>> PurePath) -> None:
>>          """Copy a file from local filesystem to the remote Node.
>> diff --git a/dts/framework/settings.py b/dts/framework/settings.py
>> index b08373b7ea..7df535bd84 100644
>> --- a/dts/framework/settings.py
>> +++ b/dts/framework/settings.py
>> @@ -159,6 +159,8 @@ class Settings:
>>      re_run: int = 0
>>      #:
>>      random_seed: int | None = None
>> +    #:
>> +    code_coverage: bool = False
>>
>>
>>  SETTINGS: Settings = Settings()
>> @@ -489,6 +491,14 @@ def _get_parser() -> _DTSArgumentParser:
>>      )
>>      _add_env_var_to_action(action)
>>
>> +    action = parser.add_argument(
>> +        "--code-coverage",
>> +        action="store_true",
>> +        default=False,
>> +        help="Used to build DPDK with code coverage enabled.",
>> +    )
>> +    _add_env_var_to_action(action)
>> +
>>      return parser
>>
>>
>> diff --git a/dts/framework/testbed_model/os_session.py
>> b/dts/framework/testbed_model/os_session.py
>> index 2c267afed1..742b074948 100644
>> --- a/dts/framework/testbed_model/os_session.py
>> +++ b/dts/framework/testbed_model/os_session.py
>> @@ -480,6 +480,16 @@ def build_dpdk(
>>              timeout: Wait at most this long in seconds for the build
>> execution to complete.
>>          """
>>
>> +    @abstractmethod
>> +    def generate_coverage_report(self, remote_build_dir: PurePath |
>> None) -> bool:
>> +        """Generates a code coverage report for a DTS run.
>> +
>> +        Args:
>> +            remote_build_dir: The remote DPDK build directory
>> +        Returns:
>> +            Whether the coverage report was able to be created or not.
>> +        """
>> +
>>      @abstractmethod
>>      def get_dpdk_version(self, version_path: str | PurePath) -> str:
>>          """Inspect the DPDK version on the remote node.
>> diff --git a/dts/framework/testbed_model/posix_session.py
>> b/dts/framework/testbed_model/posix_session.py
>> index dec952685a..d18ce27de2 100644
>> --- a/dts/framework/testbed_model/posix_session.py
>> +++ b/dts/framework/testbed_model/posix_session.py
>> @@ -295,6 +295,28 @@ def build_dpdk(
>>          except RemoteCommandExecutionError as e:
>>              raise DPDKBuildError(f"DPDK build failed when doing
>> '{e.command}'.")
>>
>> +    def generate_coverage_report(self, remote_build_dir: PurePath |
>> None) -> bool:
>> +        """Overrides
>> :meth:`~.os_session.OSSession.generate_coverage_report`."""
>> +        command_result = self.send_command(r"lcov --version | grep -oP
>> '\d+\.\d+'")
>> +        lcov_version = float(
>> +            command_result.stdout if command_result.return_code == 0 and
>> command_result else -1
>> +        )
>> +        command_result = self.send_command(
>> +            r"gcov --version | head -n 1 | grep -oP '\d+\.\d+' | tail -n
>> 1"
>> +        )
>> +        gcov_version = float(
>> +            command_result.stdout if command_result.return_code == 0 and
>> command_result else -1
>> +        )
>> +
>> +        if lcov_version >= 1.15 and gcov_version >= 8.0:
>> +            self.send_command(f"ninja -C {remote_build_dir}
>> coverage-html", timeout=600)
>> +            return True
>> +        else:
>> +            self._logger.info(
>> +                "Unable to generate code coverage report, ensure lcov
>> v1.15 and at least gcov v8.0"
>> +            )
>> +            return False
>> +
>>      def get_dpdk_version(self, build_dir: str | PurePath) -> str:
>>          """Overrides :meth:`~.os_session.OSSession.get_dpdk_version`."""
>>          out = self.send_command(f"cat {self.join_remote_path(build_dir,
>> 'VERSION')}", verify=True)
>> diff --git a/dts/framework/utils.py b/dts/framework/utils.py
>> index 9917ffbfaa..38da88cd9c 100644
>> --- a/dts/framework/utils.py
>> +++ b/dts/framework/utils.py
>> @@ -125,6 +125,14 @@ def __str__(self) -> str:
>>          """The actual args."""
>>          return " ".join(f"{self._default_library}
>> {self._dpdk_args}".split())
>>
>> +    def _add_arg(self, arg: str):
>> +        """Used to add a meson build argument to the DPDK build.
>>
>
> Nit but rephrase to "Adds an argument to the Meson setup command"
>
>
>> +
>> +        Args:
>> +            arg: The meson build argument to be added.
>> +        """
>> +        self._dpdk_args = self._dpdk_args + " " + arg
>> +
>>
>>  class TarCompressionFormat(StrEnum):
>>      """Compression formats that tar can use.
>> --
>> 2.54.0
>>
>>
> Reviewed-by: Patrick Robb <[email protected]>
>

Reply via email to