Skip to content

Commit

Permalink
Add Rust tool version reporting (#678)
Browse files Browse the repository at this point in the history
Adds support to report Rust tool versions in the version aggregator so the versions are available in the Build Tools Report.

Also prints the version to the logger at info level similar to other versions so it is part of the build output.
  • Loading branch information
makubacki authored Oct 20, 2023
1 parent 4f2b186 commit 8a0566e
Show file tree
Hide file tree
Showing 5 changed files with 195 additions and 1 deletion.
1 change: 1 addition & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
"rmtree",
"rpartition",
"rtags",
"rustc",
"schtasks",
"sdist",
"setuplog",
Expand Down
38 changes: 37 additions & 1 deletion edk2toolext/edk2_invocable.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
from typing import Iterable, Tuple

import pkg_resources
from edk2toollib.utility_functions import GetHostInfo, import_module_by_file_name, locate_class_in_module
from edk2toollib.utility_functions import GetHostInfo, RunCmd, import_module_by_file_name, locate_class_in_module

from edk2toolext.base_abstract_invocable import BaseAbstractInvocable
from edk2toolext.environment import shell_environment, version_aggregator
Expand Down Expand Up @@ -196,6 +196,42 @@ def collect_python_pip_info(cls):
logging.info("{0} version: {1}".format(package.project_name, version))
ver_agg.ReportVersion(package.project_name, version, version_aggregator.VersionTypes.PIP)

@classmethod
def collect_rust_info(cls):
"""Class method to collect Rust tool versions.
Reports them to the global version_aggregator as well as print them to the screen.
"""
import re
from io import StringIO

def get_rust_tool_version(tool_name: str, tool_params: str = "--version"):
cmd_output = StringIO()
ret = RunCmd(tool_name, tool_params, outstream=cmd_output, logging_level=logging.DEBUG)
if ret == 0:
return cmd_output.getvalue().strip()
else:
return "N/A"

tools = {
"cargo": ("cargo",),
"cargo make": ("cargo", "make --version"),
"rustc": ("rustc",)
}

for tool_name, tool_cmd in tools.items():
ver = get_rust_tool_version(*tool_cmd)
match = re.search(r'(\d+\.\d+\.\d+)', ver)
if match:
ver = match.group(1)
elif ver != "N/A":
raise Exception("A Rust tool is installed, but its version "
"format is unexpected and cannot be parsed.")

logging.info(f"{tool_name} version: {ver}")
ver_agg = version_aggregator.GetVersionAggregator()
ver_agg.ReportVersion(tool_name, ver, version_aggregator.VersionTypes.TOOL)

def GetWorkspaceRoot(self) -> os.PathLike:
"""Returns the absolute path to the workspace root.
Expand Down
1 change: 1 addition & 0 deletions edk2toolext/invocables/edk2_ci_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ def Go(self):
log_directory = os.path.join(self.GetWorkspaceRoot(), self.GetLoggingFolderRelativeToRoot())

Edk2CiBuild.collect_python_pip_info()
Edk2CiBuild.collect_rust_info()

# make Edk2Path object to handle all path operations
try:
Expand Down
1 change: 1 addition & 0 deletions edk2toolext/invocables/edk2_platform_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ def Go(self):
logging.info("Running Python version: " + str(sys.version_info))

Edk2PlatformBuild.collect_python_pip_info()
Edk2PlatformBuild.collect_rust_info()

(build_env, shell_env) = self_describing_environment.BootstrapEnvironment(
self.GetWorkspaceRoot(), self.GetActiveScopes(), self.GetSkippedDirectories())
Expand Down
155 changes: 155 additions & 0 deletions tests.unit/test_edk2_invocable.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
# @file test_edk2_invocable.py
# This contains unit tests for the edk2_invocable
##
# Copyright (c) Microsoft Corporation.
#
# SPDX-License-Identifier: BSD-2-Clause-Patent
##
"""Unit tests for the Edk2Invocable module."""

import unittest
from unittest.mock import MagicMock, patch

import edk2toolext.environment.version_aggregator as version_aggregator
from edk2toolext.edk2_invocable import Edk2Invocable


class TestEdk2Invocable(unittest.TestCase):
"""Tests for the Edk2Invocable module."""

@classmethod
def _mock_rust_tool_run_cmd_valid(cls, tool_name: str, tool_params: str,
**kwargs: dict[str, any]):
"""Returns a set of expected Rust tool versions.
Args:
tool_name (str): The name of the tool.
tool_params (str): Parameters to pass to the tool.
kwargs (dict[str, any]): A dictionary of parameters to write.
Returns:
int: 0 for successful tool invocation. Non-zero for unsuccessful.
"""
if tool_name == 'cargo' and tool_params == '--version':
kwargs['outstream'].write("cargo 1.10.0")
return 0
elif tool_name == 'cargo' and tool_params == 'make --version':
kwargs['outstream'].write("cargo make 0.30.0 (abc1234)")
return 0
elif tool_name == 'rustc':
kwargs['outstream'].write("rustc 1.10.1")
return 0
return 1

@classmethod
def _mock_rust_tool_run_cmd_invalid(cls, tool_name: str, tool_params: str,
**kwargs: dict[str, any]):
"""Returns an unexpected tool version.
Args:
tool_name (str): The name of the tool.
tool_params (str): Parameters to pass to the tool.
kwargs (dict[str, any]): A dictionary of parameters to write.
Returns:
int: 0 for successful tool invocation.
"""
kwargs['outstream'].write("unknown version format")
return 0

@classmethod
def _mock_rust_tool_run_cmd_missing(cls, tool_name: str, tool_params: str,
**kwargs: dict[str, any]):
"""Returns an unexpected tool version.
Args:
tool_name (str): The name of the tool.
tool_params (str): Parameters to pass to the tool.
kwargs (dict[str, any]): A dictionary of parameters to write.
Returns:
int: 1 indicating an error.
"""
kwargs['outstream'].write("<rust tool> is not a recognized command.")
return 1

@patch('edk2toolext.edk2_invocable.RunCmd')
def test_collect_rust_info_unknown_ver(self, mock_run_cmd: MagicMock):
"""Verifies a Rust tool with an unknown format raises an exception.
Args:
mock_run_cmd (MagicMock): A mock RunCmd object.
Returns:
None
"""
mock_run_cmd.side_effect = self._mock_rust_tool_run_cmd_invalid

with self.assertRaises(Exception) as context:
Edk2Invocable.collect_rust_info()

self.assertTrue("version format is unexpected and cannot be parsed"
in str(context.exception))

@patch('edk2toolext.edk2_invocable.RunCmd')
@patch('edk2toolext.edk2_invocable.version_aggregator.GetVersionAggregator')
def test_collect_rust_info_missing_tool(self,
mock_get_version_aggregator: MagicMock,
mock_run_cmd: MagicMock):
"""Verifies a missing Rust tool returns N/A.
Some repos may not use the Rust and the users of those repos do not
need Rust tools installed. In that case, N/A is returned to show that
the tools were not recognized. The tools could not be reported at all
but N/A is meant to make the report consistent for comparison purposes.
Args:
mock_get_version_aggregator (MagicMock): A mock version_aggregator
object.
mock_run_cmd (MagicMock): A mock RunCmd object.
Returns:
None
"""
mock_version_aggregator = MagicMock()
mock_get_version_aggregator.return_value = mock_version_aggregator
mock_run_cmd.side_effect = self._mock_rust_tool_run_cmd_missing

Edk2Invocable.collect_rust_info()

calls = [(("cargo", "N/A", version_aggregator.VersionTypes.TOOL),)]

mock_version_aggregator.ReportVersion.assert_has_calls(calls, any_order=True)

@patch('edk2toolext.edk2_invocable.RunCmd')
@patch('edk2toolext.edk2_invocable.version_aggregator.GetVersionAggregator')
def test_collect_rust_info_known_ver(self,
mock_get_version_aggregator: MagicMock,
mock_run_cmd: MagicMock):
"""Verifies Rust tools with an expected format are successful.
Verifies the tool information is passed to the version aggregator as
expected.
Args:
mock_get_version_aggregator (MagicMock): A mock version_aggregator
object.
mock_run_cmd (MagicMock): A mock RunCmd object.
Returns:
None
"""
mock_version_aggregator = MagicMock()
mock_get_version_aggregator.return_value = mock_version_aggregator
mock_run_cmd.side_effect = self._mock_rust_tool_run_cmd_valid

Edk2Invocable.collect_rust_info()

calls = [
(("cargo", "1.10.0", version_aggregator.VersionTypes.TOOL),),
(("cargo make", "0.30.0", version_aggregator.VersionTypes.TOOL),),
(("rustc", "1.10.1", version_aggregator.VersionTypes.TOOL),)
]

mock_version_aggregator.ReportVersion.assert_has_calls(calls, any_order=True)

0 comments on commit 8a0566e

Please sign in to comment.