Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions .github/scripts/run-tests-with-miri.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
#!/usr/bin/env bash
#
# Parallelizes the integration tests when running with Miri#
#
# Usage: .github/scripts/run-tests-with-miri.sh
# Env: MIRI_TEST_TIMEOUT_SECS (default 300)
# MIRI_TEST_JOBS (default: nproc)
# MIRI_TEST_TARGETS (default: all targets, space-separated)
# MIRIFLAGS (passed to `cargo miri test`)
set -uo pipefail

manifest_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
cd "$manifest_dir"

cargo miri setup

timeout_secs="${MIRI_TEST_TIMEOUT_SECS:-300}"
jobs="${MIRI_TEST_JOBS:-$(nproc)}"

target_flag() {
case "$1" in
lib) echo "--lib" ;;
*) echo "--test $1" ;;
esac
}
export -f target_flag

# shellcheck disable=SC2206
targets=(${MIRI_TEST_TARGETS:-lib core_integration regression_integration custom_page_sizes_integration statistics_integration})

pairs_file="$(mktemp)"
results_file="$(mktemp)"
trap 'rm -f "$pairs_file" "$results_file"' EXIT

for label in "${targets[@]}"; do
target="$(target_flag "$label")"
RUSTFLAGS="--cfg miri" cargo test --quiet $target -- --list 2>/dev/null |
grep ': test$' | sed 's/: test$//' |
sed "s/^/${label}\t/" \
>>"$pairs_file"
done

total=$(wc -l <"$pairs_file")
echo "Running $total tests:"
echo "Executors: $jobs"
echo "Timeout: ${timeout_secs}s"

export TIMEOUT_SECS="$timeout_secs"

run_one() {
local label="$1" name="$2"
local target
target="$(target_flag "$label")"

local out status
out="$(timeout "$TIMEOUT_SECS" cargo miri test --features miri-soft-floats $target -- --exact "$name" 2>&1)"
status=$?

if [ "$status" -eq 0 ]; then
printf 'PASS\t%s\t%s\n' "$label" "$name"
elif [ "$status" -eq 124 ]; then
printf 'TIMEOUT\t%s\t%s\n' "$label" "$name"
echo "!!! TIMED OUT after ${TIMEOUT_SECS}s: $label :: $name" >&2
else
printf 'FAIL\t%s\t%s\n' "$label" "$name"
echo "!!! FAILED: $label :: $name" >&2
echo "$out" >&2
fi
}
export -f run_one

xargs -P "$jobs" -L1 bash -c 'run_one "$@"' _ <"$pairs_file" >>"$results_file"

pass_count=$(grep -c '^PASS' "$results_file" || true)
timeout_lines=$(grep '^TIMEOUT' "$results_file" || true)
fail_lines=$(grep '^FAIL' "$results_file" || true)

echo
echo "$total / $pass_count tests passed."

if [ -n "$timeout_lines" ]; then
echo "Timed out:"
echo "$timeout_lines" | awk -F'\t' '{print " - " $2 " :: " $3}'
fi
if [ -n "$fail_lines" ]; then
echo "Failed:"
echo "$fail_lines" | awk -F'\t' '{print " - " $2 " :: " $3}'
fi

if [ -n "$timeout_lines" ] || [ -n "$fail_lines" ]; then
exit 1
fi
4 changes: 3 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,9 @@ jobs:
components: miri

- name: Run Miri
run: cargo miri test -j4 --no-fail-fast --verbose
env:
MIRIFLAGS: -Zmiri-disable-isolation
run: cargo miri test --lib --no-fail-fast --verbose --features miri-soft-floats

coverage:
name: Code Coverage
Expand Down
52 changes: 52 additions & 0 deletions .github/workflows/daily.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
name: Daily CI Jobs

on:
schedule:
- cron: "0 0 * * *"
workflow_dispatch:

permissions:
contents: read

env:
RUST_BACKTRACE: 1
CARGO_TERM_COLOR: always

jobs:
miri-integration:
name: Miri Integration Tests
runs-on: ubuntu-latest
steps:
- name: Checkout main
uses: actions/checkout@v4
with:
ref: main
fetch-depth: 0

- name: Install Rust toolchain
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: nightly
override: true
components: miri

- name: Install WABT (WebAssembly Binary Toolkit)
run: |
WABT_VERSION=1.0.41
WABT_PLATFORM="linux-x64"
wget https://github.com/WebAssembly/wabt/releases/download/${WABT_VERSION}/wabt-${WABT_VERSION}-${WABT_PLATFORM}.tar.gz
tar -xzf wabt-${WABT_VERSION}-${WABT_PLATFORM}.tar.gz
sudo cp wabt-${WABT_VERSION}/bin/* /usr/local/bin/
shell: bash

- name: Convert wast files for Miri
run: cargo test --test miri_wast_convert -- --ignored

- name: Run Miri
env:
MIRIFLAGS: -Zmiri-disable-isolation
MIRI_TEST_TIMEOUT_SECS: 300
MIRI_TEST_JOBS: 4
MIRI_TEST_TARGETS: core_integration regression_integration custom_page_sizes_integration statistics_integration
run: ./.github/scripts/run-tests-with-miri.sh
6 changes: 6 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ default = []
# by the verifier. Useful for fuzzing/regression testing
strict-assertions = []

# Disables inline asm (e.g. x86 `sqrtss`) for Miri
miri-soft-floats = ["libm/force-soft-floats"]

[dependencies]
libm = "0.2.16"

Expand All @@ -65,3 +68,6 @@ panic = "abort"
opt-level = 3
lto = "thin"
codegen-units = 1

[target.'cfg(miri)'.dependencies]
libm = { version = "0.2.16", features = ["force-soft-floats"] }
6 changes: 4 additions & 2 deletions tests/core_integration.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
#![cfg(not(miri))]

mod util;
use spacewasm::vec;
use util::{run_wast_test_file, spectest_host_module};
Expand All @@ -14,6 +12,7 @@ fn address() {
}

#[test]
#[cfg_attr(miri, ignore = "stack recursion")]
fn call() {
run("core/call");
}
Expand Down Expand Up @@ -209,6 +208,7 @@ fn memory() {
}

#[test]
#[cfg_attr(miri, ignore = "stack recursion")]
fn skip_stack_guard_page() {
run("core/skip-stack-guard-page");
}
Expand Down Expand Up @@ -244,6 +244,7 @@ fn labels() {
}

#[test]
#[cfg_attr(miri, ignore = "malloc too slow")]
fn memory_grow() {
run("core/memory_grow");
}
Expand Down Expand Up @@ -309,6 +310,7 @@ fn elem() {
}

#[test]
#[cfg_attr(miri, ignore = "stack recursion")]
fn fac() {
run("core/fac");
}
Expand Down
2 changes: 0 additions & 2 deletions tests/custom_page_sizes_integration.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
#![cfg(not(miri))]

mod util;
use spacewasm::vec;
use util::{run_wast_test_file, spectest_host_module};
Expand Down
9 changes: 9 additions & 0 deletions tests/miri_wast_convert.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#![cfg(not(miri))]

mod util;

#[test]
#[ignore]
fn convert() {
util::convert_wast_for_miri();
}
2 changes: 0 additions & 2 deletions tests/regression_integration.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
#![cfg(not(miri))]

mod util;
use std::{ops::ControlFlow, sync::Mutex};

Expand Down
109 changes: 90 additions & 19 deletions tests/util/spectest.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,16 @@ use std::ops::ControlFlow;
use std::panic::catch_unwind;
use std::path::Path;
use std::path::PathBuf;
use std::process::Command as ProcessCommand;
use std::ptr::NonNull;
use std::rc::Rc;
use std::sync::{Arc, Mutex};

type SubtestLogType = Arc<Mutex<Option<Rc<RefCell<LimitedVec<String>>>>>>;
#[cfg(not(miri))]
use std::process::Command as ProcessCommand;
#[cfg(not(miri))]
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, Mutex};

type SubtestLogType = Arc<Mutex<Option<Rc<RefCell<LimitedVec<String>>>>>>;

#[derive(Debug, Deserialize, Serialize)]
struct TestFile {
Expand Down Expand Up @@ -1035,11 +1038,67 @@ fn check_initialization_error(result: InterpreterResult, text: &str) {
}
}

// Simple temp directory that cleans up on drop
/// Wrapper for `wast2json`
#[cfg(not(miri))]
fn wast2json(source_wast_path: &Path, out_dir: &Path, test_filename: &str) {
let output = ProcessCommand::new("wast2json")
.arg(source_wast_path)
.arg("--enable-custom-page-sizes")
.arg("-o")
.arg(out_dir.join(format!("{}.json", test_filename)))
.current_dir(out_dir)
.output()
.unwrap_or_else(|e| panic!("Failed to run wast2json: {e}"));

if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
panic!("wast2json failed: {}", stderr);
}
}

#[cfg(not(miri))]
pub fn convert_wast_for_miri() {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let tests_dir = PathBuf::from(manifest_dir).join("tests");
let converted_root = PathBuf::from(manifest_dir).join("target").join("miri-wast");

let _ = std::fs::remove_dir_all(&converted_root);

// The `tests/` subdirectories containing `.wast` files. Hard-coded
// rather than walked, since it's a fixed, small set; add to this list
// if a new `.wast` suite subdirectory is introduced.
let wast_dirs: &[&str] = &["core", "regression", "custom-page-sizes"];

for subdir in wast_dirs {
let dir = tests_dir.join(subdir);
for entry in
std::fs::read_dir(&dir).unwrap_or_else(|e| panic!("Failed to read {dir:?}: {e}"))
{
let wast_path = entry.unwrap().path();
if wast_path.extension().is_none_or(|ext| ext != "wast") {
continue;
}

let rel = wast_path
.strip_prefix(&tests_dir)
.unwrap()
.with_extension("");
let test_filename = rel.file_stem().unwrap().to_string_lossy().to_string();

let out_dir = converted_root.join(&rel);
std::fs::create_dir_all(&out_dir).unwrap();

wast2json(&wast_path, &out_dir, &test_filename);
}
}
}

#[cfg(not(miri))]
struct TempDir {
path: PathBuf,
}

#[cfg(not(miri))]
impl TempDir {
fn new() -> std::io::Result<Self> {
static COUNTER: AtomicU64 = AtomicU64::new(0);
Expand All @@ -1056,6 +1115,7 @@ impl TempDir {
}
}

#[cfg(not(miri))]
impl Drop for TempDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.path);
Expand Down Expand Up @@ -1416,9 +1476,11 @@ pub fn run_wast_test_file(test_name: &str, host_modules: HostModuleFactory) {
.join(format!("{}.wast", test_name));

// Create a temporary directory for generated files
#[cfg(not(miri))]
let temp_dir =
TempDir::new().unwrap_or_else(|e| panic!("Failed to create temp directory: {e}"));
let temp_path = temp_dir.path();
#[cfg(not(miri))]
let temp_path = temp_dir.path().to_path_buf();

// Extract just the filename (without directory path) for the JSON output
let test_filename = PathBuf::from(test_name)
Expand All @@ -1427,28 +1489,37 @@ pub fn run_wast_test_file(test_name: &str, host_modules: HostModuleFactory) {
.to_string_lossy()
.to_string();

// Run wast2json to generate Wasm modules and JSON descriptor
let output = ProcessCommand::new("wast2json")
.arg(&source_wast_path)
.arg("--enable-custom-page-sizes")
.arg("-o")
.arg(temp_path.join(format!("{}.json", test_filename)))
.current_dir(temp_path)
.output()
.unwrap_or_else(|e| panic!("Failed to run wast2json: {e}"));
// Pre-convert tests for Miri to run
#[cfg(not(miri))]
wast2json(&source_wast_path, &temp_path, &test_filename);
#[cfg(not(miri))]
let test_dir = temp_path;

#[cfg(miri)]
let test_dir = {
let dir = PathBuf::from(manifest_dir)
.join("target")
.join("miri-wast")
.join(test_name);

if !dir.join(format!("{}.json", test_filename)).exists() {
panic!(
"Converted wast files missing at {}. Run `cargo test --test miri_wast_convert \
-- --ignored` (with `wast2json` on PATH) before `cargo miri test`.",
dir.display()
);
}

if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
panic!("wast2json failed: {}", stderr);
}
dir
};

let wast_line = Arc::new(Mutex::new(None));
#[allow(clippy::arc_with_non_send_sync)]
let subtest_log = Arc::new(Mutex::new(None));

match catch_unwind(|| {
run_wast_test_file_inner(
temp_path.to_path_buf(),
test_dir,
&test_filename,
host_modules,
wast_line.clone(),
Expand Down
Loading