Skip to content

Commit

Permalink
.pytool/Plugin: Improve Rust support (#578)
Browse files Browse the repository at this point in the history
## Description

Makes multiple improvements to the RustHostUnitTestPlugin to support a
more reliable experience:

1. The plugin now fails when a test fails to compile or cargo tarpaulin
fails to run.
2. The plugin now logs the reason for failing as a warning in addition
to logging the output to the xml file.
3. The plugin now properly filters results to only include results of
rust crates inside the package being tested.

Closes #575 

- [ ] Impacts functionality?
- **Functionality** - Does the change ultimately impact how firmware
functions?
- Examples: Add a new library, publish a new PPI, update an algorithm,
...
- [ ] Impacts security?
- **Security** - Does the change have a direct security impact on an
application,
    flow, or firmware?
  - Examples: Crypto algorithm change, buffer overflow fix, parameter
    validation improvement, ...
- [ ] Breaking change?
- **Breaking change** - Will anyone consuming this change experience a
break
    in build or boot behavior?
- Examples: Add a new library class, move a module to a different repo,
call
    a function in a new library class in a pre-existing module, ...
- [ ] Includes tests?
  - **Tests** - Does the change include any explicit test code?
  - Examples: Unit tests, integration tests, robot tests, ...
- [ ] Includes documentation?
- **Documentation** - Does the change contain explicit documentation
additions
    outside direct code modifications (and comments)?
- Examples: Update readme file, add feature readme file, link to
documentation
    on an a separate Web page, ...

## How This Was Tested

1. Verified only rust crates in the package being tested, are tested
2. Verified coverage results from rust crates being tested only show
coverage values of rust crates in that specific EDKII package (If a
crate has a dependency on another crate in a different EDKII package,
the coverage results for the other crate would show up.
3. Verified we properly log (and fail the plugin) when a test fails to
compile
4. Verified we properly log (and fail the plugin) when the command as a
whole fails for some reason
5. Verified we log (in addition to the xml log) the particular test that
is failing the plugin

## Integration Instructions

N/A
  • Loading branch information
Javagedes authored and kenlautner committed Dec 18, 2023
1 parent a74b7b4 commit 0bbd155
Show file tree
Hide file tree
Showing 2 changed files with 31 additions and 12 deletions.
31 changes: 23 additions & 8 deletions .pytool/Plugin/RustHostUnitTestPlugin/RustHostUnitTestPlugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,30 +26,39 @@ def RunBuildPlugin(self, packagename, Edk2pathObj, pkgconfig, environment, PLM,

# Build list of packages that are in the EDK2 package we are running CI on
pp = Path(Edk2pathObj.GetAbsolutePathOnThisSystemFromEdk2RelativePath(packagename))
package_name_list = [pkg.name for pkg in filter(lambda pkg: Path(pkg.path).is_relative_to(pp), rust_ws.members)]
package_path_list = [pkg.path for pkg in filter(lambda pkg: Path(pkg.path).is_relative_to(pp), rust_ws.members)]
logging.debug(f"Rust Packages to test: {' '.join(package_name_list)}")
crate_name_list = [pkg.name for pkg in filter(lambda pkg: Path(pkg.path).is_relative_to(pp), rust_ws.members)]
crate_path_list = [pkg.path for pkg in filter(lambda pkg: Path(pkg.path).is_relative_to(pp), rust_ws.members)]
logging.debug(f"Rust Crates to test: {' '.join(crate_name_list)}")

# Build a list of paths to ignore when computing results. This includes:
# 1. Any tests folder in a rust package
# 2. Everything in a submodule
# 3. Everything in an EDK2 package not being tested.
ignore_list = [Path("**", "tests", "*")]
ignore_list.extend([Path(s, "**", "*") for s in repo_details(ws)["Submodules"]])
ignore_list.extend(list(set([pkg.path for pkg in rust_ws.members]) - set(package_path_list)))
ignored_local_crates = list(set(crate.path for crate in rust_ws.members) - set(crate_path_list))
ignore_list.extend([Path(crate, "**", "*").relative_to(ws) for crate in ignored_local_crates])
ignore_list = [str(i) for i in ignore_list]
logging.debug(f"Paths to ignore when computing coverage: {' '.join(ignore_list)}")

# Run tests and evaluate results
results = rust_ws.coverage(package_name_list, ignore_list = ignore_list, report_type = "xml")
try:
results = rust_ws.coverage(crate_name_list, ignore_list = ignore_list, report_type = "xml")
except RuntimeError as e:
logging.warning(str(e))
tc.LogStdError(str(e))
tc.SetFailed(str(e), "CHECK_FAILED")
return 1

# Evaluate unit test results
failed = 0
for test in results["pass"]:
tc.LogStdOut(f'{test} ... PASS')

for test in results["fail"]:
tc.LogStdError(f'{test} ... FAIL')
e = f'{test} ... FAIL'
logging.warning(e)
tc.LogStdError(e)
failed += 1

# If we failed a unit test, we have no coverage data to evaluate
Expand All @@ -61,7 +70,11 @@ def RunBuildPlugin(self, packagename, Edk2pathObj, pkgconfig, environment, PLM,
coverage = {}
for file, cov in results["coverage"].items():
try:
package = next(pkg.name for pkg in rust_ws.members if Path(ws,file).is_relative_to(pkg.path))
package = next(
crate.name
for crate in rust_ws.members
if Path(ws, file).is_relative_to(crate.path)
)
except StopIteration:
continue
covered, total = cov.split("/")
Expand All @@ -80,7 +93,9 @@ def RunBuildPlugin(self, packagename, Edk2pathObj, pkgconfig, environment, PLM,
if calc_cov >= required_cov:
tc.LogStdOut(f'coverage::{pkg}: {calc_cov} greater than {required_cov} ... PASS')
else:
tc.LogStdError(f'coverage::{pkg}: {calc_cov} less than {required_cov} ... FAIL')
e = f'coverage::{pkg}: {calc_cov}% less than {required_cov}% ... FAIL'
logging.warning(e)
tc.LogStdError(e)
failed += 1

# Move coverage.xml to Build Directory
Expand Down
12 changes: 8 additions & 4 deletions .pytool/Plugin/RustPackageHelper/RustPackageHelper.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,14 @@ def coverage(self, pkg_list = None, ignore_list = None, report_type: str = "html
command = "cargo"
params = "make"
if ignore_list:
params += f' -e COV_FLAGS="--out {report_type} --exclude-files {",".join(ignore_list)}"'
params += f' -e COV_FLAGS="--out {report_type} --exclude-files {" --exclude-files ".join(ignore_list)}"'
else:
params += f' -e COV_FLAGS="--out {report_type}"'
params += f" coverage {','.join(pkg_list)}"

# Run the command
output = io.StringIO()
RunCmd(command, params, workingdir=self.path, outstream=output)
return_value = RunCmd(command, params, workingdir=self.path, outstream=output)
output.seek(0)
lines = output.readlines()

Expand All @@ -88,7 +88,7 @@ def coverage(self, pkg_list = None, ignore_list = None, report_type: str = "html
# Determine passed and failed tests
for line in lines:
line = line.strip().strip("\n")

if line.startswith("test result:"):
continue

Expand All @@ -99,7 +99,11 @@ def coverage(self, pkg_list = None, ignore_list = None, report_type: str = "html
else:
result["fail"].append(line.replace(" ... FAILED", ""))
continue


# Command failed, but we didn't parse any failed tests
if return_value != 0 and len(result["fail"]) == 0:
raise RuntimeError("Failed to compile tests or run command.")

if len(result["fail"]) > 0:
return result

Expand Down

0 comments on commit 0bbd155

Please sign in to comment.