buildRustCrate: add buildTests flag to tell rustc to build tests instead of binaries

This helps us instruct rustc to build tests instead of binaries. The
actual build will then ONLY produce test executables. This is a first
step towards having rust crate tests within nixpkgs.

We default back to only a single output in test cases since that is the
only reasonable thing to do here.

Producing libraries or binaries in addition to tests would theoretically
be feasible but usually generates different dependency trees. It is very
common to have some libraries in `[dev-depdendencies]` within Cargo.toml
just for your tests. To not start mixing things up going with a
dedicated derivation for the test build sounds like the best choice for
now.

To use this you must provide a proper test dependency chain to
`buildRustCrate` (as you would usually do with your non-test inputs).
And then set the `buildTests` attribute to `true`. The derivation will
then contain all tests that were built in `$out/tests`. All common test
patterns and directories should be supported and tested by this change.

Below is an example how you would run a single test from the derivation.
This commit contains some more examples in the `buildRustCrateTests`
attribute set that might be helpful.

```
let
  drv = buildRustCrate {
     …
     buildTests true;
  };
in runCommand "test-my-crate" {} ''
  touch $out
  exec ${drv}/tests/my-test
''
```
This commit is contained in:
Andreas Rammhold 2019-12-17 21:15:53 +01:00
parent 2c4c7c929c
commit a3a51763f9
No known key found for this signature in database
GPG Key ID: E432E410B5E48C86
5 changed files with 196 additions and 27 deletions

View File

@ -4,6 +4,7 @@
crateFeatures, crateRenames, libName, release, libPath, crateFeatures, crateRenames, libName, release, libPath,
crateType, metadata, crateBin, hasCrateBin, crateType, metadata, crateBin, hasCrateBin,
extraRustcOpts, verbose, colors, extraRustcOpts, verbose, colors,
buildTests
}: }:
let let
@ -30,6 +31,7 @@
baseRustcOpts baseRustcOpts
); );
build_bin = if buildTests then "build_bin_test" else "build_bin";
in '' in ''
runHook preBuild runHook preBuild
${echo_build_heading colors} ${echo_build_heading colors}
@ -48,14 +50,18 @@
setup_link_paths setup_link_paths
if [[ -e "$LIB_PATH" ]]; then if [[ -e "$LIB_PATH" ]]; then
build_lib $LIB_PATH build_lib "$LIB_PATH"
${lib.optionalString buildTests ''build_lib_test "$LIB_PATH"''}
elif [[ -e src/lib.rs ]]; then elif [[ -e src/lib.rs ]]; then
build_lib src/lib.rs build_lib src/lib.rs
${lib.optionalString buildTests "build_lib_test src/lib.rs"}
elif [[ -e "src/$LIB_NAME.rs" ]]; then elif [[ -e "src/$LIB_NAME.rs" ]]; then
build_lib src/$LIB_NAME.rs build_lib src/$LIB_NAME.rs
${lib.optionalString buildTests ''build_lib_test "src/$LIB_NAME.rs"''}
fi fi
${lib.optionalString (lib.length crateBin > 0) (lib.concatMapStringsSep "\n" (bin: '' ${lib.optionalString (lib.length crateBin > 0) (lib.concatMapStringsSep "\n" (bin: ''
mkdir -p target/bin mkdir -p target/bin
BIN_NAME='${bin.name or crateName}' BIN_NAME='${bin.name or crateName}'
@ -65,19 +71,39 @@
'' else '' '' else ''
BIN_PATH='${bin.path}' BIN_PATH='${bin.path}'
''} ''}
build_bin "$BIN_NAME" "$BIN_PATH" ${build_bin} "$BIN_NAME" "$BIN_PATH"
'') crateBin)} '') crateBin)}
${lib.optionalString buildTests ''
# When tests are enabled build all the files in the `tests` directory as
# test binaries.
if [ -d tests ]; then
# find all the .rs files (or symlinks to those) in the tests directory, no subdirectories
find tests -maxdepth 1 \( -type f -o -type l \) -a -name '*.rs' -print0 | while IFS= read -r -d ''' file; do
mkdir -p target/bin
build_bin_test_file "$file"
done
# find all the subdirectories of tests/ that contain a main.rs file as
# that is also a test according to cargo
find tests/ -mindepth 1 -maxdepth 2 \( -type f -o -type l \) -a -name 'main.rs' -print0 | while IFS= read -r -d ''' file; do
mkdir -p target/bin
build_bin_test_file "$file"
done
fi
''}
# If crateBin is empty and hasCrateBin is not set then we must try to # If crateBin is empty and hasCrateBin is not set then we must try to
# detect some kind of bin target based on some files that might exist. # detect some kind of bin target based on some files that might exist.
${lib.optionalString (lib.length crateBin == 0 && !hasCrateBin) '' ${lib.optionalString (lib.length crateBin == 0 && !hasCrateBin) ''
if [[ -e src/main.rs ]]; then if [[ -e src/main.rs ]]; then
mkdir -p target/bin mkdir -p target/bin
build_bin ${crateName} src/main.rs ${build_bin} ${crateName} src/main.rs
fi fi
for i in src/bin/*.rs; do #*/ for i in src/bin/*.rs; do #*/
mkdir -p target/bin mkdir -p target/bin
build_bin "$(basename $i .rs)" "$i" ${build_bin} "$(basename $i .rs)" "$i"
done done
''} ''}
# Remove object files to avoid "wrong ELF type" # Remove object files to avoid "wrong ELF type"

View File

@ -39,12 +39,12 @@ let
inherit lib stdenv echo_build_heading noisily mkRustcDepArgs rust; inherit lib stdenv echo_build_heading noisily mkRustcDepArgs rust;
}; };
installCrate = import ./install-crate.nix; installCrate = import ./install-crate.nix { inherit stdenv; };
in in
crate_: lib.makeOverridable ({ rust, release, verbose, features, buildInputs, crateOverrides, crate_: lib.makeOverridable ({ rust, release, verbose, features, buildInputs, crateOverrides,
dependencies, buildDependencies, crateRenames, dependencies, buildDependencies, crateRenames,
extraRustcOpts, extraRustcOpts, buildTests,
preUnpack, postUnpack, prePatch, patches, postPatch, preUnpack, postUnpack, prePatch, patches, postPatch,
preConfigure, postConfigure, preBuild, postBuild, preInstall, postInstall }: preConfigure, postConfigure, preBuild, postBuild, preInstall, postInstall }:
@ -59,6 +59,7 @@ let crate = crate_ // (lib.attrByPath [ crate_.crateName ] (attr: {}) crateOverr
extraDerivationAttrs = lib.filterAttrs (n: v: ! lib.elem n processedAttrs) crate; extraDerivationAttrs = lib.filterAttrs (n: v: ! lib.elem n processedAttrs) crate;
buildInputs_ = buildInputs; buildInputs_ = buildInputs;
extraRustcOpts_ = extraRustcOpts; extraRustcOpts_ = extraRustcOpts;
buildTests_ = buildTests;
# take a list of crates that we depend on and override them to fit our overrides, rustc, release, … # take a list of crates that we depend on and override them to fit our overrides, rustc, release, …
makeDependencies = map (dep: lib.getLib (dep.override { inherit release verbose crateOverrides; })); makeDependencies = map (dep: lib.getLib (dep.override { inherit release verbose crateOverrides; }));
@ -78,7 +79,7 @@ stdenv.mkDerivation (rec {
crate.src crate.src
else else
fetchCrate { inherit (crate) crateName version sha256; }; fetchCrate { inherit (crate) crateName version sha256; };
name = "rust_${crate.crateName}-${crate.version}"; name = "rust_${crate.crateName}-${crate.version}${lib.optionalString buildTests "-test"}";
depsBuildBuild = [ rust stdenv.cc ]; depsBuildBuild = [ rust stdenv.cc ];
buildInputs = (crate.buildInputs or []) ++ buildInputs_; buildInputs = (crate.buildInputs or []) ++ buildInputs_;
dependencies = makeDependencies dependencies_; dependencies = makeDependencies dependencies_;
@ -122,6 +123,8 @@ stdenv.mkDerivation (rec {
++ extraRustcOpts_ ++ extraRustcOpts_
++ (lib.optional (edition != null) "--edition ${edition}"); ++ (lib.optional (edition != null) "--edition ${edition}");
buildTests = buildTests_;
configurePhase = configureCrate { configurePhase = configureCrate {
inherit crateName buildDependencies completeDeps completeBuildDeps crateDescription inherit crateName buildDependencies completeDeps completeBuildDeps crateDescription
crateFeatures crateRenames libName build workspace_member release libPath crateVersion crateFeatures crateRenames libName build workspace_member release libPath crateVersion
@ -132,12 +135,14 @@ stdenv.mkDerivation (rec {
inherit crateName dependencies inherit crateName dependencies
crateFeatures crateRenames libName release libPath crateType crateFeatures crateRenames libName release libPath crateType
metadata hasCrateBin crateBin verbose colors metadata hasCrateBin crateBin verbose colors
extraRustcOpts; extraRustcOpts buildTests;
}; };
installPhase = installCrate crateName metadata; installPhase = installCrate crateName metadata buildTests;
outputs = [ "out" "lib" ]; # depending on the test setting we are either producing something with bins
outputDev = [ "lib" ]; # and libs or just test binaries
outputs = if buildTests then [ "out" ] else [ "out" "lib" ];
outputDev = if buildTests then [ "out" ] else [ "lib" ];
} // extraDerivationAttrs } // extraDerivationAttrs
)) { )) {
@ -162,4 +167,5 @@ stdenv.mkDerivation (rec {
dependencies = crate_.dependencies or []; dependencies = crate_.dependencies or [];
buildDependencies = crate_.buildDependencies or []; buildDependencies = crate_.buildDependencies or [];
crateRenames = crate_.crateRenames or {}; crateRenames = crate_.crateRenames or {};
buildTests = crate_.buildTests or false;
} }

View File

@ -1,5 +1,6 @@
crateName: metadata: { stdenv }:
'' crateName: metadata: buildTests:
if !buildTests then ''
runHook preInstall runHook preInstall
# always create $out even if we do not have binaries. We are detecting binary targets during compilation, if those are missing there is no way to only have $lib # always create $out even if we do not have binaries. We are detecting binary targets during compilation, if those are missing there is no way to only have $lib
mkdir $out mkdir $out
@ -28,5 +29,23 @@ crateName: metadata:
cp -P target/bin/* $out/bin # */ cp -P target/bin/* $out/bin # */
fi fi
fi fi
runHook postInstall
'' else
# for tests we just put them all in the output. No execution.
''
runHook preInstall
mkdir -p $out/tests
if [ -e target/bin ]; then
find target/bin/ -type f -executable -exec cp {} $out/tests \;
fi
if [ -e target/lib ]; then
find target/lib/ -type f \! -name '*.rlib' \
-a \! -name '*${stdenv.hostPlatform.extensions.sharedLibrary}' \
-a \! -name '*.d' \
-executable \
-print0 | xargs --no-run-if-empty --null install --target $out/tests;
fi
runHook postInstall runHook postInstall
'' ''

View File

@ -13,6 +13,7 @@ build_lib() {
$BUILD_OUT_DIR \ $BUILD_OUT_DIR \
$EXTRA_BUILD \ $EXTRA_BUILD \
$EXTRA_FEATURES \ $EXTRA_FEATURES \
$EXTRA_RUSTC_FLAGS \
--color $colors --color $colors
EXTRA_LIB=" --extern $CRATE_NAME=target/lib/lib$CRATE_NAME-$metadata.rlib" EXTRA_LIB=" --extern $CRATE_NAME=target/lib/lib$CRATE_NAME-$metadata.rlib"
@ -22,9 +23,10 @@ build_lib() {
} }
build_bin() { build_bin() {
crate_name=$1 local crate_name=$1
crate_name_=$(echo $crate_name | tr '-' '_') local crate_name_=$(echo $crate_name | tr '-' '_')
main_file="" local main_file=""
if [[ ! -z $2 ]]; then if [[ ! -z $2 ]]; then
main_file=$2 main_file=$2
fi fi
@ -43,6 +45,7 @@ build_bin() {
$BUILD_OUT_DIR \ $BUILD_OUT_DIR \
$EXTRA_BUILD \ $EXTRA_BUILD \
$EXTRA_FEATURES \ $EXTRA_FEATURES \
$EXTRA_RUSTC_FLAGS \
--color ${colors} \ --color ${colors} \
if [ "$crate_name_" != "$crate_name" ]; then if [ "$crate_name_" != "$crate_name" ]; then
@ -50,6 +53,24 @@ build_bin() {
fi fi
} }
build_lib_test() {
local file="$1"
EXTRA_RUSTC_FLAGS="--test $EXTRA_RUSTC_FLAGS" build_lib "$1" "$2"
}
build_bin_test() {
local crate="$1"
local file="$2"
EXTRA_RUSTC_FLAGS="--test $EXTRA_RUSTC_FLAGS" build_bin "$1" "$2"
}
build_bin_test_file() {
local file="$1"
local derived_crate_name="${file//\//_}"
derived_crate_name="${derived_crate_name%.rs}"
build_bin_test "$derived_crate_name" "$file"
}
setup_link_paths() { setup_link_paths() {
EXTRA_LIB="" EXTRA_LIB=""
if [[ -e target/link_ ]]; then if [[ -e target/link_ ]]; then

View File

@ -29,10 +29,30 @@ let
} }
''; '';
mkTestFile = name: functionName: mkFile name ''
#[cfg(test)]
#[test]
fn ${functionName}() {
assert!(true);
}
'';
mkTestFileWithMain = name: functionName: mkFile name ''
#[cfg(test)]
#[test]
fn ${functionName}() {
assert!(true);
}
fn main() {}
'';
mkLib = name: mkFile name "pub fn test() -> i32 { return 23; }"; mkLib = name: mkFile name "pub fn test() -> i32 { return 23; }";
mkTest = crateArgs: let mkTest = crateArgs: let
crate = mkCrate crateArgs; crate = mkCrate (builtins.removeAttrs crateArgs ["expectedTestOutput"]);
hasTests = crateArgs.buildTests or false;
expectedTestOutputs = crateArgs.expectedTestOutputs or null;
binaries = map (v: ''"${v.name}"'') (crateArgs.crateBin or []); binaries = map (v: ''"${v.name}"'') (crateArgs.crateBin or []);
isLib = crateArgs ? libName || crateArgs ? libPath; isLib = crateArgs ? libName || crateArgs ? libPath;
crateName = crateArgs.crateName or "nixtestcrate"; crateName = crateArgs.crateName or "nixtestcrate";
@ -44,16 +64,28 @@ let
src = mkBinExtern "src/main.rs" libName; src = mkBinExtern "src/main.rs" libName;
}; };
in runCommand "run-buildRustCrate-${crateName}-test" { in
nativeBuildInputs = [ crate ]; assert expectedTestOutputs != null -> hasTests;
} '' assert hasTests -> expectedTestOutputs != null;
${lib.concatStringsSep "\n" binaries}
${lib.optionalString isLib '' runCommand "run-buildRustCrate-${crateName}-test" {
test -e ${crate}/lib/*.rlib || exit 1 nativeBuildInputs = [ crate ];
${libTestBinary}/bin/run-test-${crateName} } (if !hasTests then ''
''} ${lib.concatStringsSep "\n" binaries}
touch $out ${lib.optionalString isLib ''
''; test -e ${crate}/lib/*.rlib || exit 1
${libTestBinary}/bin/run-test-${crateName}
''}
touch $out
'' else ''
for file in ${crate}/tests/*; do
$file 2>&1 >> $out
done
set -e
${lib.concatMapStringsSep "\n" (o: "grep '${o}' $out || { echo 'output \"${o}\" not found in:'; cat $out; exit 23; }") expectedTestOutputs}
''
);
in rec { in rec {
tests = let tests = let
@ -85,6 +117,71 @@ let
dependencies = [ (mkCrate { crateName = "foo"; libName = "foolib"; src = mkLib "src/lib.rs"; }) ]; dependencies = [ (mkCrate { crateName = "foo"; libName = "foolib"; src = mkLib "src/lib.rs"; }) ];
crateRenames = { "foo" = "foo_renamed"; }; crateRenames = { "foo" = "foo_renamed"; };
}; };
rustLibTestsDefault = {
src = mkTestFile "src/lib.rs" "baz";
buildTests = true;
expectedTestOutputs = [ "test baz ... ok" ];
};
rustLibTestsCustomLibName = {
libName = "test_lib";
src = mkTestFile "src/test_lib.rs" "foo";
buildTests = true;
expectedTestOutputs = [ "test foo ... ok" ];
};
rustLibTestsCustomLibPath = {
libPath = "src/test_path.rs";
src = mkTestFile "src/test_path.rs" "bar";
buildTests = true;
expectedTestOutputs = [ "test bar ... ok" ];
};
rustLibTestsCustomLibPathWithTests = {
libPath = "src/test_path.rs";
src = symlinkJoin {
name = "rust-lib-tests-custom-lib-path-with-tests-dir";
paths = [
(mkTestFile "src/test_path.rs" "bar")
(mkTestFile "tests/something.rs" "something")
];
};
buildTests = true;
expectedTestOutputs = [
"test bar ... ok"
"test something ... ok"
];
};
rustBinTestsCombined = {
src = symlinkJoin {
name = "rust-bin-tests-combined";
paths = [
(mkTestFileWithMain "src/main.rs" "src_main")
(mkTestFile "tests/foo.rs" "tests_foo")
(mkTestFile "tests/bar.rs" "tests_bar")
];
};
buildTests = true;
expectedTestOutputs = [
"test src_main ... ok"
"test tests_foo ... ok"
"test tests_bar ... ok"
];
};
rustBinTestsSubdirCombined = {
src = symlinkJoin {
name = "rust-bin-tests-subdir-combined";
paths = [
(mkTestFileWithMain "src/main.rs" "src_main")
(mkTestFile "tests/foo/main.rs" "tests_foo")
(mkTestFile "tests/bar/main.rs" "tests_bar")
];
};
buildTests = true;
expectedTestOutputs = [
"test src_main ... ok"
"test tests_foo ... ok"
"test tests_bar ... ok"
];
};
}; };
brotliCrates = (callPackage ./brotli-crates.nix {}); brotliCrates = (callPackage ./brotli-crates.nix {});
in lib.mapAttrs (key: value: mkTest (value // lib.optionalAttrs (!value?crateName) { crateName = key; })) cases // { in lib.mapAttrs (key: value: mkTest (value // lib.optionalAttrs (!value?crateName) { crateName = key; })) cases // {