Skip to content

Commit

Permalink
ptest-cargo.bbclass: create class
Browse files Browse the repository at this point in the history
This new class offers the possibility to build rust unit tests
(and integration tests) and find them correctly.
Due to non deterministic names of generated binaries, a custom
parsing of build result must be performed.
See rust-lang/cargo#1924

All rust projects will generate a test binary with "cargo build --tests"
command, even if there are no test defined in source code.
The binary will just output that it ran 0 tests.

Signed-off-by: Frederic Martinsons <frederic.martinsons@gmail.com>
Signed-off-by: Alexandre Belloni <alexandre.belloni@bootlin.com>
Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
  • Loading branch information
fmartinsons authored and rpurdie committed May 5, 2023
1 parent d5ce88c commit dad9bad
Showing 1 changed file with 130 additions and 0 deletions.
130 changes: 130 additions & 0 deletions meta/classes-recipe/ptest-cargo.bbclass
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
inherit cargo ptest

# I didn't find a cleaner way to share data between compile and install tasks
CARGO_TEST_BINARIES_FILES ?= "${B}/test_binaries_list"

# Sadly, generated test binaries have no deterministic names (https://github.com/rust-lang/cargo/issues/1924)
# This forces us to parse the cargo output in json format to find those test binaries.
python do_compile_ptest_cargo() {
import subprocess
import json

cargo = bb.utils.which(d.getVar("PATH"), d.getVar("CARGO", True))
cargo_build_flags = d.getVar("CARGO_BUILD_FLAGS", True)
rust_flags = d.getVar("RUSTFLAGS", True)
manifest_path = d.getVar("MANIFEST_PATH", True)

env = os.environ.copy()
env['RUSTFLAGS'] = rust_flags
cmd = f"{cargo} build --tests --message-format json {cargo_build_flags}"
bb.note(f"Building tests with cargo ({cmd})")

try:
proc = subprocess.Popen(cmd, shell=True, env=env, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
bb.fatal(f"Cannot build test with cargo: {e}")

lines = []
for line in proc.stdout:
data = line.decode('utf-8').strip('\n')
lines.append(data)
bb.note(data)
proc.communicate()
if proc.returncode != 0:
bb.fatal(f"Unable to compile test with cargo, '{cmd}' failed")

# Definition of the format: https://doc.rust-lang.org/cargo/reference/external-tools.html#json-messages
test_bins = []
for line in lines:
try:
data = json.loads(line)
except json.JSONDecodeError:
# skip lines that are not a json
pass
else:
try:
# Filter the test packages coming from the current manifest
current_manifest_path = os.path.normpath(data['manifest_path'])
project_manifest_path = os.path.normpath(manifest_path)
if current_manifest_path == project_manifest_path:
if data['target']['test'] or data['target']['doctest'] and data['executable']:
test_bins.append(data['executable'])
except KeyError as e:
# skip lines that do not meet the requirements
pass

# All rust project will generate at least one unit test binary
# It will just run a test suite with 0 tests, if the project didn't define some
# So it is not expected to have an empty list here
if not test_bins:
bb.fatal("Unable to find any test binaries")

cargo_test_binaries_file = d.getVar('CARGO_TEST_BINARIES_FILES', True)
bb.note(f"Found {len(test_bins)} tests, write their paths into {cargo_test_binaries_file}")
with open(cargo_test_binaries_file, "w") as f:
for test_bin in test_bins:
f.write(f"{test_bin}\n")

}

python do_install_ptest_cargo() {
import shutil

dest_dir = d.getVar("D", True)
pn = d.getVar("PN", True)
ptest_path = d.getVar("PTEST_PATH", True)
cargo_test_binaries_file = d.getVar('CARGO_TEST_BINARIES_FILES', True)

ptest_dir = os.path.join(dest_dir, ptest_path.lstrip('/'))
os.makedirs(ptest_dir, exist_ok=True)

test_bins = []
with open(cargo_test_binaries_file, "r") as f:
for line in f.readlines():
test_bins.append(line.strip('\n'))

test_paths = []
for test_bin in test_bins:
shutil.copy2(test_bin, ptest_dir)
test_paths.append(os.path.join(ptest_path, os.path.basename(test_bin)))

ptest_script = os.path.join(ptest_dir, "run-ptest")
if os.path.exists(ptest_script):
with open(ptest_script, "a") as f:
f.write(f"\necho \"\"\n")
f.write(f"echo \"## starting to run rust tests ##\"\n")
for test_path in test_paths:
f.write(f"{test_path}\n")
else:
with open(ptest_script, "a") as f:
f.write("#!/bin/sh\n")
for test_path in test_paths:
f.write(f"{test_path}\n")
os.chmod(ptest_script, 0o755)

# this is chown -R root:root ${D}${PTEST_PATH}
for root, dirs, files in os.walk(ptest_dir):
for d in dirs:
shutil.chown(os.path.join(root, d), "root", "root")
for f in files:
shutil.chown(os.path.join(root, f), "root", "root")
}

do_install_ptest_cargo[dirs] = "${B}"
do_install_ptest_cargo[doc] = "Create or update the run-ptest script with rust test binaries generated"
do_compile_ptest_cargo[dirs] = "${B}"
do_compile_ptest_cargo[doc] = "Generate rust test binaries through cargo"

addtask compile_ptest_cargo after do_compile before do_compile_ptest_base
addtask install_ptest_cargo after do_install_ptest_base before do_package

python () {
if not bb.data.inherits_class('native', d) and not bb.data.inherits_class('cross', d):
d.setVarFlag('do_install_ptest_cargo', 'fakeroot', '1')
d.setVarFlag('do_install_ptest_cargo', 'umask', '022')

# Remove all '*ptest_cargo' tasks when ptest is not enabled
if not(d.getVar('PTEST_ENABLED') == "1"):
for i in ['do_compile_ptest_cargo', 'do_install_ptest_cargo']:
bb.build.deltask(i, d)
}

0 comments on commit dad9bad

Please sign in to comment.