Repository navigation
Description
This is tracking the #[bench] and #[test_case] attributes and the custom_test_frameworks lang item as well as the libtest crate. There is currently no plan/RFC for stabilizing them.
Old issue description.
Details
This is a tracking issue for the eRFC "Custom test frameworks" (rust-lang/rfcs#2318).
Documentation:
- Implementation proposal: https://www.jrenner.net/rust/testing/2018/08/06/custom-test-framework-prop.html
- https://doc.rust-lang.org/nightly/unstable-book/language-features/custom-test-frameworks.html
Steps:
- Implement the RFC (cc @rust-lang/dev-tools @rust-lang/cargo -- can anyone write up mentoring instructions?)
- Adjust documentation (see instructions on forge)
- Stabilization PR (see instructions on forge)
Unresolved questions:
- Integration with doctests
- Standardizing the output
- Namespacing
- Runtime dependencies and flags
- Naming
- Bencher
- Cargo integration
Notes:
- Destabilization of
#[bench]: [1.38]#[bench]accepted on stable but not beta #63798 (comment)
Implementation history:
- Initial implementation: Introduce Custom Test Frameworks #53410
I went to emphasize this real quick.
From what I understand of eRFC 2318 is that it exposes some of the libtest and compiler machinery to end users. Most of these mechanisms are wanted outside of testing and this is fairly restrictive in terms of what can be done for testing. The current thought is to explore custom test harnesses through Cargo's harness = false (like libtest-mimic), see what more general features are needed, and iterate from there.
For example, maybe test discovery can instead be done through externally implementable items (#125418)
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsNo status
Activity
CAD97 commentedon May 14, 2018
On stdout/err capture (after doing some research around the proof-of-concept #50457):
The current state is sub-optimal, to say the least. Both
stdoutandstderrexist as thread-local handles that only the stdlib has access to, whichprintfamily of macros use, as doespanic.Relevant libstd snippets
rust/src/libstd/io/stdio.rs
Lines 23 to 27 in 935a2f1
rust/src/libstd/panicking.rs
Lines 38 to 42 in 935a2f1
rust/src/libstd/io/stdio.rs
Lines 614 to 711 in 935a2f1
The existing stdout/err capture for the test harness works by setting the thread-local, which, if set, the
printfamily of macros will use, otherwise falling back toio::stdout(). This setup is problematic because it's fairly trivial to escape this (#12309) and 99.9% of logging frameworks will escape accidentally (#40298).The reason for this is that any solution that's generic over
io::Writeis going to useio::stdout()as the sink to hit stdout, and that doesn't go through the tread-local plumbing, so hits the true stdout directly.The way I see it, there are three ways to resolve this quirk:
io::Writesink that uses the thread-local plumbing that theprintmacros do today, and implement theprintmacros as writing to that sink. That's the approach I proved was possible with Reusable handle to thread-local replaceable stdout/err #50457.io::Writesink that uses (e)print!behind the scenes. This is possible entirely in an external crate:(
E)PrintWriter- feel free to use these, I am the author and release them under MIT/UnlicenseUnfortunately, this suffers from a large number of drawbacks. Because we only have access to the
printmacro, we have to interpret whatever sequence of bytes the sink gets as a UTF-8 string, even though that's potentially not true, etc. etc. not zero-cost when it's not, and so on. It's a decent solution for hooking up logging in tests, but is very much not a general solution that could be used outside tests.That takes us to the third option, which is almost certainly the best, but also the most complicated:
It sounds simple, but it's a lot simpler said than done with the current test harness setup.
io::Stderrboils down to a handle to a write stream available to the process, and is a process-local resource -- there's only one stdout per process. The test harness currently runs tests parallel on different threads within the same process, thus the current thread-local replacement.The other issue is that the test harness doesn't just want to capture stdout of the tests, it wants to capture stdout of the tests separately for each test, so it can print just the stdout of the failing test(s) and not have the other tests' output interspersed with it.
The only solution I see at this time that maintains current behavior of separate captures per test run is to run them all in their own process, and capture stdout that way. That way each process can pipe its stdout/err to the test runner, which can then synchronize them and decide which ones to output to the true stdout/err if desired.
A sketch of the plumbing required to make an out-of-tree libtest that works with this behavior:
#[test] fnare all collected into a listfn mainis generated that takes an argument of one of the paths, and runs that testfn mainis hidden)Note that this is unfortunately stateful: we need to fold over all
#[test]fn before we can emit the propermain. I'm not knowledgeable enough around compiler internals to be much help implementing the machinery right now (though I'd love to learn my way around and help out!), but give me an API to build against and I'll definitely help with any test frameworks built against the plumbing the eRFC has us set to experiment with.CAD97 commentedon May 14, 2018
TL;DR:
To have sane capture behavior for stdout and stderr for
print-family macros,panic, andio::{stdout, stderr}, running each test in its own (child) process is probably required. I'd love to help out wherever I can, but lack confidence.retep998 commentedon May 15, 2018
This would be significantly slower on certain platforms where process creation is slow, notably Windows. Especially when the tests are all very quick this will cause severe regressions in total time.
CAD97 commentedon May 15, 2018
Another possibility for process separation of tests is to spawn a process for each unit of parallelism, but each process is reused for multiple tests.
Of course, we could also just promote the thread-local replaceable stdout/err to
io::Stdout/Err, but that would be suboptimal itself.The issue is that stdout is, as far as I can tell, a process-local resource. To capture
io::Stdoutwe either need our own stdout replacement machinery on top or to separate processes.SoniEx2 commentedon Jun 13, 2018
if I want to argue about this do I argue about it here?
so like I made
hexchat-plugincrate. the problem is you can't test hexchat side without hexchat. so I'd need some sort of#[hexchat_test].but I still want the users to be able to use plain old vanilla rust
#[test]s. I shouldn't have to reimplement/replace the whole testing system just to be able to produce some hexchat-based unit tests (in addition to normal tests).(disclaimer: I don't fully understand the RFC, but it seems to require either replacing everything, or not having tests)
jonhoo commentedon Jun 13, 2018
@SoniEx2 this RFC is about how we'd like to change the underlying mechanism that is used to arrange testing in Rust so that authors can write new test frameworks (e.g., quickcheck) that have access to the same mechanisms as existing built-in tests ( able to access private functions, can find all tagged functions, etc.). The current proposal is relatively wholesale (you change the test framework for a whole crate), but this is something that may be improved upon. We decided against more sophisticated designs in the initial implementation phase (this is just an "experimental" RFC) so that we don't bite off more than we can chew, but I think down the line we'd want more fine-grained test framework selection too.
As for your specific example, you don't actually need custom frameworks for that feature :) It seems like you want to only run a test if hexchat isn't running? To do that, you could either mark the test with
#[allow_fail], or you could have the test justreturnif it fails to connect. If you wanted a more fine-grained approach, you could write a procedural macro that re-writes any function annotated with#[hexchat_test]into a#[test]that returns if hexchat isn't running.SoniEx2 commentedon Jun 13, 2018
no you don't understand, they have to run inside hexchat. loaded with dlopen (or equivalent). after plugin registration.
djrenren commentedon Aug 7, 2018
Hey all, I've come up with a simplified proposal and I have an implementation now (though it needs a little cleaning up before a PR).
https://blog.jrenner.net/rust/testing/2018/08/06/custom-test-framework-prop.html
Lemme know what you think!
CAD97 commentedon Aug 21, 2018
I've thought some more about stdin/stderr capture. Here's a draft of a system that should be able to "perfectly" capture stdin/stdout, though not without O(1) (once per test suite) overhead:
cargo test. It does no actual testing; its job is to coordinate child processes to do the actual tests.jonhoo commentedon Aug 21, 2018
@djrenren finally had a chance to read your proposal. I'm excited by the progress on this, though your write-up did raise some questions for me:
const,static, orfn.". Further down, when you discuss the use-case of a framework that supports test suites, you show a#[test_suite]annotation on amodwith#[suite_member]on the nested tests. It'd be good to show how that expansion works. Does a proc macro turn#[test_suite]into aconstwith an element for each#[suite_member]? How does it find all of them? Are the#[suite_member]s all turned into#[test_case]? How do you avoid double-executing those test cases (once as a "child" and once due to the#[test-case]annotation)?#![test_runner]you say "The type of the function must beFn(&[&mut Foo]) -> impl Terminationfor someFoowhich is the test type", yet further down you write aFn(&[&dyn Foo]). I assume the latter is the correct version?TestDescAndFnsufficient for our purposes here? I don't think that can (currently) wrap astructorconstin a meaningful way?121 remaining items
Rollup merge of rust-lang#134273 - RalfJung:de-stabilize-bench, r=ibr…
Unrolled build for rust-lang#134273
Rollup merge of rust-lang#134273 - RalfJung:de-stabilize-bench, r=ibr…
mrchantey commentedon Dec 16, 2025
If I understand correctly the issue of doctests being excluded from custom test frameworks would be resolved by #141083 because in this case they get converted to a regular
#[test]in the host crate.@fmease can you confirm this?
Rollup merge of rust-lang#152210 - Ozzy1423:attrs7, r=JonathanBrouwer
Unrolled build for #152210
Rollup merge of #152210 - Ozzy1423:attrs7, r=JonathanBrouwer
Rollup merge of #152210 - Ozzy1423:attrs7, r=JonathanBrouwer
Rollup merge of #152210 - Ozzy1423:attrs7, r=JonathanBrouwer
Rollup merge of #152210 - Ozzy1423:attrs7, r=JonathanBrouwer
Rollup merge of #152210 - Ozzy1423:attrs7, r=JonathanBrouwer
Rollup merge of #152210 - Ozzy1423:attrs7, r=JonathanBrouwer
epage commentedon Nov 2, 2026
I went to emphasize this real quick.
From what I understand of eRFC 2318 is that it exposes some of the libtest and compiler machinery to end users. Most of these mechanisms are wanted outside of testing and this is fairly restrictive in terms of what can be done for testing. The current thought is to explore custom test harnesses through Cargo's
harness = false(likelibtest-mimic), see what more general features are needed, and iterate from there.For example, maybe test discovery can instead be done through externally implementable items (#125418)