From 4d071dbe75a9f90fe1213d1d9ebb9d74e7125fb4 Mon Sep 17 00:00:00 2001 From: Alex Huszagh Date: Sun, 3 Jul 2022 13:15:18 -0500 Subject: [PATCH] Add integration tests. Adds an an integration test for remote Docker support with and without persistent data volumes, and adds it to CI. In addition, we've added integration tests for custom toolchains using `cargo-bisect-rustc`, and tests for docker-in-docker scenarios. Also changes the workflow to ensure publish depends on fmt, clippy, and cargo-deny. Fixes a bug when removing data volumes, since it does not require a target. The docker-in-docker scenarios both run the cross unittests, and test workspace and manifest paths. --- .github/workflows/ci.yml | 63 ++++++++++++- ci/shared.sh | 23 +++++ ci/test-bisect.sh | 52 +++++++++++ ci/test-docker-in-docker.sh | 56 ++++++++++++ ci/test-remote.sh | 55 ++++++++++++ ci/test.sh | 25 +----- src/bin/commands/containers.rs | 1 + src/docker/shared.rs | 156 +++++++++++++++++++++++++++++++++ 8 files changed, 407 insertions(+), 24 deletions(-) create mode 100755 ci/shared.sh create mode 100755 ci/test-bisect.sh create mode 100755 ci/test-docker-in-docker.sh create mode 100755 ci/test-remote.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b521928dd..80e89bf63 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -321,8 +321,69 @@ jobs: LATEST: ${{ needs.check.outputs.is-latest || 'false' }} shell: bash + # we should always have an artifact from a previous build. + remote: + needs: [shellcheck, test, check] + runs-on: ubuntu-latest + if: github.actor == 'bors[bot]' + steps: + - uses: actions/checkout@v3 + - uses: ./.github/actions/setup-rust + + - name: LLVM instrument coverage + id: remote-cov + uses: ./.github/actions/cargo-llvm-cov + with: + name: integration-remote + + - name: Run Remote Test + env: + TARGET: aarch64-unknown-linux-gnu + run: ./ci/test-remote.sh + shell: bash + + bisect: + needs: [shellcheck, test, check] + runs-on: ubuntu-latest + if: github.actor == 'bors[bot]' + steps: + - uses: actions/checkout@v3 + - uses: ./.github/actions/setup-rust + + - name: LLVM instrument coverage + id: bisect-cov + uses: ./.github/actions/cargo-llvm-cov + with: + name: integration-bisect + + - name: Run Bisect Test + env: + TARGET: aarch64-unknown-linux-gnu + run: ./ci/test-bisect.sh + shell: bash + + docker-in-docker: + needs: [shellcheck, test, check] + runs-on: ubuntu-latest + if: github.actor == 'bors[bot]' + steps: + - uses: actions/checkout@v3 + - uses: ./.github/actions/setup-rust + + - name: LLVM instrument coverage + id: docker-in-docker-cov + uses: ./.github/actions/cargo-llvm-cov + with: + name: integration-docker-in-docker + + - name: Run Docker-in-Docker Test + env: + TARGET: aarch64-unknown-linux-gnu + run: ./ci/test-docker-in-docker.sh + shell: bash + publish: - needs: [build, check] + needs: [build, check, fmt, clippy, cargo-deny, remote, bisect, docker-in-docker] runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 diff --git a/ci/shared.sh b/ci/shared.sh new file mode 100755 index 000000000..457861dd7 --- /dev/null +++ b/ci/shared.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +function retry { + local tries="${TRIES-5}" + local timeout="${TIMEOUT-1}" + local try=0 + local exit_code=0 + + while (( try < tries )); do + if "${@}"; then + return 0 + else + exit_code=$? + fi + + sleep "${timeout}" + echo "Retrying ..." 1>&2 + try=$(( try + 1 )) + timeout=$(( timeout * 2 )) + done + + return ${exit_code} +} diff --git a/ci/test-bisect.sh b/ci/test-bisect.sh new file mode 100755 index 000000000..4db9a5acf --- /dev/null +++ b/ci/test-bisect.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# shellcheck disable=SC1091,SC1090 + +# test to see that custom toolchains work + +set -x +set -eo pipefail + +if [[ -z "${TARGET}" ]]; then + export TARGET="aarch64-unknown-linux-gnu" +fi + +ci_dir=$(dirname "${BASH_SOURCE[0]}") +ci_dir=$(realpath "${ci_dir}") +. "${ci_dir}"/shared.sh +project_home=$(dirname "${ci_dir}") + +main() { + local td= + local err= + + retry cargo fetch + cargo build + cargo install cargo-bisect-rustc --debug + export CROSS="${project_home}/target/debug/cross" + + td="$(mktemp -d)" + git clone --depth 1 https://github.com/cross-rs/rust-cpp-hello-word "${td}" + + pushd "${td}" + retry cargo fetch + # shellcheck disable=SC2016 + echo '#!/usr/bin/env bash +export CROSS_CUSTOM_TOOLCHAIN=1 +exec "${CROSS}" run --target '"${TARGET}" > bisect.sh + chmod +x bisect.sh + + if ! err=$(cargo bisect-rustc --script=./bisect.sh --target "${TARGET}" 2>&1 >/dev/null); then + if [[ "${err}" != *"does not reproduce the regression"* ]]; then + echo "${err}" + exit 1 + fi + else + echo "should have failed, instead succeeded" 1>&2 + exit 1 + fi + popd + + rm -rf "${td}" +} + +main diff --git a/ci/test-docker-in-docker.sh b/ci/test-docker-in-docker.sh new file mode 100755 index 000000000..3057c68d8 --- /dev/null +++ b/ci/test-docker-in-docker.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# shellcheck disable=SC1004 + +# test to see that running docker-in-docker works + +set -x +set -eo pipefail + +if [[ -z "${TARGET}" ]]; then + export TARGET="aarch64-unknown-linux-gnu" +fi + +source=$(dirname "${BASH_SOURCE[0]}") +source=$(realpath "${source}") +home=$(dirname "${source}") + +main() { + docker run -v "${home}":"${home}" -w "${home}" \ + --rm -e TARGET -e RUSTFLAGS -e RUST_TEST_THREADS \ + -e LLVM_PROFILE_FILE -e CARGO_INCREMENTAL \ + -v /var/run/docker.sock:/var/run/docker.sock \ + docker:18.09-dind sh -c ' +#!/usr/bin/env sh +set -x +set -euo pipefail + +apk add curl +curl --proto "=https" --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y +source "${HOME}/.cargo/env" + +# building on release is slow +apk add libgcc gcc musl-dev +cargo test --workspace +cargo install --path . --force --debug + +export CROSS_CONTAINER_IN_CONTAINER=1 + +apk add git +td="$(mktemp -d)" +git clone --depth 1 https://github.com/cross-rs/rust-cpp-hello-word "${td}" +cd "${td}" +cross run --target "${TARGET}" --verbose + +td="$(mktemp -d)" +git clone --depth 1 https://github.com/cross-rs/test-workspace "${td}" +cd "${td}" +cross build --target "${TARGET}" --workspace \ + --manifest-path="./workspace/Cargo.toml" --verbose +cd workspace +cross build --target "${TARGET}" --workspace --verbose +cd binary +cross run --target "${TARGET}" --verbose +' +} + +main diff --git a/ci/test-remote.sh b/ci/test-remote.sh new file mode 100755 index 000000000..8fb3583ed --- /dev/null +++ b/ci/test-remote.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# shellcheck disable=SC1091,SC1090 + +# test to see that remote docker support works. + +set -x +set -eo pipefail + +export CROSS_REMOTE=1 +if [[ -z "${TARGET}" ]]; then + export TARGET="aarch64-unknown-linux-gnu" +fi + +ci_dir=$(dirname "${BASH_SOURCE[0]}") +ci_dir=$(realpath "${ci_dir}") +. "${ci_dir}"/shared.sh +project_home=$(dirname "${ci_dir}") + +main() { + local err= + + retry cargo fetch + cargo build + export CROSS="${project_home}/target/debug/cross" + export CROSS_UTIL="${project_home}/target/debug/cross-util" + + # if the create volume fails, ensure it exists. + if ! err=$("${CROSS_UTIL}" volumes create 2>&1 >/dev/null); then + if [[ "${err}" != *"already exists"* ]]; then + echo "${err}" + exit 1 + fi + fi + cross_test_cpp + "${CROSS_UTIL}" volumes remove + + # ensure the data volume was removed. + cross_test_cpp +} + +cross_test_cpp() { + local td= + td="$(mktemp -d)" + + git clone --depth 1 https://github.com/cross-rs/rust-cpp-hello-word "${td}" + + pushd "${td}" + retry cargo fetch + "${CROSS}" run --target "${TARGET}" + popd + + rm -rf "${td}" +} + +main diff --git a/ci/test.sh b/ci/test.sh index 4b9b494e1..fbfaddaa5 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# shellcheck disable=SC2086 +# shellcheck disable=SC2086,SC1091,SC1090 set -x set -euo pipefail @@ -10,30 +10,9 @@ set -euo pipefail ci_dir=$(dirname "${BASH_SOURCE[0]}") ci_dir=$(realpath "${ci_dir}") +. "${ci_dir}"/shared.sh project_home=$(dirname "${ci_dir}") -function retry { - local tries="${TRIES-5}" - local timeout="${TIMEOUT-1}" - local try=0 - local exit_code=0 - - while (( try < tries )); do - if "${@}"; then - return 0 - else - exit_code=$? - fi - - sleep "${timeout}" - echo "Retrying ..." 1>&2 - try=$(( try + 1 )) - timeout=$(( timeout * 2 )) - done - - return ${exit_code} -} - workspace_test() { "${CROSS[@]}" build --target "${TARGET}" --workspace "$@" ${CROSS_FLAGS} "${CROSS[@]}" run --target "${TARGET}" -p binary "$@" ${CROSS_FLAGS} diff --git a/src/bin/commands/containers.rs b/src/bin/commands/containers.rs index ffa3e863a..a6a3712b5 100644 --- a/src/bin/commands/containers.rs +++ b/src/bin/commands/containers.rs @@ -482,6 +482,7 @@ pub fn remove_persistent_volume( engine: &docker::Engine, channel: Option<&str>, ) -> cross::Result<()> { + // we only need a triple that needs docker: the actual target doesn't matter. let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?; let triple = cross::Host::X86_64UnknownLinuxGnu.triple(); let (_, _, dirs) = diff --git a/src/docker/shared.rs b/src/docker/shared.rs index 36893b2af..0465ee454 100644 --- a/src/docker/shared.rs +++ b/src/docker/shared.rs @@ -763,6 +763,162 @@ mod tests { } } + mod directories { + use super::*; + use crate::cargo::cargo_metadata_with_args; + use crate::temp; + + fn unset_env() -> Vec<(&'static str, Option)> { + let mut result = vec![]; + let envvars = ["CARGO_HOME", "XARGO_HOME", "NIX_STORE"]; + for var in envvars { + result.push((var, env::var(var).ok())); + env::remove_var(var); + } + + result + } + + fn reset_env(vars: Vec<(&'static str, Option)>) { + for (var, value) in vars { + if let Some(value) = value { + env::set_var(var, value); + } + } + } + + fn create_engine(msg_info: MessageInfo) -> Result { + Engine::from_path(get_container_engine()?, Some(false), msg_info) + } + + fn cargo_metadata(subdir: bool, msg_info: MessageInfo) -> Result { + let mut metadata = cargo_metadata_with_args( + Some(Path::new(env!("CARGO_MANIFEST_DIR"))), + None, + msg_info, + )? + .ok_or_else(|| eyre::eyre!("could not find cross workspace"))?; + + let root = match subdir { + true => get_cwd()?.join("member"), + false => get_cwd()?.parent().unwrap().to_path_buf(), + }; + fs::create_dir_all(&root)?; + metadata.workspace_root = root; + metadata.target_directory = metadata.workspace_root.join("target"); + + Ok(metadata) + } + + fn home() -> Result { + home::home_dir().ok_or_else(|| eyre::eyre!("could not find home directory")) + } + + fn get_cwd() -> Result { + // we need this directory to exist for Windows + let path = temp::dir()?.join("Documents").join("package"); + fs::create_dir_all(&path)?; + Ok(path) + } + + fn get_sysroot() -> Result { + Ok(home()? + .join(".rustup") + .join("toolchains") + .join("stable-x86_64-unknown-linux-gnu")) + } + + fn get_directories( + metadata: &CargoMetadata, + mount_finder: &MountFinder, + ) -> Result { + let cwd = get_cwd()?; + let sysroot = get_sysroot()?; + Directories::create(mount_finder, metadata, &cwd, &sysroot) + } + + fn path_to_posix(path: &Path) -> Result { + #[cfg(target_os = "windows")] + { + path.as_wslpath() + } + #[cfg(not(target_os = "windows"))] + { + path.as_posix() + } + } + + #[track_caller] + fn paths_equal(x: &Path, y: &Path) -> Result<()> { + assert_eq!(path_to_posix(x)?, path_to_posix(y)?); + Ok(()) + } + + #[test] + fn test_host() -> Result<()> { + let vars = unset_env(); + let mount_finder = MountFinder::new(vec![]); + let metadata = cargo_metadata(false, MessageInfo::default())?; + let directories = get_directories(&metadata, &mount_finder)?; + paths_equal(&directories.cargo, &home()?.join(".cargo"))?; + paths_equal(&directories.xargo, &home()?.join(".xargo"))?; + paths_equal(&directories.host_root, &metadata.workspace_root)?; + assert_eq!( + &directories.mount_root, + &path_to_posix(&metadata.workspace_root)? + ); + assert_eq!(&directories.mount_cwd, &path_to_posix(&get_cwd()?)?); + + reset_env(vars); + Ok(()) + } + + #[test] + #[cfg_attr(not(target_os = "linux"), ignore)] + fn test_docker_in_docker() -> Result<()> { + let vars = unset_env(); + + let engine = create_engine(MessageInfo::default()); + let hostname = env::var("HOSTNAME"); + if engine.is_err() || hostname.is_err() { + eprintln!("could not get container engine or no hostname found"); + reset_env(vars); + return Ok(()); + } + let engine = engine.unwrap(); + let hostname = hostname.unwrap(); + let output = subcommand(&engine, "inspect") + .arg(hostname) + .run_and_get_output(MessageInfo::default())?; + if !output.status.success() { + eprintln!("inspect failed"); + reset_env(vars); + return Ok(()); + } + + let mount_finder = MountFinder::create(&engine, true)?; + let metadata = cargo_metadata(true, MessageInfo::default())?; + let directories = get_directories(&metadata, &mount_finder)?; + let mount_finder = MountFinder::new(docker_read_mount_paths(&engine)?); + let mount_path = |p| mount_finder.find_mount_path(p); + + paths_equal(&directories.cargo, &mount_path(home()?.join(".cargo")))?; + paths_equal(&directories.xargo, &mount_path(home()?.join(".xargo")))?; + paths_equal(&directories.host_root, &mount_path(get_cwd()?))?; + assert_eq!( + &directories.mount_root, + &path_to_posix(&mount_path(get_cwd()?))? + ); + assert_eq!( + &directories.mount_cwd, + &path_to_posix(&mount_path(get_cwd()?))? + ); + + reset_env(vars); + Ok(()) + } + } + mod mount_finder { use super::*;