diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 93d63d85e8..0ea80b82b6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,7 +11,6 @@ env: RUST_BACKTRACE: 1 CARGO_NET_GIT_FETCH_WITH_CLI: true MDBOOK_VERSION: v0.4.21 - MSRV: "1.64.0" # Required for [workspace.package] concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -23,13 +22,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: ${{ env.MSRV }} - override: true - target: wasm32-unknown-unknown - name: Rust Cache uses: Swatinem/rust-cache@v2 - name: Install Node @@ -43,6 +35,13 @@ jobs: - name: Install Pipenv run: curl https://raw.githubusercontent.com/pypa/pipenv/master/get-pipenv.py | python3 - run: yarn --version + - name: Setup Wasmer + uses: wasmerio/setup-wasmer@v1 + - name: Install cargo-wapm + uses: actions-rs/cargo@v1 + with: + command: install + args: cargo-wapm --debug --verbose - name: Type Checking uses: actions-rs/cargo@v1 with: @@ -73,8 +72,7 @@ jobs: message: | Make sure you keep an eye on build times! - One of this project's goals is to [keep CI runs under 5 minutes][goal] - so developers can maintain fast edit-compile-test cycles. + One of this project's goals is to [keep CI runs under 5 minutes][goal] so developers can maintain fast edit-compile-test cycles. [goal]: https://github.com/wasmerio/wasmer-pack/blob/bdfd5c9483821651cf0bbd70189fc04416bc22b1/CONTRIBUTING.md#goal-1-fast-compile-times @@ -83,12 +81,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - components: rustfmt, clippy - name: Rust Cache uses: Swatinem/rust-cache@v2 - name: Check Formatting @@ -107,11 +99,6 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: nightly - override: true - name: Rust Cache uses: Swatinem/rust-cache@v2 - name: Install mdbook diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index ee8e704436..9b18946864 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -9,18 +9,12 @@ on: env: RUST_BACKTRACE: 1 CARGO_NET_GIT_FETCH_WITH_CLI: true - MSRV: "1.64.0" # Required for [workspace.package] jobs: publish-to-wapm: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: ${{ env.MSRV }} - name: Rust Cache uses: Swatinem/rust-cache@v2 - name: Install WebAssembly targets diff --git a/.gitignore b/.gitignore index 03dfcd7ab7..e0c55970a0 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ yarn-error.log .pytest_cache __pycache__/ *.pyc +.venv/ diff --git a/Cargo.lock b/Cargo.lock index a6db8929d1..8b45d6b80a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -856,6 +856,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "integration-tests" +version = "0.0.0" +dependencies = [ + "anyhow", + "tracing-subscriber", + "wasmer-pack-testing", +] + [[package]] name = "io-lifetimes" version = "1.0.3" @@ -977,6 +986,15 @@ dependencies = [ "crc", ] +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata", +] + [[package]] name = "matches" version = "0.1.9" @@ -1044,6 +1062,16 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "nuke-dir" version = "0.1.0" @@ -1081,6 +1109,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "path-clean" version = "0.1.0" @@ -1350,6 +1384,9 @@ name = "regex-automata" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax", +] [[package]] name = "regex-syntax" @@ -1591,6 +1628,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + [[package]] name = "similar" version = "2.2.1" @@ -1606,6 +1652,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + [[package]] name = "socket2" version = "0.4.7" @@ -1725,6 +1777,15 @@ dependencies = [ "syn", ] +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -1827,6 +1888,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -1954,6 +2045,12 @@ dependencies = [ "url", ] +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "version_check" version = "0.9.4" @@ -2278,6 +2375,15 @@ dependencies = [ "webc 0.4.0", ] +[[package]] +name = "wasmer-pack-testing" +version = "0.1.0" +dependencies = [ + "tempfile", + "tracing", + "wasmer-pack-cli", +] + [[package]] name = "wasmer-pack-wasm" version = "0.5.3" diff --git a/Cargo.toml b/Cargo.toml index 3683307696..c43823c9e9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["crates/*", "examples/*"] +members = ["crates/*", "examples/*", "integration-tests"] [workspace.package] authors = ["Wasmer Engineering Team "] diff --git a/crates/cli/src/codegen.rs b/crates/cli/src/codegen.rs index 2b67d00c82..d7273bc620 100644 --- a/crates/cli/src/codegen.rs +++ b/crates/cli/src/codegen.rs @@ -1,16 +1,17 @@ use std::path::PathBuf; -use anyhow::{Context, Error}; +use crate::Error; +use anyhow::Context; use clap::Parser; #[derive(Debug, Parser)] pub struct Codegen { /// Where to save the generated bindings. #[clap(short, long)] - out_dir: Option, + pub out_dir: Option, /// The Pirita file to read. #[clap(parse(from_os_str))] - input: PathBuf, + pub input: PathBuf, } impl Codegen { diff --git a/crates/cli/src/lib.rs b/crates/cli/src/lib.rs index ba3045fb28..8f00f8e1f5 100644 --- a/crates/cli/src/lib.rs +++ b/crates/cli/src/lib.rs @@ -6,3 +6,5 @@ pub use crate::{ codegen::{Codegen, Language}, show::{Format, Show}, }; + +pub type Error = anyhow::Error; diff --git a/crates/cli/src/pirita.rs b/crates/cli/src/pirita.rs index 714f65d469..0d2187d130 100644 --- a/crates/cli/src/pirita.rs +++ b/crates/cli/src/pirita.rs @@ -3,7 +3,8 @@ use std::{ path::{Path, PathBuf}, }; -use anyhow::{Context, Error}; +use crate::Error; +use anyhow::Context; use wapm_targz_to_pirita::{generate_webc_file, TransformManifestFunctions}; use wasmer_pack::{Command, Interface, Library, Metadata, Module, Package}; use webc::{DirOrFile, Manifest, ParseOptions, WebC, WebCOwned}; diff --git a/crates/testing/Cargo.toml b/crates/testing/Cargo.toml new file mode 100644 index 0000000000..d93d2325d6 --- /dev/null +++ b/crates/testing/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "wasmer-pack-testing" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tempfile = "3.3.0" +tracing = "0.1.37" +wasmer-pack-cli = { version = "0.5.3", path = "../cli" } diff --git a/crates/testing/src/errors.rs b/crates/testing/src/errors.rs new file mode 100644 index 0000000000..59597e2d2c --- /dev/null +++ b/crates/testing/src/errors.rs @@ -0,0 +1,155 @@ +use std::{ + ffi::OsString, + fmt::{self, Display, Formatter}, + path::PathBuf, +}; + +#[derive(Debug)] +pub enum CommandFailed { + Spawn { + command: OsString, + error: std::io::Error, + }, + CompletedUnsuccessfully { + command: String, + stdout: String, + stderr: String, + exit_code: Option, + }, +} + +impl std::error::Error for CommandFailed { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + CommandFailed::Spawn { error, .. } => Some(error), + CommandFailed::CompletedUnsuccessfully { .. } => None, + } + } +} + +impl Display for CommandFailed { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + CommandFailed::Spawn { command, .. } => write!(f, "Unable to spawn {command:?}"), + CommandFailed::CompletedUnsuccessfully { + command, + stdout, + stderr, + exit_code, + } => { + write!(f, "Executing {command} failed")?; + if let Some(exit_code) = exit_code { + write!(f, " (exit code: {exit_code})")?; + } + write!(f, ".")?; + + if !stdout.trim().is_empty() { + writeln!(f)?; + writeln!(f, "Stdout: {stdout}")?; + } + if !stderr.trim().is_empty() { + writeln!(f)?; + writeln!(f, "Stderr: {stderr}")?; + } + + Ok(()) + } + } + } +} + +#[derive(Debug)] +pub enum TestFailure { + InitializingJavascriptEnvironment(CommandFailed), + BindingsGeneration(wasmer_pack_cli::Error), + DeterminingScriptDirectory, + DeterminingScriptFilename, + InstallingDependencies(CommandFailed), + CreatingVirtualEnvironment { + venv_dir: PathBuf, + error: CommandFailed, + }, + TestScript(CommandFailed), +} + +impl std::error::Error for TestFailure { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + TestFailure::BindingsGeneration(e) => Some(&**e), + TestFailure::InstallingDependencies(e) + | TestFailure::CreatingVirtualEnvironment { error: e, .. } + | TestFailure::TestScript(e) => Some(e), + TestFailure::InitializingJavascriptEnvironment(e) => Some(e), + TestFailure::DeterminingScriptDirectory | TestFailure::DeterminingScriptFilename => { + None + } + } + } +} + +impl Display for TestFailure { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + TestFailure::BindingsGeneration(_) => write!(f, "Unable to generate bindings"), + TestFailure::DeterminingScriptDirectory => { + write!(f, "Unable to determine the script directory") + } + TestFailure::DeterminingScriptFilename => { + write!(f, "Unable to determine the script filename") + } + TestFailure::InstallingDependencies(_) => write!(f, "Unable to install dependencies"), + TestFailure::CreatingVirtualEnvironment { venv_dir, .. } => write!( + f, + "Unable to create a virtual environment in \"{}\"", + venv_dir.display() + ), + TestFailure::TestScript(_) => write!(f, "The tests failed"), + TestFailure::InitializingJavascriptEnvironment(_) => { + write!(f, "Unable to initialize the JavaScript environment") + } + } + } +} + +#[derive(Debug)] +pub enum LoadError { + ManifestNotFound { path: PathBuf }, + TempDir(std::io::Error), + SpawnFailed(std::io::Error), + CargoWapmFailed(CommandFailed), + UnableToLocateBindings { dir: PathBuf, error: std::io::Error }, +} + +impl std::error::Error for LoadError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + LoadError::TempDir(e) + | LoadError::SpawnFailed(e) + | LoadError::UnableToLocateBindings { error: e, .. } => Some(e), + LoadError::CargoWapmFailed(e) => Some(e), + LoadError::ManifestNotFound { .. } => None, + } + } +} + +impl Display for LoadError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + LoadError::ManifestNotFound { path } => { + write!(f, "\"{}\" doesn't exist", path.display()) + } + LoadError::TempDir(_) => write!(f, "Unable to create a temporary directory"), + LoadError::SpawnFailed(_) => { + write!(f, "Unable to start \"cargo wapm\". Is it installed?") + } + LoadError::CargoWapmFailed(_) => { + write!(f, "Generating a WAPM package with \"cargo wapm\" failed") + } + LoadError::UnableToLocateBindings { dir, .. } => write!( + f, + "Unable to locate the generated bindings in \"{}\"", + dir.display() + ), + } + } +} diff --git a/crates/testing/src/javascript.rs b/crates/testing/src/javascript.rs new file mode 100644 index 0000000000..327456b4f4 --- /dev/null +++ b/crates/testing/src/javascript.rs @@ -0,0 +1,53 @@ +use std::{path::Path, process::Command}; + +use wasmer_pack_cli::Language; + +use crate::{utils, TestFailure}; + +pub(crate) fn run(script_path: &Path, wapm_dir: &Path, temp_dir: &Path) -> Result<(), TestFailure> { + let dest = temp_dir.join("javascript"); + tracing::info!("Preparing the JavaScript package"); + + utils::generate_bindings(&dest, wapm_dir, Language::JavaScript)?; + let script_dir = script_path + .parent() + .ok_or(TestFailure::DeterminingScriptDirectory)?; + + utils::execute_command( + Command::new("yarn") + .arg("init") + .arg("--yes") + .current_dir(script_dir), + ) + .map_err(TestFailure::InitializingJavascriptEnvironment)?; + + let package_path = dest.join("package"); + utils::execute_command( + Command::new("yarn") + .arg("add") + .arg(&package_path) + .current_dir(script_dir), + ) + .map_err(TestFailure::InstallingDependencies)?; + + let test_filename = script_path + .file_name() + .ok_or(TestFailure::DeterminingScriptFilename)?; + + utils::execute_command( + Command::new("node") + .arg(test_filename) + .current_dir(script_dir), + ) + .map_err(TestFailure::InstallingDependencies)?; + + utils::execute_command( + Command::new("yarn") + .arg("remove") + .arg(&package_path) + .current_dir(script_dir), + ) + .map_err(TestFailure::InstallingDependencies)?; + + Ok(()) +} diff --git a/crates/testing/src/lib.rs b/crates/testing/src/lib.rs new file mode 100644 index 0000000000..ae7bb36697 --- /dev/null +++ b/crates/testing/src/lib.rs @@ -0,0 +1,76 @@ +//! Utilities for testing bindings generated by Wasmer Pack. +//! +//! Typical. +//! +//! ```rust,no_run +//! # use wasmer_pack_testing::TestEnvironment; +//! # fn main() -> Result<(), Box> { +//! let env = TestEnvironment::for_crate("./path/to/Cargo.toml", "/tmp")?; +//! env.python("./my_tests.py")?; +//! env.javascript("./my_test.js")?; +//! env.typescript("./my_tests.ts")?; +//! # Ok(()) +//! # } +//! ``` +//! +//! Under the hood, this will use `cargo wapm` to compile a Rust crate to +//! WebAssembly and turn it into a WAPM package. +//! +//! You can tell it to run test scripts written in various languages. +//! +//! The [`TestEnvironment::python()`] method will create a Virtual Environment +//! in the script's directory and install the generated Python library. The +//! provided test script will then be run in that environment using +//! [py.test][pytest]. +//! +//! The [`TestEnvironment::javascript()`] and [`TestEnvironment::typescript()`] +//! methods will generate JavaScript bindings for the Rust crate and use +//! `yarn link` to add them as a dependency. From there, the test script will +//! be run using [Jest][jest]. +//! +//! [pytest]: https://docs.pytest.org/ +//! [jest]: https://jestjs.io/ + +mod errors; +mod javascript; +mod python; +mod utils; + +pub use crate::errors::{CommandFailed, LoadError, TestFailure}; + +use std::path::{Path, PathBuf}; + +#[derive(Debug)] +pub struct TestEnvironment { + temp_dir: PathBuf, + wapm_dir: PathBuf, +} + +impl TestEnvironment { + pub fn for_crate( + manifest_path: impl AsRef, + temp_dir: impl AsRef, + ) -> Result { + let manifest_path = manifest_path.as_ref(); + let temp_dir = temp_dir.as_ref(); + + let wapm_dir = utils::compile_rust_to_wapm_package(manifest_path, temp_dir.join("target"))?; + + Ok(TestEnvironment { + temp_dir: temp_dir.to_path_buf(), + wapm_dir, + }) + } + + pub fn python(&self, script_path: impl AsRef) -> Result<(), TestFailure> { + python::run(script_path.as_ref(), &self.wapm_dir, &self.temp_dir) + } + + pub fn javascript(&self, script_path: impl AsRef) -> Result<(), TestFailure> { + javascript::run(script_path.as_ref(), &self.wapm_dir, &self.temp_dir) + } + + pub fn typescript(&self, _script_path: impl AsRef) -> Result<(), TestFailure> { + todo!(); + } +} diff --git a/crates/testing/src/python.rs b/crates/testing/src/python.rs new file mode 100644 index 0000000000..e465d5201c --- /dev/null +++ b/crates/testing/src/python.rs @@ -0,0 +1,110 @@ +use std::{ + path::{Path, PathBuf}, + process::Command, +}; + +use wasmer_pack_cli::Language; + +use crate::{ + utils::{self, execute_command}, + TestFailure, +}; + +pub(crate) fn run(script_path: &Path, wapm_dir: &Path, temp_dir: &Path) -> Result<(), TestFailure> { + let dest = temp_dir.join("python"); + tracing::info!("Preparing the python package"); + + utils::generate_bindings(&dest, wapm_dir, Language::Python)?; + + let script_dir = script_path + .parent() + .ok_or(TestFailure::DeterminingScriptDirectory)?; + + let venv_dir = script_dir.join(".venv"); + + if !venv_dir.exists() { + tracing::debug!( + venv = %venv_dir.display(), + "Creating a new virtual environment", + ); + initialize_python_virtual_environment(&venv_dir)?; + } + + let pip = get_executable_from_venv(&venv_dir, "pip"); + + pip_install_generated_bindings(&pip, &dest, script_dir)?; + + let pytest = get_executable_from_venv(&venv_dir, "pytest"); + if !pytest.exists() { + install_python_package(&pip, "pytest")?; + } + + run_test_suite(&pytest, script_path)?; + + Ok(()) +} + +fn run_test_suite(pytest: &Path, script_path: &Path) -> Result<(), TestFailure> { + tracing::info!("Running the test suite"); + let mut cmd = Command::new(pytest); + cmd.arg(script_path); + execute_command(&mut cmd).map_err(TestFailure::TestScript)?; + Ok(()) +} + +fn install_python_package(pip: &Path, package_name: &str) -> Result<(), TestFailure> { + tracing::debug!(package = package_name, "Installing Package"); + + let mut cmd = Command::new(pip); + cmd.arg("install").arg(package_name); + execute_command(&mut cmd).map_err(TestFailure::InstallingDependencies)?; + Ok(()) +} + +fn pip_install_generated_bindings( + pip: &Path, + dest: &Path, + script_dir: &Path, +) -> Result<(), TestFailure> { + tracing::info!( + pip = %pip.display(), + bindings = %dest.display(), + "Installing the bindings", + ); + + // TODO: check if this works on Windows. We might need to invoke pip + // through cmd.exe + let mut cmd = Command::new(pip); + cmd.arg("install") + .arg("-e") + .arg(dest) + .current_dir(script_dir); + execute_command(&mut cmd).map_err(TestFailure::InstallingDependencies)?; + Ok(()) +} + +fn initialize_python_virtual_environment(venv_dir: &Path) -> Result<(), TestFailure> { + let python = if cfg!(windows) { + "python.exe" + } else { + "python3" + }; + + let mut cmd = Command::new(python); + cmd.arg("-m").arg("venv").arg(venv_dir); + + execute_command(&mut cmd).map_err(|e| TestFailure::CreatingVirtualEnvironment { + venv_dir: venv_dir.to_path_buf(), + error: e, + })?; + + Ok(()) +} + +fn get_executable_from_venv(venv_dir: &Path, binary: &str) -> PathBuf { + if cfg!(windows) { + venv_dir.join("Scripts").join(binary).with_extension("exe") + } else { + venv_dir.join("bin").join(binary) + } +} diff --git a/crates/testing/src/utils.rs b/crates/testing/src/utils.rs new file mode 100644 index 0000000000..cacec2b25b --- /dev/null +++ b/crates/testing/src/utils.rs @@ -0,0 +1,107 @@ +use std::{ + io::ErrorKind, + path::{Path, PathBuf}, + process::{Command, Output, Stdio}, +}; + +use wasmer_pack_cli::{Codegen, Language}; + +use crate::{CommandFailed, LoadError, TestFailure}; + +pub(crate) fn execute_command(cmd: &mut Command) -> Result<(), CommandFailed> { + let command = format!("{cmd:?}"); + + tracing::debug!(%command, "Executing"); + + let Output { + status, + stdout, + stderr, + } = cmd + .stderr(Stdio::piped()) + .stdout(Stdio::piped()) + .output() + .map_err(|e| CommandFailed::Spawn { + command: cmd.get_program().to_os_string(), + error: e, + })?; + + if status.success() { + Ok(()) + } else { + return Err(CommandFailed::CompletedUnsuccessfully { + command, + stdout: String::from_utf8_lossy(&stdout).into_owned(), + stderr: String::from_utf8_lossy(&stderr).into_owned(), + exit_code: status.code(), + }); + } +} + +pub(crate) fn compile_rust_to_wapm_package( + manifest_path: &Path, + target_dir: impl AsRef, +) -> Result { + let target_dir = target_dir.as_ref(); + + let mut cmd = Command::new("cargo"); + cmd.arg("wapm") + .arg("--dry-run") + .arg("--manifest-path") + .arg(manifest_path) + .env("CARGO_TARGET_DIR", target_dir); + + if let Some(parent) = manifest_path.parent() { + cmd.current_dir(parent); + } + + execute_command(&mut cmd).map_err(LoadError::CargoWapmFailed)?; + + let wapm_dir = target_dir.join("wapm"); + + let generated_package_dir = + first_dir_in_folder(&wapm_dir).map_err(|e| LoadError::UnableToLocateBindings { + dir: wapm_dir, + error: e, + })?; + + Ok(generated_package_dir) +} + +fn first_dir_in_folder(dir: &Path) -> Result { + let mut entries = dir.read_dir()?; + + let first_item = match entries.next() { + Some(Ok(entry)) => entry.path(), + Some(Err(e)) => return Err(e), + None => todo!(), + }; + + if !first_item.is_dir() { + return Err(std::io::Error::new( + ErrorKind::Other, + format!("Expected \"{}\" to be a directory", first_item.display(),), + )); + } + + Ok(first_item) +} + +pub(crate) fn generate_bindings( + dest: &Path, + wapm_dir: &Path, + lang: Language, +) -> Result<(), TestFailure> { + tracing::info!( + output_dir=%dest.display(), + wapm_dir=%wapm_dir.display(), + language=?lang, + "Generating bindings", + ); + let codegen = Codegen { + out_dir: Some(dest.to_path_buf()), + input: wapm_dir.to_path_buf(), + }; + codegen.run(lang).map_err(TestFailure::BindingsGeneration)?; + Ok(()) +} diff --git a/examples/hello-wasi/Cargo.toml b/examples/hello-wasi/Cargo.toml index cd0aa2de09..e541b782f2 100644 --- a/examples/hello-wasi/Cargo.toml +++ b/examples/hello-wasi/Cargo.toml @@ -13,7 +13,7 @@ wai-bindgen-rust = "0.2.1" [package.metadata.wapm] namespace = "Michael-F-Bryan" abi = "wasi" -bindings = { wai-version = "0.2.0", exports = "hello-wasi.exports.wai" } +bindings = { wai-version = "0.2.0", exports = "hello-wasi.export.wai" } [lib] crate-type = ["cdylib"] diff --git a/examples/hello-wasi/hello-wasi.test.mjs b/examples/hello-wasi/hello-wasi.test.mjs new file mode 100644 index 0000000000..e131567178 --- /dev/null +++ b/examples/hello-wasi/hello-wasi.test.mjs @@ -0,0 +1,9 @@ +import { bindings } from "@michael-f-bryan/hello-wasi"; + +async function main() { + const wasm = await bindings.hello_wasi(); + wasm.printHelloWasi(); + console.log("Done!"); +} + +main(); diff --git a/examples/hello-wasi/package.json b/examples/hello-wasi/package.json new file mode 100644 index 0000000000..26e9d952f7 --- /dev/null +++ b/examples/hello-wasi/package.json @@ -0,0 +1,9 @@ +{ + "name": "hello-wasi", + "version": "1.0.0", + "main": "index.js", + "license": "MIT", + "dependencies": { + "@michael-f-bryan/hello-wasi": "/home/consulting/Documents/wasmer/wasmer-pack/target/tmp/javascript/package" + } +} diff --git a/examples/hello-wasi/test.py b/examples/hello-wasi/test.py new file mode 100644 index 0000000000..b5f9aea5bb --- /dev/null +++ b/examples/hello-wasi/test.py @@ -0,0 +1,10 @@ +import hello_wasi + + +def test_expected_items_are_generated(): + # Top-level items + assert callable(hello_wasi.bindings.hello_wasi) + + # Our WebAssembly library + wasm = hello_wasi.bindings.hello_wasi() + assert callable(wasm.print_hello_wasi) diff --git a/examples/hello-wasi/yarn.lock b/examples/hello-wasi/yarn.lock new file mode 100644 index 0000000000..85b207bc6c --- /dev/null +++ b/examples/hello-wasi/yarn.lock @@ -0,0 +1,18 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@michael-f-bryan/hello-wasi@/home/consulting/Documents/wasmer/wasmer-pack/target/tmp/javascript/package": + version "0.1.0" + dependencies: + "@wasmer/wasi" "^1.2.2" + +"@michael-f-bryan/hello-wasi@file:../../target/tmp/javascript/package": + version "0.1.0" + dependencies: + "@wasmer/wasi" "^1.2.2" + +"@wasmer/wasi@^1.2.2": + version "1.2.2" + resolved "https://registry.yarnpkg.com/@wasmer/wasi/-/wasi-1.2.2.tgz#46689b568100ad3923fb1bee0205ccb01f9c5b80" + integrity sha512-39ZB3gefOVhBmkhf7Ta79RRSV/emIV8LhdvcWhP/MOZEjMmtzoZWMzt7phdKj8CUXOze+AwbvGK60lKaKldn1w== diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml new file mode 100644 index 0000000000..ae6edeaeb5 --- /dev/null +++ b/integration-tests/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "integration-tests" +version = "0.0.0" +edition = "2021" +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +anyhow = "1.0.66" +wasmer-pack-testing = { version = "0.1.0", path = "../crates/testing" } + +[dev-dependencies] +tracing-subscriber = { version = "0.3.16", features = ["env-filter"] } diff --git a/integration-tests/src/lib.rs b/integration-tests/src/lib.rs new file mode 100644 index 0000000000..8b13789179 --- /dev/null +++ b/integration-tests/src/lib.rs @@ -0,0 +1 @@ + diff --git a/integration-tests/tests/examples.rs b/integration-tests/tests/examples.rs new file mode 100644 index 0000000000..fb68b5e1e8 --- /dev/null +++ b/integration-tests/tests/examples.rs @@ -0,0 +1,53 @@ +use anyhow::Context; +use std::path::Path; +use tracing_subscriber::EnvFilter; +use wasmer_pack_testing::TestEnvironment; + +fn initialize_logging() { + if std::env::var_os("RUST_LOG").is_none() { + std::env::set_var("RUST_LOG", "debug"); + } + + let _ = tracing_subscriber::fmt() + .with_test_writer() + .with_env_filter(EnvFilter::from_default_env()) + .try_init(); +} + +#[test] +fn hello_wasi_python() { + initialize_logging(); + + let hello_wasi_dir = project_root().join("examples").join("hello-wasi"); + let cargo_toml = hello_wasi_dir.join("Cargo.toml"); + + let env = TestEnvironment::for_crate(cargo_toml, env!("CARGO_TARGET_TMPDIR")) + .context("Unable to initialize the test environment") + .unwrap(); + env.python(hello_wasi_dir.join("test.py")) + .context("Python test") + .unwrap(); +} + +#[test] +#[ignore = "JavaScript tests aren't fully implemented yet"] +fn hello_wasi_javascript() { + initialize_logging(); + + let hello_wasi_dir = project_root().join("examples").join("hello-wasi"); + let cargo_toml = hello_wasi_dir.join("Cargo.toml"); + + let env = TestEnvironment::for_crate(cargo_toml, env!("CARGO_TARGET_TMPDIR")) + .context("Unable to initialize the test environment") + .unwrap(); + env.javascript(hello_wasi_dir.join("hello-wasi.test.mjs")) + .context("JavaScript test") + .unwrap(); +} + +fn project_root() -> &'static Path { + let crate_dir = Path::new(env!("CARGO_MANIFEST_DIR")); + let project_root = crate_dir.ancestors().nth(1).unwrap(); + assert!(project_root.join(".git").exists()); + project_root +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000000..845ae54621 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,5 @@ +[toolchain] +components = ["rustfmt", "clippy"] +targets = ["wasm32-unknown-unknown", "wasm32-wasi"] +# MSRV - Required for [workspace.package] +channel = "1.64"