Skip to content

Commit

Permalink
Remove rusty_fork in favor of handrolled solution
Browse files Browse the repository at this point in the history
This commit changes up how the assay macro works by forking the code
into it's Command process instead of using rusty_fork which did the same
thing under the hood. rusty_fork is both outdated and not the most
ergonomic to work with. With these changes our handrolled solution is a
bit easier to work with as a library author and makes it easier for us
to make sure tests fail the way we expect and capture that output
properly. In order to support this feature we also:

- Added support for the ignore attribute in assay
- Added a python script to run the main test suite and two tests we
  expect to fail so that we know the Command forking actually works
- Upgraded the dependencies to more modern versions since it has been
  2 years since the last commit

Closes #12
Closes #3
  • Loading branch information
mgattozzi committed Nov 22, 2024
1 parent ff4afc5 commit 1d7188c
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 38 deletions.
9 changes: 4 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,10 @@ categories = ["development-tools", "development-tools::testing"]

[dependencies]
assay-proc-macro = { path = "assay-proc-macro", version = "0.1.0", default-features = false }
async-std = { version = "^1.10.0", optional = true }
pretty_assertions_sorted = "^1.0.0"
rusty-fork = "^0.3.0"
tempfile = "3.2.0"
tokio = { version = "^1.16.0", features = ["rt-multi-thread"], optional = true }
async-std = { version = "^1.13.0", optional = true }
pretty_assertions_sorted = "^1.2.3"
tempfile = "3.14.0"
tokio = { version = "^1.41.1", features = ["rt-multi-thread"], optional = true }

[workspace]
members = ["assay-proc-macro"]
Expand Down
73 changes: 42 additions & 31 deletions assay-proc-macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use syn::{

struct AssayAttribute {
include: Option<Vec<String>>,
ignore: bool,
should_panic: bool,
env: Option<Vec<(String, String)>>,
setup: Option<Expr>,
Expand All @@ -24,6 +25,7 @@ struct AssayAttribute {
impl Parse for AssayAttribute {
fn parse(input: ParseStream) -> Result<Self> {
let mut include = None;
let mut ignore = false;
let mut should_panic = false;
let mut env = None;
let mut setup = None;
Expand Down Expand Up @@ -55,6 +57,7 @@ impl Parse for AssayAttribute {
);
}
"should_panic" => should_panic = true,
"ignore" => ignore = true,
"env" => {
let _: Token![=] = input.parse()?;
let array: ExprArray = input.parse()?;
Expand Down Expand Up @@ -96,6 +99,7 @@ impl Parse for AssayAttribute {

Ok(AssayAttribute {
include,
ignore,
should_panic,
env,
setup,
Expand Down Expand Up @@ -125,6 +129,12 @@ pub fn assay(attr: TokenStream, item: TokenStream) -> TokenStream {
}
};

let ignore = if attr.ignore {
quote! { #[ignore] }
} else {
quote! {}
};

let should_panic = if attr.should_panic {
quote! { #[should_panic] }
} else {
Expand Down Expand Up @@ -177,29 +187,17 @@ pub fn assay(attr: TokenStream, item: TokenStream) -> TokenStream {
let expanded = quote! {
#[test]
#should_panic
#ignore
#vis #sig {
fn modify(_: &mut std::process::Command) {}

fn parent(child: &mut assay::ChildWrapper, _: &mut std::fs::File) {
let child = child.wait().unwrap();
if !child.success() {
panic!("Assay test failed")
}
}

fn child() {
#[allow(unreachable_code)]
if let Err(e) = || -> Result<(), Box<dyn std::error::Error>> {
use assay::{assert_eq, assert_eq_sorted, assert_ne};
#include
#setup
#env
#body
#teardown
Ok(())
}() {
panic!("Error: {}", e);
}
#[allow(unreachable_code)]
fn child() -> Result<(), Box<dyn std::error::Error>> {
use assay::{assert_eq, assert_eq_sorted, assert_ne};
#include
#setup
#env
#body
#teardown
Ok(())
}

if std::env::var("NEXTEST_EXECUTION_MODE")
Expand All @@ -208,7 +206,7 @@ pub fn assay(attr: TokenStream, item: TokenStream) -> TokenStream {
.map(|s| s.as_str() == "process-per-test")
.unwrap_or(false)
{
child();
child().unwrap();
} else {
let name = {
let mut module = module_path!()
Expand All @@ -219,14 +217,27 @@ pub fn assay(attr: TokenStream, item: TokenStream) -> TokenStream {
module.push(stringify!(#name));
module.join("::")
};

assay::fork(
&name,
assay::rusty_fork_id!(),
modify,
parent,
child
).expect("We forked the test using assay");
if std::env::var("ASSAY_SPLIT")
.as_ref()
.map(|s| s.as_str() != "1")
.unwrap_or(true)
{
let mut args = std::env::args().collect::<Vec<String>>();
if !args.contains(&name) {
args.push(name.clone());
}
let out = std::process::Command::new(&args[0])
.args(if args.len() == 1 { &[] } else { &args[1..] })
.env("ASSAY_SPLIT", "1")
.output()
.expect("executed a subprocess");
let stdout = String::from_utf8(out.stdout).unwrap();
if stdout.contains(&format!("{name} - should panic ... ok")) || stdout.contains(&format!("{name} ... FAILED")) {
panic!();
}
} else{
child().unwrap();
}
}
}
};
Expand Down
2 changes: 0 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@
pub use assay_proc_macro::assay;
#[doc(hidden)]
pub use pretty_assertions_sorted::{assert_eq, assert_eq_sorted, assert_ne};
#[doc(hidden)]
pub use rusty_fork::{fork, rusty_fork_id, rusty_fork_test_name, ChildWrapper};

use std::{
env,
Expand Down
27 changes: 27 additions & 0 deletions test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#!/usr/bin/env -S uv run

# /// script
# requires-python = ">=3.12"
# dependencies = ["termcolor == 2.5"]
# ///

from subprocess import run, DEVNULL
from sys import exit
from termcolor import colored

passing_tests = run(["cargo", "test", "--workspace"])
failing_tests = run(
["cargo", "test", "--workspace", "--", "--ignored"], stdout=DEVNULL, stderr=DEVNULL
)

passing_tests.check_returncode()
if failing_tests.returncode == 0:
print(
colored("ERROR: ", "red")
+ "Ignored tests failed to fail properly. Forking of assay processes is broken somehow"
)
print(
colored("HINT: ", "cyan")
+ "run the tests with 'cargo test --workspace -- --ignored' to see what failed"
)
exit(1)
25 changes: 25 additions & 0 deletions tests/should_fail.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright (C) 2021 Michael Gattozzi <[email protected]>
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/

//! Inverted tests that we *want* to panic or pass. Given assay causes a test
//! to spawn itself we need to make sure these tests can actually properly fail.
//! We run these as part of a script to actually call them. Mainly because these
//! test that the assay macro works and if a test fails or does not it works as
//! expected. These are all ignored by default and must be explicitly called for
//! if we want them to run. This is because we expect these to fail and so we need
//! to run commands to check for a failure output for CI purposes
use assay::assay;

#[assay(ignore)]
fn should_panic_and_cause_a_failure_case() {
panic!()
}

#[assay(ignore, should_panic)]
fn should_not_panic_and_cause_a_failure_case() {}

0 comments on commit 1d7188c

Please sign in to comment.