-
Notifications
You must be signed in to change notification settings - Fork 11
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
The `GhcidNg` type copies a given directory to a tempdir and then launches `ghcid-ng` there. It also spawns an async task to read JSON log events from the log file. `GhcidNg` uses thread-local storage to determine which version of `ghc` to use when launching tests, and will error appropriately if the thread-local storage is not set. This takes advantage of the fact that async tests run in the `tokio` "current-thread" runtime by default, which schedules all tasks in the test on the same thread. The second part is a proc macro attribute exported as `#[test_harness::test]`, which rewrites tests so that 1. The tests are async functions which run under the default `#[tokio::test]` current-thread runtime. 2. Tracing is set up in the tests so that log messages from the `test-harness` library are visible in the test output. 3. The appropriate thread-local variables are set for each test (this includes the current GHC version). 4. One test is generated for each GHC version. 5. If tests fail, the relevant `ghcid-ng` logs are saved to a directory under `target/` and the path is printed at the end of the tests. Here's a sample test from #44 using this test harness: ```rust /// Test that `ghcid-ng` can start up and then reload on changes. #[test] async fn can_reload() { let mut session = GhcidNg::new("tests/data/simple") .await .expect("ghcid-ng starts"); session .wait_until_ready() .await .expect("ghcid-ng loads ghci"); fs::append(session.path("src/MyLib.hs"), "\n\nhello = 1\n") .await .unwrap(); session .wait_until_reload() .await .expect("ghcid-ng reloads on changes"); session .get_log( Matcher::span_close() .in_module("ghcid_ng::ghci") .in_spans(["on_action", "reload"]), ) .await .expect("ghcid-ng finishes reloading"); } ``` --------- Co-authored-by: Gabriella Gonzalez <gabriella@mercury.com>
- Loading branch information
1 parent
55e616b
commit 5c1f1b5
Showing
13 changed files
with
776 additions
and
26 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,6 +3,7 @@ | |
members = [ | ||
"ghcid-ng", | ||
"test-harness", | ||
"test-harness-macro", | ||
] | ||
|
||
resolver = "2" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
[package] | ||
name = "test-harness-macro" | ||
version = "0.1.0" | ||
edition = "2021" | ||
|
||
description = "Test attribute for ghcid-ng" | ||
|
||
publish = false | ||
|
||
[lib] | ||
proc-macro = true | ||
|
||
[dependencies] | ||
quote = "1.0.33" | ||
syn = { version = "2.0.29", features = ["full"] } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
use proc_macro::TokenStream; | ||
|
||
use quote::quote; | ||
use quote::ToTokens; | ||
use syn::parse; | ||
use syn::parse::Parse; | ||
use syn::parse::ParseStream; | ||
use syn::Attribute; | ||
use syn::Block; | ||
use syn::Ident; | ||
use syn::ItemFn; | ||
|
||
/// Runs a test asynchronously in the `tokio` current-thread runtime with `tracing` enabled. | ||
/// | ||
/// One test is generated for each GHC version listed in the `$GHC_VERSIONS` environment variable | ||
/// at compile-time. | ||
#[proc_macro_attribute] | ||
pub fn test(_attr: TokenStream, item: TokenStream) -> TokenStream { | ||
// Parse annotated function | ||
let mut function: ItemFn = parse(item).expect("Could not parse item as function"); | ||
|
||
// Add attributes to run the test in the `tokio` current-thread runtime and enable tracing. | ||
function.attrs.extend( | ||
parse::<Attributes>( | ||
quote! { | ||
#[tokio::test] | ||
#[tracing_test::traced_test] | ||
#[allow(non_snake_case)] | ||
} | ||
.into(), | ||
) | ||
.expect("Could not parse quoted attributes") | ||
.0, | ||
); | ||
|
||
let ghc_versions = match option_env!("GHC_VERSIONS") { | ||
None => { | ||
panic!("`$GHC_VERSIONS` should be set to a list of GHC versions to run tests under, separated by spaces, like `9.0.2 9.2.8 9.4.6 9.6.2`."); | ||
} | ||
Some(versions) => versions.split_ascii_whitespace().collect::<Vec<_>>(), | ||
}; | ||
|
||
// Generate functions for each GHC version we want to test. | ||
let mut ret = TokenStream::new(); | ||
for ghc_version in ghc_versions { | ||
ret.extend::<TokenStream>( | ||
make_test_fn(function.clone(), ghc_version) | ||
.to_token_stream() | ||
.into(), | ||
); | ||
} | ||
ret | ||
} | ||
|
||
struct Attributes(Vec<Attribute>); | ||
|
||
impl Parse for Attributes { | ||
fn parse(input: ParseStream) -> syn::Result<Self> { | ||
Ok(Self(input.call(Attribute::parse_outer)?)) | ||
} | ||
} | ||
|
||
fn make_test_fn(mut function: ItemFn, ghc_version: &str) -> ItemFn { | ||
let ghc_version_ident = ghc_version.replace('.', ""); | ||
let stmts = function.block.stmts; | ||
let test_name_base = function.sig.ident.to_string(); | ||
let test_name = format!("{test_name_base}_{ghc_version_ident}"); | ||
function.sig.ident = Ident::new(&test_name, function.sig.ident.span()); | ||
|
||
// Wrap the test code in startup/cleanup code. | ||
let new_body = parse::<Block>( | ||
quote! { | ||
{ | ||
::test_harness::internal::wrap_test( | ||
async { | ||
#(#stmts);* | ||
}, | ||
#ghc_version, | ||
#test_name, | ||
env!("CARGO_TARGET_TMPDIR"), | ||
).await; | ||
} | ||
} | ||
.into(), | ||
) | ||
.expect("Could not parse function body"); | ||
|
||
// Replace function body | ||
*function.block = new_body; | ||
|
||
function | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.