test-casing-0.1.3/.cargo_vcs_info.json0000644000000001410000000000100132520ustar { "git": { "sha1": "9c6ba1fff5de67800e1191a0b3e581ae1eac03ac" }, "path_in_vcs": "lib" }test-casing-0.1.3/CHANGELOG.md000064400000000000000000000014340072674642500137110ustar 00000000000000# Changelog All notable changes to this project will be documented in this file. The project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## [Unreleased] ## 0.1.3 - 2024-03-03 ### Fixed - Fix `clippy::no_effect_underscore_binding` lint triggered by the generated code in Rust 1.76+. ## 0.1.2 - 2023-11-02 ### Fixed - Fix `unused_must_use` lint triggered for async functions without the explicit return value after the previous fix. - Pin a version of the macro dependency in the main library so that it does not break in the future releases. ## 0.1.1 - 2023-10-08 ### Fixed - Fix `ignored_unit_patterns` Clippy lint triggered by the generated code in Rust 1.73+. ## 0.1.0 - 2023-06-03 The initial release of `test-casing`. test-casing-0.1.3/Cargo.toml0000644000000025320000000000100112560ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.65" name = "test-casing" version = "0.1.3" authors = ["Alex Ostrovski "] description = "Parameterized test cases and test decorators" readme = "README.md" keywords = [ "testing", "parameterized", "case", "decorator", ] categories = ["development-tools::testing"] license = "MIT OR Apache-2.0" repository = "https://github.com/slowli/test-casing" [dependencies.once_cell] version = "1.19.0" optional = true [dependencies.test-casing-macro] version = "=0.1.3" [dev-dependencies.async-std] version = "1.12.0" features = ["attributes"] [dev-dependencies.doc-comment] version = "0.3.3" [dev-dependencies.rand] version = "0.8.5" [dev-dependencies.trybuild] version = "1.0.89" [dev-dependencies.version-sync] version = "0.9.4" [features] default = [] nightly = [ "test-casing-macro/nightly", "once_cell", ] test-casing-0.1.3/Cargo.toml.orig000064400000000000000000000016060072674642500147700ustar 00000000000000[package] name = "test-casing" version.workspace = true edition.workspace = true rust-version.workspace = true authors.workspace = true license.workspace = true repository.workspace = true readme = "README.md" keywords = ["testing", "parameterized", "case", "decorator"] categories = ["development-tools::testing"] description = "Parameterized test cases and test decorators" [dependencies] once_cell = { workspace = true, optional = true } test-casing-macro = { version = "=0.1.3", path = "../macro" } [dev-dependencies] async-std.workspace = true doc-comment.workspace = true rand.workspace = true trybuild.workspace = true version-sync.workspace = true [features] default = [] # Uses custom test frameworks APIs together with a generous spicing of hacks # to include arguments in the names of the generated tests. nightly = ["test-casing-macro/nightly", "once_cell"] test-casing-0.1.3/LICENSE-APACHE000064400000000000000000000254460072674642500140350ustar 00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.test-casing-0.1.3/LICENSE-MIT000064400000000000000000000021120072674642500135260ustar 00000000000000Copyright 2022-current Developers of test-casing Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. test-casing-0.1.3/README.md000064400000000000000000000155200072674642500133600ustar 00000000000000# Parameterized Rust Tests & Test Decorators [![Build Status](https://github.com/slowli/test-casing/workflows/CI/badge.svg?branch=main)](https://github.com/slowli/test-casing/actions) [![License: MIT OR Apache-2.0](https://img.shields.io/badge/License-MIT%2FApache--2.0-blue)](https://github.com/slowli/test-casing#license) ![rust 1.65+ required](https://img.shields.io/badge/rust-1.65+-blue.svg?label=Required%20Rust) **Documentation:** [![Docs.rs](https://docs.rs/test-casing/badge.svg)](https://docs.rs/test-casing/) [![crate docs (main)](https://img.shields.io/badge/main-yellow.svg?label=docs)](https://slowli.github.io/test-casing/test_casing/) `test-casing` is a minimalistic Rust framework for generating tests for a given set of test cases and decorating them to add retries, timeouts, sequential test processing etc. In other words, the framework implements: - Parameterized tests of reasonably low cardinality for the standard Rust test runner - Fully code-based, composable and extensible test decorators. Since a separate test wrapper is generated for each case, their number should be reasonably low (roughly speaking, no more than 20). Isolating each test case makes most sense if the cases involve some heavy lifting (spinning up a runtime, logging considerable amount of information, etc.). ## Usage Add this to your `Crate.toml`: ```toml [dev-dependencies] test-casing = "0.1.3" ``` ### Examples: test cases ```rust use test_casing::{cases, test_casing, TestCases}; use std::error::Error; #[test_casing(4, [2, 3, 5, 8])] fn numeric_test(number: i32) { assert!(number < 10); } // Cases can be extracted to a constant for better readability. const CASES: TestCases<(String, i32)> = cases! { [2, 3, 5, 8].map(|i| (i.to_string(), i)) }; #[test_casing(4, CASES)] fn parsing_number( #[map(ref)] s: &str, // ^ specifies that argument should be borrowed from `String` // returned by the `CASES` iterator expected: i32, ) -> Result<(), Box> { assert_eq!(s.parse::()?, expected); Ok(()) } ``` Other features include the support of async tests and `ignore` / `should_panic` attributes (the latter are applied to all generated cases). ```rust use test_casing::test_casing; #[test_casing(4, [2, 3, 5, 8])] #[async_std::test] // ^ test attribute should be specified below the case spec async fn test_async(number: i32) { assert!(number < 10); } #[test_casing(3, ["not", "a", "number"])] #[should_panic(expected = "ParseIntError")] fn parsing_number_errors(s: &str) { s.parse::().unwrap(); } ``` ### Examples: test decorators ```rust use test_casing::{ decorate, test_casing, decorators::{Retry, Sequence, Timeout}, }; #[test] #[decorate(Retry::times(3), Timeout::secs(3))] fn test_with_retry_and_timeouts() { // Test logic } static SEQUENCE: Sequence = Sequence::new().abort_on_failure(); // Execute all test cases sequentially and abort if one of them fails. #[test_casing(4, [2, 3, 5, 8])] #[async_std::test] #[decorate(&SEQUENCE)] async fn test_async(number: i32) { assert!(number < 10); } ``` See the crate docs for more examples of usage. ### Descriptive test case names With the help of [custom test frameworks] APIs and a generous spicing of hacks, the names of generated tests include the values of arguments provided to the targeted test function if the `nightly` crate feature is enabled. As the name implies, the feature only works on the nightly Rust. Here's an excerpt of the output of integration tests in this crate to illustrate: ```text test cartesian_product::case_6 [number = 5, s = "first"] ... ok test cartesian_product::case_9 [number = 8, s = "first"] ... ok test number_can_be_converted_to_string::case_0 [number = 2, expected = "2"] ... ok test number_can_be_converted_to_string::case_1 [number = 3, expected = "3"] ... ok test number_can_be_converted_to_string::case_2 [number = 5, expected = "5"] ... ok test number_can_be_converted_to_string_with_tuple_input::case_0 [(arg 0) = (2, "2")] ... ok test number_can_be_converted_to_string_with_tuple_input::case_1 [(arg 0) = (3, "3")] ... ok test number_can_be_converted_to_string_with_tuple_input::case_2 [(arg 0) = (5, "5")] ... ok test numbers_are_large::case_0 [number = 2] ... ignored, testing that `#[ignore]` attr works test numbers_are_large::case_1 [number = 3] ... ignored, testing that `#[ignore]` attr works test string_conversion_fail::case_0 [bogus_str = "not a number"] - should panic ... ok test string_conversion_fail::case_1 [bogus_str = "-"] - should panic ... ok test string_conversion_fail::case_2 [bogus_str = ""] - should panic ... ok test unit_test_detection_works ... ok ``` The arguments are a full-fledged part of test names, meaning that they can be included into test filters (like `cargo test 'number = 3'`) etc. ## Alternatives and similar tools - The approach from this crate can be reproduced with some amount of copy-pasting by manually feeding necessary inputs to a common parametric testing function. Optionally, these tests may be collected in a module for better structuring. The main downside of this approach is the amount of copy-pasting. - Alternatively, multiple test cases may be run in a single `#[test]` (e.g., in a loop). This is fine for the large amount of small cases (e.g., mini-fuzzing), but may have downsides such as overflowing or overlapping logs and increased test runtimes. - The [`test-case`] crate uses a similar approach to test case structuring, but differs in how test case inputs are specified. Subjectively, the approach used by this crate is more extensible and easier to read. - [Property testing] / [`quickcheck`]-like frameworks provide a much more exhaustive approach to parameterized testing, but they require significantly more setup effort. - [`rstest`] supports test casing and some test decorators (e.g., timeouts). - [`nextest`] is an alternative test runner that supports most of the test decorators defined by this library. It does not use a code-based decorator config and does not allow for custom decorators. Tests produced with this library can be run by `cargo nextest`. ## License Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or [MIT license](LICENSE-MIT) at your option. Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in `test-casing` by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions. [custom test frameworks]: https://github.com/rust-lang/rust/issues/50297 [`test-case`]: https://crates.io/crates/test-case [Property testing]: https://crates.io/crates/proptest [`quickcheck`]: https://crates.io/crates/quickcheck [`rstest`]: https://crates.io/crates/rstest [`nextest`]: https://nexte.st/ test-casing-0.1.3/src/decorators.rs000064400000000000000000000455330072674642500154120ustar 00000000000000//! Test decorator trait and implementations. //! //! # Overview //! //! A [test decorator](DecorateTest) takes a [tested function](TestFn) and calls it zero or more times, //! perhaps with additional logic spliced between calls. Examples of decorators include [retries](Retry), //! [`Timeout`]s and test [`Sequence`]s. //! //! Decorators are composable: `DecorateTest` is automatically implemented for a tuple with //! 2..=8 elements where each element implements `DecorateTest`. The decorators in a tuple //! are applied in the order of their appearance in the tuple. //! //! # Examples //! //! See [`decorate`](crate::decorate) macro docs for the examples of usage. use std::{ any::Any, fmt, panic, sync::{ mpsc::{self, RecvTimeoutError}, Mutex, PoisonError, }, thread, time::Duration, }; /// Tested function or closure. /// /// This trait is automatically implemented for all functions without arguments. pub trait TestFn: Fn() -> R + panic::UnwindSafe + Send + Sync + Copy + 'static {} impl TestFn for F where F: Fn() -> R + panic::UnwindSafe + Send + Sync + Copy + 'static {} /// Test decorator. /// /// See [module docs](index.html#overview) for the extended description. /// /// # Examples /// /// The following decorator implements a `#[should_panic]` analogue for errors. /// /// ``` /// use test_casing::decorators::{DecorateTest, TestFn}; /// /// #[derive(Debug, Clone, Copy)] /// pub struct ShouldError(pub &'static str); /// /// impl DecorateTest> for ShouldError { /// fn decorate_and_test>>( /// &self, /// test_fn: F, /// ) -> Result<(), E> { /// let Err(err) = test_fn() else { /// panic!("Expected test to error, but it completed successfully"); /// }; /// let err = err.to_string(); /// if err.contains(self.0) { /// Ok(()) /// } else { /// panic!( /// "Expected error message to contain `{}`, but it was: {err}", /// self.0 /// ); /// } /// } /// } /// /// // Usage: /// # use test_casing::decorate; /// # use std::error::Error; /// #[test] /// # fn eat_test_attribute() {} /// #[decorate(ShouldError("oops"))] /// fn test_with_an_error() -> Result<(), Box> { /// Err("oops, this test failed".into()) /// } /// ``` pub trait DecorateTest: panic::RefUnwindSafe + Send + Sync + 'static { /// Decorates the provided test function and runs the test. fn decorate_and_test>(&'static self, test_fn: F) -> R; } impl> DecorateTest for &'static T { fn decorate_and_test>(&'static self, test_fn: F) -> R { (**self).decorate_and_test(test_fn) } } /// Object-safe version of [`DecorateTest`]. #[doc(hidden)] // used in the `decorate` proc macro; logically private pub trait DecorateTestFn: panic::RefUnwindSafe + Send + Sync + 'static { fn decorate_and_test_fn(&'static self, test_fn: fn() -> R) -> R; } impl> DecorateTestFn for T { fn decorate_and_test_fn(&'static self, test_fn: fn() -> R) -> R { self.decorate_and_test(test_fn) } } /// [Test decorator](DecorateTest) that fails a wrapped test if it doesn't complete /// in the specified [`Duration`]. /// /// # Examples /// /// ``` /// use test_casing::{decorate, decorators::Timeout}; /// /// #[test] /// # fn eat_test_attribute() {} /// #[decorate(Timeout::secs(5))] /// fn test_with_timeout() { /// // test logic /// } /// ``` #[derive(Debug, Clone, Copy)] pub struct Timeout(pub Duration); impl Timeout { /// Defines a timeout with the specified number of seconds. pub const fn secs(secs: u64) -> Self { Self(Duration::from_secs(secs)) } /// Defines a timeout with the specified number of milliseconds. pub const fn millis(millis: u64) -> Self { Self(Duration::from_millis(millis)) } } impl DecorateTest for Timeout { #[allow(clippy::similar_names)] fn decorate_and_test>(&self, test_fn: F) -> R { let (output_sx, output_rx) = mpsc::channel(); let handle = thread::spawn(move || { output_sx.send(test_fn()).ok(); }); match output_rx.recv_timeout(self.0) { Ok(output) => { handle.join().unwrap(); // ^ `unwrap()` is safe; the thread didn't panic before `send`ing the output, // and there's nowhere to panic after that. output } Err(RecvTimeoutError::Timeout) => { panic!("Timeout {:?} expired for the test", self.0); } Err(RecvTimeoutError::Disconnected) => { let panic_object = handle.join().unwrap_err(); panic::resume_unwind(panic_object) } } } } /// [Test decorator](DecorateTest) that retries a wrapped test the specified number of times, /// potentially with a delay between retries. /// /// # Examples /// /// ``` /// use test_casing::{decorate, decorators::Retry}; /// use std::time::Duration; /// /// const RETRY_DELAY: Duration = Duration::from_millis(200); /// /// #[test] /// # fn eat_test_attribute() {} /// #[decorate(Retry::times(3).with_delay(RETRY_DELAY))] /// fn test_with_retries() { /// // test logic /// } /// ``` #[derive(Debug)] pub struct Retry { times: usize, delay: Duration, } impl Retry { /// Specified the number of retries. The delay between retries is zero. pub const fn times(times: usize) -> Self { Self { times, delay: Duration::ZERO, } } /// Specifies the delay between retries. #[must_use] pub const fn with_delay(self, delay: Duration) -> Self { Self { delay, ..self } } /// Converts this retry specification to only retry specific errors. pub const fn on_error(self, matcher: fn(&E) -> bool) -> RetryErrors { RetryErrors { inner: self, matcher, } } fn handle_panic(&self, attempt: usize, panic_object: Box) { if attempt < self.times { let panic_str = extract_panic_str(&panic_object).unwrap_or(""); let punctuation = if panic_str.is_empty() { "" } else { ": " }; println!("Test attempt #{attempt} panicked{punctuation}{panic_str}"); } else { panic::resume_unwind(panic_object); } } fn run_with_retries( &self, test_fn: impl TestFn>, should_retry: fn(&E) -> bool, ) -> Result<(), E> { for attempt in 0..=self.times { println!("Test attempt #{attempt}"); match panic::catch_unwind(test_fn) { Ok(Ok(())) => return Ok(()), Ok(Err(err)) => { if attempt < self.times && should_retry(&err) { println!("Test attempt #{attempt} errored: {err}"); } else { return Err(err); } } Err(panic_object) => { self.handle_panic(attempt, panic_object); } } if self.delay > Duration::ZERO { thread::sleep(self.delay); } } Ok(()) } } impl DecorateTest<()> for Retry { fn decorate_and_test>(&self, test_fn: F) { for attempt in 0..=self.times { println!("Test attempt #{attempt}"); match panic::catch_unwind(test_fn) { Ok(()) => break, Err(panic_object) => { self.handle_panic(attempt, panic_object); } } if self.delay > Duration::ZERO { thread::sleep(self.delay); } } } } impl DecorateTest> for Retry { fn decorate_and_test(&self, test_fn: F) -> Result<(), E> where F: TestFn>, { self.run_with_retries(test_fn, |_| true) } } fn extract_panic_str(panic_object: &(dyn Any + Send)) -> Option<&str> { if let Some(panic_str) = panic_object.downcast_ref::<&'static str>() { Some(panic_str) } else if let Some(panic_string) = panic_object.downcast_ref::() { Some(panic_string.as_str()) } else { None } } /// [Test decorator](DecorateTest) that retries a wrapped test a certain number of times /// only if an error matches the specified predicate. /// /// Constructed using [`Retry::on_error()`]. /// /// # Examples /// /// ``` /// use test_casing::{decorate, decorators::{Retry, RetryErrors}}; /// use std::error::Error; /// /// const RETRY: RetryErrors> = Retry::times(3) /// .on_error(|err| err.to_string().contains("retry please")); /// /// #[test] /// # fn eat_test_attribute() {} /// #[decorate(RETRY)] /// fn test_with_retries() -> Result<(), Box> { /// // test logic /// # Ok(()) /// } /// ``` pub struct RetryErrors { inner: Retry, matcher: fn(&E) -> bool, } impl fmt::Debug for RetryErrors { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter .debug_struct("RetryErrors") .field("inner", &self.inner) .finish_non_exhaustive() } } impl DecorateTest> for RetryErrors { fn decorate_and_test(&self, test_fn: F) -> Result<(), E> where F: TestFn>, { self.inner.run_with_retries(test_fn, self.matcher) } } /// [Test decorator](DecorateTest) that makes runs of decorated tests sequential. The sequence /// can optionally be aborted if a test in it fails. /// /// The run ordering of tests in the sequence is not deterministic. This is because depending /// on the command-line args that the test was launched with, not all tests in the sequence may run /// at all. /// /// # Examples /// /// ``` /// use test_casing::{decorate, decorators::{Sequence, Timeout}}; /// /// static SEQUENCE: Sequence = Sequence::new().abort_on_failure(); /// /// #[test] /// # fn eat_test_attribute() {} /// #[decorate(&SEQUENCE)] /// fn sequential_test() { /// // test logic /// } /// /// #[test] /// # fn eat_test_attribute2() {} /// #[decorate(Timeout::secs(1), &SEQUENCE)] /// fn other_sequential_test() { /// // test logic /// } /// ``` #[derive(Debug, Default)] pub struct Sequence { failed: Mutex, abort_on_failure: bool, } impl Sequence { /// Creates a new test sequence. pub const fn new() -> Self { Self { failed: Mutex::new(false), abort_on_failure: false, } } /// Makes the decorated tests abort immediately if one test from the sequence fails. #[must_use] pub const fn abort_on_failure(mut self) -> Self { self.abort_on_failure = true; self } fn decorate_inner>( &self, test_fn: F, ok_value: R, match_failure: fn(&R) -> bool, ) -> R { let mut guard = self.failed.lock().unwrap_or_else(PoisonError::into_inner); if *guard && self.abort_on_failure { println!("Skipping test because a previous test in the same sequence has failed"); return ok_value; } let output = panic::catch_unwind(test_fn); *guard = output.as_ref().map_or(true, match_failure); drop(guard); output.unwrap_or_else(|panic_object| { panic::resume_unwind(panic_object); }) } } impl DecorateTest<()> for Sequence { fn decorate_and_test>(&self, test_fn: F) { self.decorate_inner(test_fn, (), |()| false); } } impl DecorateTest> for Sequence { fn decorate_and_test(&self, test_fn: F) -> Result<(), E> where F: TestFn>, { self.decorate_inner(test_fn, Ok(()), Result::is_err) } } macro_rules! impl_decorate_test_for_tuple { ($($field:ident : $ty:ident),* => $last_field:ident : $last_ty:ident) => { impl DecorateTest for ($($ty,)* $last_ty,) where $($ty: DecorateTest,)* $last_ty: DecorateTest, { fn decorate_and_test>(&'static self, test_fn: Fn) -> R { let ($($field,)* $last_field,) = self; $( let test_fn = move || $field.decorate_and_test(test_fn); )* $last_field.decorate_and_test(test_fn) } } }; } impl_decorate_test_for_tuple!(=> a: A); impl_decorate_test_for_tuple!(a: A => b: B); impl_decorate_test_for_tuple!(a: A, b: B => c: C); impl_decorate_test_for_tuple!(a: A, b: B, c: C => d: D); impl_decorate_test_for_tuple!(a: A, b: B, c: C, d: D => e: E); impl_decorate_test_for_tuple!(a: A, b: B, c: C, d: D, e: E => f: F); impl_decorate_test_for_tuple!(a: A, b: B, c: C, d: D, e: E, f: F => g: G); impl_decorate_test_for_tuple!(a: A, b: B, c: C, d: D, e: E, f: F, g: G => h: H); #[cfg(test)] mod tests { use std::{ io, sync::{ atomic::{AtomicU32, Ordering}, Mutex, }, time::Instant, }; use super::*; #[test] #[should_panic(expected = "Timeout 100ms expired")] fn timeouts() { const TIMEOUT: Timeout = Timeout(Duration::from_millis(100)); let test_fn: fn() = || thread::sleep(Duration::from_secs(1)); TIMEOUT.decorate_and_test(test_fn); } #[test] fn retrying_with_delay() { const RETRY: Retry = Retry::times(1).with_delay(Duration::from_millis(100)); fn test_fn() -> Result<(), &'static str> { static TEST_START: Mutex> = Mutex::new(None); let mut test_start = TEST_START.lock().unwrap(); if let Some(test_start) = *test_start { assert!(test_start.elapsed() > RETRY.delay); Ok(()) } else { *test_start = Some(Instant::now()); Err("come again?") } } RETRY.decorate_and_test(test_fn).unwrap(); } const RETRY: RetryErrors = Retry::times(2).on_error(|err| matches!(err.kind(), io::ErrorKind::AddrInUse)); #[test] fn retrying_on_error() { static TEST_COUNTER: AtomicU32 = AtomicU32::new(0); fn test_fn() -> io::Result<()> { if TEST_COUNTER.fetch_add(1, Ordering::Relaxed) == 2 { Ok(()) } else { Err(io::Error::new( io::ErrorKind::AddrInUse, "please retry later", )) } } let test_fn: fn() -> _ = test_fn; RETRY.decorate_and_test(test_fn).unwrap(); assert_eq!(TEST_COUNTER.load(Ordering::Relaxed), 3); let err = RETRY.decorate_and_test(test_fn).unwrap_err(); assert!(err.to_string().contains("please retry later")); assert_eq!(TEST_COUNTER.load(Ordering::Relaxed), 6); } #[test] fn retrying_on_error_failure() { static TEST_COUNTER: AtomicU32 = AtomicU32::new(0); fn test_fn() -> io::Result<()> { if TEST_COUNTER.fetch_add(1, Ordering::Relaxed) == 0 { Err(io::Error::new(io::ErrorKind::BrokenPipe, "oops")) } else { Ok(()) } } let err = RETRY.decorate_and_test(test_fn).unwrap_err(); assert!(err.to_string().contains("oops")); assert_eq!(TEST_COUNTER.load(Ordering::Relaxed), 1); } #[test] fn sequential_tests() { static SEQUENCE: Sequence = Sequence::new(); static ENTRY_COUNTER: AtomicU32 = AtomicU32::new(0); let first_test = || { let counter = ENTRY_COUNTER.fetch_add(1, Ordering::Relaxed); assert_eq!(counter, 0); thread::sleep(Duration::from_millis(10)); ENTRY_COUNTER.store(0, Ordering::Relaxed); panic!("oops"); }; let second_test = || { let counter = ENTRY_COUNTER.fetch_add(1, Ordering::Relaxed); assert_eq!(counter, 0); thread::sleep(Duration::from_millis(20)); ENTRY_COUNTER.store(0, Ordering::Relaxed); Ok::<_, io::Error>(()) }; let first_test_handle = thread::spawn(move || SEQUENCE.decorate_and_test(first_test)); SEQUENCE.decorate_and_test(second_test).unwrap(); first_test_handle.join().unwrap_err(); } #[test] fn sequential_tests_with_abort() { static SEQUENCE: Sequence = Sequence::new().abort_on_failure(); let failing_test = || Err::<(), _>(io::Error::new(io::ErrorKind::AddrInUse, "please try later")); let second_test = || unreachable!("Second test should not be called!"); SEQUENCE.decorate_and_test(failing_test).unwrap_err(); SEQUENCE.decorate_and_test(second_test); } // We need independent test counters for different tests, hence defining a function // via a macro. macro_rules! define_test_fn { () => { fn test_fn() -> Result<(), &'static str> { static TEST_COUNTER: AtomicU32 = AtomicU32::new(0); match TEST_COUNTER.fetch_add(1, Ordering::Relaxed) { 0 => { thread::sleep(Duration::from_secs(1)); Ok(()) } 1 => Err("oops"), 2 => Ok(()), _ => unreachable!(), } } }; } #[test] fn composing_decorators() { define_test_fn!(); const DECORATORS: (Timeout, Retry) = (Timeout(Duration::from_millis(100)), Retry::times(2)); DECORATORS.decorate_and_test(test_fn).unwrap(); } #[test] fn making_decorator_into_trait_object() { define_test_fn!(); static DECORATORS: &dyn DecorateTestFn> = &(Timeout(Duration::from_millis(100)), Retry::times(2)); DECORATORS.decorate_and_test_fn(test_fn).unwrap(); } #[test] fn making_sequence_into_trait_object() { static SEQUENCE: Sequence = Sequence::new(); static DECORATORS: &dyn DecorateTestFn<()> = &(&SEQUENCE,); DECORATORS.decorate_and_test_fn(|| {}); } } test-casing-0.1.3/src/lib.rs000064400000000000000000000376400072674642500140130ustar 00000000000000//! Minimalistic testing framework for generating tests for a given set of test cases //! and decorating them to add retries, timeouts, sequential test processing etc. In other words, //! the framework implements: //! //! - Parameterized tests of reasonably low cardinality for the standard Rust test runner //! - Fully code-based, composable and extensible test decorators. //! //! # Overview //! //! ## Test cases //! //! [`test_casing`](macro@test_casing) attribute macro wraps a free-standing function //! with one or more arguments and transforms it into a collection of test cases. //! The arguments to the function are supplied by an iterator (more precisely, //! an expression implementing [`IntoIterator`]). //! //! For convenience, there is [`TestCases`], a lazy iterator wrapper that allows constructing //! test cases which cannot be constructed in compile time (e.g., ones requiring access to heap). //! [`TestCases`] can be instantiated using the [`cases!`] macro. //! //! Since a separate test wrapper is generated for each case, their number should be //! reasonably low (roughly speaking, no more than 20). //! Isolating each test case makes most sense if the cases involve some heavy lifting //! (spinning up a runtime, logging considerable amount of information, etc.). //! //! ## Test decorators //! //! [`decorate`] attribute macro can be placed on a test function to add generic functionality, //! such as retries, timeouts or running tests in a sequence. //! //! The [`decorators`] module defines some basic decorators and the //! [`DecorateTest`](decorators::DecorateTest) trait allowing to define custom decorators. //! Test decorators support async tests, tests returning `Result`s and test cases; see //! the module docs for more details. //! //! # Test cases structure //! //! The generated test cases are placed in a module with the same name as the target function //! near the function. //! This allows specifying the (potentially qualified) function name to restrict the test scope. //! //! If the [`nightly` crate feature](#nightly) is not enabled, names of particular test cases //! are not descriptive; they have the `case_NN` format, where `NN` is the 0-based case index. //! The values of arguments provided to the test are printed to the standard output //! at the test start. (The standard output is captured and thus may be not visible //! unless the `--nocapture` option is specified in the `cargo test` command.) //! //! If the `nightly` feature *is* enabled, the names are more descriptive, containing [`Debug`] //! presentation of all args together with their names. Here's an excerpt from the integration //! tests for this crate: //! //! ```text //! test number_can_be_converted_to_string::case_1 [number = 3, expected = "3"] ... ok //! test number_can_be_converted_to_string::case_2 [number = 5, expected = "5"] ... ok //! test numbers_are_large::case_0 [number = 2] ... ignored, testing that `#[ignore]` attr works //! test numbers_are_large::case_1 [number = 3] ... ignored, testing that `#[ignore]` attr works //! test string_conversion_fail::case_0 [bogus_str = "not a number"] - should panic ... ok //! test string_conversion_fail::case_1 [bogus_str = "-"] - should panic ... ok //! test string_conversion_fail::case_2 [bogus_str = ""] - should panic ... ok //! ``` //! //! The names are fully considered when filtering tests, meaning that it's possible to run //! particular cases using a filter like `cargo test 'number = 5'`. //! //! # Alternatives and similar tools //! //! - The approach to test casing from this crate can be reproduced with some amount of copy-pasting //! by manually feeding necessary inputs to a common parametric testing function. //! Optionally, these tests may be collected in a module for better structuring. //! The main downside of this approach is the amount of copy-pasting. //! - Alternatively, multiple test cases may be run in a single `#[test]` (e.g., in a loop). //! This is fine for the large amount of small cases (e.g., mini-fuzzing), but may have downsides //! such as overflowing or overlapping logs and increased test runtimes. //! - The [`test-case`] crate uses a similar approach to test case structuring, but differs //! in how test case inputs are specified. Subjectively, the approach used by this crate //! is more extensible and easier to read. //! - [Property testing] / [`quickcheck`]-like frameworks provide much more exhaustive approach //! to parameterized testing, but they require significantly more setup effort. //! - [`rstest`] supports test casing and some of the test decorators (e.g., timeouts). //! - [`nextest`] is an alternative test runner that supports most of the test decorators //! defined in the [`decorators`] module. It does not use code-based decorator config and //! does not allow for custom decorator. //! //! [`test-case`]: https://docs.rs/test-case/ //! [Property testing]: https://docs.rs/proptest/ //! [`quickcheck`]: https://docs.rs/quickcheck/ //! [`rstest`]: https://crates.io/crates/rstest //! [`nextest`]: https://nexte.st/ //! //! # Crate features //! //! ## `nightly` //! //! *(Off by default)* //! //! Uses [custom test frameworks] APIs together with a generous spicing of hacks //! to include arguments in the names of the generated tests (see an excerpt above //! for an illustration). `test_casing` actually does not require a custom test runner, //! but rather hacks into the standard one; thus, the generated test cases can run alongside with //! ordinary / non-parameterized tests. //! //! Requires a nightly Rust toolchain and specifying `#![feature(test, custom_test_frameworks)]` //! in the using crate. Because `custom_test_frameworks` APIs may change between toolchain releases, //! the feature may break. See [the CI config] for the nightly toolchain version the crate //! is tested against. //! //! [custom test frameworks]: https://github.com/rust-lang/rust/issues/50297 //! [the CI config]: https://github.com/slowli/test-casing/blob/main/.github/workflows/ci.yml #![cfg_attr(feature = "nightly", feature(custom_test_frameworks, test))] // Documentation settings #![doc(html_root_url = "https://docs.rs/test-casing/0.1.3")] // Linter settings #![warn(missing_debug_implementations, missing_docs, bare_trait_objects)] #![warn(clippy::all, clippy::pedantic)] #![allow(clippy::must_use_candidate, clippy::module_name_repetitions)] /// Wraps a tested function to add retries, timeouts etc. /// /// # Inputs /// /// This attribute must be placed on a test function (i.e., one decorated with `#[test]`, /// `#[tokio::test]`, etc.). The attribute must be invoked with a comma-separated list /// of one or more [test decorators](decorators::DecorateTest). Each decorator must /// be a constant expression (i.e., it should be usable as a definition of a `static` variable). /// /// # Examples /// /// ## Basic usage /// /// ``` /// use test_casing::{decorate, decorators::Timeout}; /// /// #[test] /// # fn eat_test_attribute() {} /// #[decorate(Timeout::secs(1))] /// fn test_with_timeout() { /// // test logic /// } /// ``` /// /// ## Tests returning `Result`s /// /// Decorators can be used on tests returning `Result`s, too: /// /// ``` /// use test_casing::{decorate, decorators::{Retry, Timeout}}; /// use std::error::Error; /// /// #[test] /// # fn eat_test_attribute() {} /// #[decorate(Timeout::millis(200), Retry::times(2))] /// // ^ Decorators are applied in the order of their mention. In this case, /// // if the test times out, errors or panics, it will be retried up to 2 times. /// fn test_with_retries() -> Result<(), Box> { /// // test logic /// # Ok(()) /// } /// ``` /// /// ## Multiple `decorate` attributes /// /// Multiple `decorate` attributes are allowed. Thus, the test above is equivalent to /// /// ``` /// # use test_casing::{decorate, decorators::{Retry, Timeout}}; /// # use std::error::Error; /// #[test] /// # fn eat_test_attribute() {} /// #[decorate(Timeout::millis(200))] /// #[decorate(Retry::times(2))] /// fn test_with_retries() -> Result<(), Box> { /// // test logic /// # Ok(()) /// } /// ``` /// /// ## Async tests /// /// Decorators work on async tests as well, as long as the `decorate` macro is applied after /// the test macro: /// /// ``` /// # use test_casing::{decorate, decorators::Retry}; /// #[async_std::test] /// #[decorate(Retry::times(3))] /// async fn async_test() { /// // test logic /// } /// ``` /// /// ## Composability and reuse /// /// Decorators can be extracted to a `const`ant or a `static` for readability, composability /// and/or reuse: /// /// ``` /// # use test_casing::{decorate, decorators::*}; /// # use std::time::Duration; /// const RETRY: RetryErrors = Retry::times(2) /// .with_delay(Duration::from_secs(1)) /// .on_error(|s| s.contains("oops")); /// /// static SEQUENCE: Sequence = Sequence::new().abort_on_failure(); /// /// #[test] /// # fn eat_test_attribute() {} /// #[decorate(RETRY, &SEQUENCE)] /// fn test_with_error_retries() -> Result<(), String> { /// // test logic /// # Ok(()) /// } /// /// #[test] /// # fn eat_test_attribute2() {} /// #[decorate(&SEQUENCE)] /// fn other_test() { /// // test logic /// } /// ``` /// /// ## Use with `test_casing` /// /// When used together with the [`test_casing`](macro@test_casing) macro, the decorators will apply /// to each generated case. /// /// ``` /// use test_casing::{decorate, test_casing, decorators::Timeout}; /// /// #[test_casing(3, [3, 5, 42])] /// #[decorate(Timeout::secs(1))] /// fn parameterized_test_with_timeout(input: u64) { /// // test logic /// } /// ``` pub use test_casing_macro::decorate; /// Flattens a parameterized test into a collection of test cases. /// /// # Inputs /// /// This attribute must be placed on a free-standing function with 1..8 arguments. /// The attribute must be invoked with 2 values: /// /// 1. Number of test cases, a number literal /// 2. A *case iterator* expression evaluating to an implementation of [`IntoIterator`] /// with [`Debug`]gable, `'static` items. /// If the target function has a single argument, the iterator item type must equal to /// the argument type. Otherwise, the iterator must return a tuple in which each item /// corresponds to the argument with the same index. /// /// A case iterator expression may reference the environment (e.g., it can be a name of a constant). /// It doesn't need to be a constant expression (e.g., it may allocate in heap). It should /// return at least the number of items specified as the first attribute argument, and can /// return more items; these additional items will not be tested. /// /// [`Debug`]: core::fmt::Debug /// /// # Mapping arguments /// /// To support more idiomatic signatures for parameterized test functions, it is possible /// to *map* from the type returned by the case iterator. The only supported kind of mapping /// so far is taking a shared reference (i.e., `T` → `&T`). The mapping is enabled by placing /// the `#[map(ref)]` attribute on the corresponding argument. Optionally, the reference `&T` /// can be further mapped with a function / method (e.g., `&String` → `&str` with /// [`String::as_str()`]). This is specified as `#[map(ref = path::to::method)]`, a la /// `serde` transforms. /// /// # Examples /// /// ## Basic usage /// /// `test_casing` macro accepts 2 args: number of cases and the iterator expression. /// The latter can be any valid Rust expression. /// /// ``` /// # use test_casing::test_casing; /// #[test_casing(5, 0..5)] /// // #[test] attribute is optional and is added automatically /// // provided that the test function is not `async`. /// fn number_is_small(number: i32) { /// assert!(number < 10); /// } /// ``` /// /// Functions returning `Result`s are supported as well. /// /// ``` /// # use test_casing::test_casing; /// use std::error::Error; /// /// #[test_casing(3, ["0", "42", "-3"])] /// fn parsing_numbers(s: &str) -> Result<(), Box> { /// let number: i32 = s.parse()?; /// assert!(number.abs() < 100); /// Ok(()) /// } /// ``` /// /// The function on which the `test_casing` attribute is placed can be accessed from other code /// (e.g., for more tests): /// /// ``` /// # use test_casing::test_casing; /// # use std::error::Error; /// #[test_casing(3, ["0", "42", "-3"])] /// fn parsing_numbers(s: &str) -> Result<(), Box> { /// // snipped... /// # Ok(()) /// } /// /// #[test] /// fn parsing_number_error() { /// assert!(parsing_numbers("?").is_err()); /// } /// ``` /// /// ## Case expressions /// /// Case expressions can be extracted to a constant for reuse or better code structuring. /// /// ``` /// # use test_casing::{cases, test_casing, TestCases}; /// const CASES: TestCases<(String, i32)> = cases! { /// [0, 42, -3].map(|i| (i.to_string(), i)) /// }; /// /// #[test_casing(3, CASES)] /// fn parsing_numbers(s: String, expected: i32) { /// let parsed: i32 = s.parse().unwrap(); /// assert_eq!(parsed, expected); /// } /// ``` /// /// This example also shows that semantics of args is up to the writer; some of the args may be /// expected values, etc. /// /// ## Cartesian product /// /// One of possible case expressions is a [`Product`]; it can be used to generate test cases /// as a Cartesian product of the expressions for separate args. /// /// ``` /// # use test_casing::{test_casing, Product}; /// #[test_casing(6, Product((0_usize..3, ["foo", "bar"])))] /// fn numbers_and_strings(number: usize, s: &str) { /// assert!(s.len() <= number); /// } /// ``` /// /// ## Reference args /// /// It is possible to go from a generated argument to its reference by adding /// a `#[map(ref)]` attribute on the argument. The attribute may optionally specify /// a path to the transform function from the reference to the desired type /// (similar to transform specifications in the [`serde`](https://docs.rs/serde/) attr). /// /// ``` /// # use test_casing::{cases, test_casing, TestCases}; /// const CASES: TestCases<(String, i32)> = cases! { /// [0, 42, -3].map(|i| (i.to_string(), i)) /// }; /// /// #[test_casing(3, CASES)] /// fn parsing_numbers(#[map(ref)] s: &str, expected: i32) { /// // Snipped... /// } /// /// #[test_casing(3, CASES)] /// fn parsing_numbers_too( /// #[map(ref = String::as_str)] s: &str, /// expected: i32, /// ) { /// // Snipped... /// } /// ``` /// /// ## `ignore` and `should_panic` attributes /// /// `ignore` or `should_panic` attributes can be specified below the `test_casing` attribute. /// They will apply to all generated tests. /// /// ``` /// # use test_casing::test_casing; /// #[test_casing(3, ["not", "implemented", "yet"])] /// #[ignore = "Promise this will work sometime"] /// fn future_test(s: &str) { /// unimplemented!() /// } /// /// #[test_casing(3, ["not a number", "-", ""])] /// #[should_panic(expected = "ParseIntError")] /// fn string_conversion_fail(bogus_str: &str) { /// bogus_str.parse::().unwrap(); /// } /// ``` /// /// ## Async tests /// /// `test_casing` supports all kinds of async test wrappers, such as `async_std::test`, /// `tokio::test`, `actix::test` etc. The corresponding attribute just needs to be specified /// *below* the `test_casing` attribute. /// /// ``` /// # use test_casing::test_casing; /// # use std::error::Error; /// #[test_casing(3, ["0", "42", "-3"])] /// #[async_std::test] /// async fn parsing_numbers(s: &str) -> Result<(), Box> { /// assert!(s.parse::()?.abs() < 100); /// Ok(()) /// } /// ``` pub use test_casing_macro::test_casing; pub mod decorators; #[cfg(feature = "nightly")] #[doc(hidden)] // used by the `#[test_casing]` macro; logically private pub mod nightly; mod test_casing; pub use crate::test_casing::{case, ArgNames, Product, ProductIter, TestCases}; test-casing-0.1.3/src/nightly.rs000064400000000000000000000106050072674642500147130ustar 00000000000000//! Functionality gated by the `nightly` feature and requiring unstable features. extern crate test; use once_cell::sync::Lazy; use std::{fmt, ops}; use test::{ShouldPanic, TestDesc, TestFn, TestName, TestType}; pub use test::assert_test_result; pub type LazyTestCase = Lazy; // Wrapper to overcome `!Sync` for `TestDescAndFn` caused by dynamic `TestFn` variants. pub struct TestDescAndFn { inner: test::TestDescAndFn, } impl fmt::Debug for TestDescAndFn { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Debug::fmt(&self.inner, formatter) } } // SAFETY: we only ever construct instances with a `Sync` variant of `TestFn` // (namely `StaticTestFn`). unsafe impl Sync for TestDescAndFn {} impl TestDescAndFn { pub fn new(desc: TestDesc, testfn: fn() -> Result<(), String>) -> Self { Self { inner: test::TestDescAndFn { desc, testfn: TestFn::StaticTestFn(testfn), }, } } } impl ops::Deref for TestDescAndFn { type Target = test::TestDescAndFn; fn deref(&self) -> &Self::Target { &self.inner } } #[doc(hidden)] pub fn create_test_description( is_unit_test: bool, base_name: &'static str, arg_names: impl crate::ArgNames, cases: impl IntoIterator, index: usize, ) -> TestDesc { let path_in_crate = match base_name.split_once("::") { Some((_, path)) => path, None => "", }; let test_args = crate::case(cases, index); let description = arg_names.print_with_args(&test_args); TestDesc { name: TestName::DynTestName(format!("{path_in_crate}::case_{index} [{description}]")), ignore: false, ignore_message: None, source_file: "", start_line: 0, start_col: 0, end_line: 0, end_col: 0, should_panic: ShouldPanic::No, compile_fail: false, no_run: false, test_type: if is_unit_test { TestType::UnitTest } else { TestType::IntegrationTest }, } } pub fn set_location( desc: &mut TestDesc, source_file: &'static str, start_line: usize, start_col: usize, end_line: usize, end_col: usize, ) { desc.source_file = source_file; desc.start_line = start_line; desc.start_col = start_col; desc.end_line = end_line; desc.end_col = end_col; } pub fn set_ignore(desc: &mut TestDesc, message: Option<&'static str>) { desc.ignore = true; desc.ignore_message = message; } pub fn set_should_panic(desc: &mut TestDesc, message: Option<&'static str>) { desc.should_panic = match message { None => ShouldPanic::Yes, Some(message) => ShouldPanic::YesWithMessage(message), }; } // We cannot declare a `const fn` to produce `LazyTestCase`s because the closure // provided to `LazyTestCase::new()` cannot be inlined in a function. For the same reason, // the closure in `TestDescAndFn::new()` is not inlined. #[doc(hidden)] #[macro_export] macro_rules! declare_test_case { ( base_name: $base_name:expr, source_file: $source_file:expr, start_line: $start_line:expr, start_col: $start_col:expr, end_line: $end_line:expr, end_col: $end_col:expr, arg_names: $arg_names:expr, cases: $cases:expr, index: $test_index:expr, $(ignore: $ignore:expr,)? $(panic_message: $panic_message:expr,)? testfn: $test_fn:path ) => { $crate::nightly::LazyTestCase::new(|| { let is_unit_test = ::core::option_env!("CARGO_TARGET_TMPDIR").is_none(); let mut desc = $crate::nightly::create_test_description( is_unit_test, $base_name, $arg_names, $cases, $test_index, ); $crate::nightly::set_location( &mut desc, $source_file, $start_line, $start_col, $end_line, $end_col, ); $( $crate::nightly::set_ignore(&mut desc, $ignore); )? $( $crate::nightly::set_should_panic(&mut desc, $panic_message); )? $crate::nightly::TestDescAndFn::new(desc, || { $crate::nightly::assert_test_result($test_fn()) }) }) }; } test-casing-0.1.3/src/test_casing.rs000064400000000000000000000172000072674642500155360ustar 00000000000000//! Support types for the `test_casing` macro. use std::{fmt, iter::Fuse}; /// Obtains a test case from an iterator. #[doc(hidden)] // used by the `#[test_casing]` macro; logically private pub fn case(iter: I, index: usize) -> I::Item where I::Item: fmt::Debug, { iter.into_iter().nth(index).unwrap_or_else(|| { panic!("case #{index} not provided from the cases iterator"); }) } /// Allows printing named arguments together with their values to a `String`. #[doc(hidden)] // used by the `#[test_casing]` macro; logically private pub trait ArgNames: Copy + IntoIterator { fn print_with_args(self, args: &T) -> String; } impl ArgNames for [&'static str; 1] { fn print_with_args(self, args: &T) -> String { format!("{name} = {args:?}", name = self[0]) } } macro_rules! impl_arg_names { ($n:tt => $($idx:tt: $arg_ty:ident),+) => { impl<$($arg_ty : fmt::Debug,)+> ArgNames<($($arg_ty,)+)> for [&'static str; $n] { fn print_with_args(self, args: &($($arg_ty,)+)) -> String { use std::fmt::Write as _; let mut buffer = String::new(); $( write!(buffer, "{} = {:?}", self[$idx], args.$idx).unwrap(); if $idx + 1 < self.len() { buffer.push_str(", "); } )+ buffer } } }; } impl_arg_names!(2 => 0: T, 1: U); impl_arg_names!(3 => 0: T, 1: U, 2: V); impl_arg_names!(4 => 0: T, 1: U, 2: V, 3: W); impl_arg_names!(5 => 0: T, 1: U, 2: V, 3: W, 4: X); impl_arg_names!(6 => 0: T, 1: U, 2: V, 3: W, 4: X, 5: Y); impl_arg_names!(7 => 0: T, 1: U, 2: V, 3: W, 4: X, 5: Y, 6: Z); /// Container for test cases based on a lazily evaluated iterator. Should be constructed /// using the [`cases!`](crate::cases) macro. /// /// # Examples /// /// ``` /// # use test_casing::{cases, TestCases}; /// const NUMBER_CASES: TestCases = cases!([2, 3, 5, 8]); /// const MORE_CASES: TestCases = cases! { /// NUMBER_CASES.into_iter().chain([42, 555]) /// }; /// /// // The `cases!` macro can wrap a statement block: /// const COMPLEX_CASES: TestCases = cases!({ /// use rand::{rngs::StdRng, Rng, SeedableRng}; /// /// let mut rng = StdRng::seed_from_u64(123); /// (0..5).map(move |_| rng.gen()) /// }); /// ``` pub struct TestCases { lazy: fn() -> Box>, } impl fmt::Debug for TestCases { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.debug_struct("TestCases").finish_non_exhaustive() } } impl Clone for TestCases { fn clone(&self) -> Self { *self } } impl Copy for TestCases {} impl TestCases { /// Creates a new set of test cases. pub const fn new(lazy: fn() -> Box>) -> Self { Self { lazy } } } impl IntoIterator for TestCases { type Item = T; type IntoIter = Box>; fn into_iter(self) -> Self::IntoIter { (self.lazy)() } } /// Creates [`TestCases`] based on the provided expression implementing [`IntoIterator`] /// (e.g., an array, a range or an iterator). /// /// # Examples /// /// See [`TestCases`](TestCases#examples) docs for the examples of usage. #[macro_export] macro_rules! cases { ($iter:expr) => { $crate::TestCases::<_>::new(|| { std::boxed::Box::new(core::iter::IntoIterator::into_iter($iter)) }) }; } /// Cartesian product of several test cases. /// /// For now, this supports products of 2..8 values. The provided [`IntoIterator`] expression /// for each value must implement [`Clone`]. One way to do that is using [`TestCases`], which /// wraps a lazy iterator initializer and is thus always [`Copy`]able. /// /// # Examples /// /// ``` /// # use test_casing::Product; /// let product = Product((0..2, ["test", "other"])); /// let values: Vec<_> = product.into_iter().collect(); /// assert_eq!( /// values, /// [(0, "test"), (0, "other"), (1, "test"), (1, "other")] /// ); /// ``` #[derive(Debug, Clone, Copy)] pub struct Product(pub Ts); impl IntoIterator for Product<(T, U)> where T: Clone + IntoIterator, U: Clone + IntoIterator, { type Item = (T::Item, U::Item); type IntoIter = ProductIter; fn into_iter(self) -> Self::IntoIter { let (_, second) = &self.0; let second = second.clone(); ProductIter { sources: self.0, first_idx: 0, second_iter: second.into_iter().fuse(), is_finished: false, } } } macro_rules! impl_product { ($head:ident: $head_ty:ident, $($tail:ident: $tail_ty:ident),+) => { impl<$head_ty, $($tail_ty,)+> IntoIterator for Product<($head_ty, $($tail_ty,)+)> where $head_ty: 'static + Clone + IntoIterator, $($tail_ty: 'static + Clone + IntoIterator,)+ { type Item = ($head_ty::Item, $($tail_ty::Item,)+); type IntoIter = Box>; fn into_iter(self) -> Self::IntoIter { let ($head, $($tail,)+) = self.0; let tail = Product(($($tail,)+)); let iter = Product(($head, tail)) .into_iter() .map(|($head, ($($tail,)+))| ($head, $($tail,)+)); Box::new(iter) } } }; } impl_product!(t: T, u: U, v: V); impl_product!(t: T, u: U, v: V, w: W); impl_product!(t: T, u: U, v: V, w: W, x: X); impl_product!(t: T, u: U, v: V, w: W, x: X, y: Y); impl_product!(t: T, u: U, v: V, w: W, x: X, y: Y, z: Z); /// Iterator over test cases in [`Product`]. #[derive(Debug)] pub struct ProductIter { sources: (T, U), first_idx: usize, second_iter: Fuse, is_finished: bool, } impl Iterator for ProductIter where T: Clone + IntoIterator, U: Clone + IntoIterator, { type Item = (T::Item, U::Item); fn next(&mut self) -> Option { if self.is_finished { return None; } loop { if let Some(second_case) = self.second_iter.next() { let mut first_iter = self.sources.0.clone().into_iter(); let Some(first_case) = first_iter.nth(self.first_idx) else { self.is_finished = true; return None; }; return Some((first_case, second_case)); } self.first_idx += 1; self.second_iter = self.sources.1.clone().into_iter().fuse(); } } } #[cfg(doctest)] doc_comment::doctest!("../README.md"); #[cfg(test)] mod tests { use std::collections::HashSet; use super::*; #[test] fn cartesian_product() { let numbers = cases!(0..3); let strings = cases!(["0", "1"]); let cases: Vec<_> = Product((numbers, strings)).into_iter().collect(); assert_eq!( cases.as_slice(), [(0, "0"), (0, "1"), (1, "0"), (1, "1"), (2, "0"), (2, "1")] ); let booleans = [false, true]; let cases: HashSet<_> = Product((numbers, strings, booleans)).into_iter().collect(); assert_eq!(cases.len(), 12); // 3 * 2 * 2 } #[test] fn unit_test_detection_works() { assert!(option_env!("CARGO_TARGET_TMPDIR").is_none()); } } test-casing-0.1.3/tests/integration/decorate.rs000064400000000000000000000131330072674642500177200ustar 00000000000000//! Integration tests for the `decorate` macro. use async_std::task; use std::{ error::Error, sync::atomic::{AtomicBool, AtomicU32, Ordering}, thread, time::Duration, }; use test_casing::{decorate, decorators::*, test_casing}; #[test] #[decorate(Timeout(Duration::from_secs(5)))] fn with_inlined_timeout() { thread::sleep(Duration::from_millis(10)); } const TIMEOUT: Timeout = Timeout::secs(3); #[test] #[decorate(TIMEOUT)] fn with_timeout_constant() { thread::sleep(Duration::from_millis(10)); } #[test] #[decorate(TIMEOUT, Retry::times(2))] fn with_mixed_decorators() { thread::sleep(Duration::from_millis(10)); } #[test] #[decorate(Retry::times(1))] fn with_retries() { static COUNTER: AtomicU32 = AtomicU32::new(0); assert!( COUNTER.fetch_add(1, Ordering::Relaxed) != 0, "Sometimes we all fail" ); } #[test] #[decorate(Retry::times(1))] fn with_retries_and_error() -> Result<(), Box> { static COUNTER: AtomicU32 = AtomicU32::new(0); if COUNTER.fetch_add(1, Ordering::Relaxed) == 0 { Err("Sometimes we all fail".into()) } else { Ok(()) } } const RETRY_ERRORS: RetryErrors> = Retry::times(1).on_error(|err| err.to_string().contains("retry")); #[test] #[decorate(RETRY_ERRORS)] fn with_error_retries() -> Result<(), Box> { static COUNTER: AtomicU32 = AtomicU32::new(0); if COUNTER.fetch_add(1, Ordering::Relaxed) == 0 { Err("please retry me".into()) } else { Ok(()) } } #[derive(Debug, Clone, Copy)] struct ShouldError(&'static str); impl DecorateTest> for ShouldError { fn decorate_and_test>>(&'static self, test_fn: F) -> Result<(), E> { let Err(err) = test_fn() else { panic!("Expected test to error, but it completed successfully"); }; let err = err.to_string(); if err.contains(self.0) { Ok(()) } else { panic!( "Expected error message to contain `{}`, but it was: {err}", self.0 ); } } } #[test] #[decorate(RETRY_ERRORS, ShouldError("oops"))] // also tests custom decorators fn mismatched_error_with_error_retries() -> Result<(), Box> { Err("oops".into()) } #[test] #[decorate(ShouldError("oops"), Retry::times(2))] fn with_custom_decorator_and_retries() -> Result<(), &'static str> { static COUNTER: AtomicU32 = AtomicU32::new(0); match COUNTER.fetch_add(1, Ordering::Relaxed) { 1 => Err("nothing to see here"), 2 => Err("oops"), _ => Ok(()), } } #[test] #[decorate(ShouldError("oops"))] #[decorate(Retry::times(2))] fn with_several_decorator_macros() -> Result<(), &'static str> { static COUNTER: AtomicU32 = AtomicU32::new(0); match COUNTER.fetch_add(1, Ordering::Relaxed) { 1 => Err("nothing to see here"), 2 => Err("oops"), _ => Ok(()), } } #[async_std::test] #[decorate(Timeout::millis(100), Retry::times(1))] async fn async_test_with_timeout() { static COUNTER: AtomicU32 = AtomicU32::new(0); if COUNTER.fetch_add(1, Ordering::Relaxed) == 0 { task::sleep(Duration::from_millis(500)).await; // ^ will cause the test failure } } static SEQUENCE: Sequence = Sequence::new().abort_on_failure(); /// Checks that test in a `Sequence` are in fact sequential. #[derive(Debug)] struct SequenceChecker { is_running: AtomicBool, } impl SequenceChecker { const fn new() -> Self { Self { is_running: AtomicBool::new(false), } } fn start(&self) -> SequenceCheckerGuard<'_> { let prev_value = self.is_running.swap(true, Ordering::SeqCst); assert!(!prev_value, "Sequential tests are not sequential!"); SequenceCheckerGuard { is_running: &self.is_running, } } } #[derive(Debug)] struct SequenceCheckerGuard<'a> { is_running: &'a AtomicBool, } impl Drop for SequenceCheckerGuard<'_> { fn drop(&mut self) { self.is_running.store(false, Ordering::SeqCst); } } static SEQUENCE_CHECKER: SequenceChecker = SequenceChecker::new(); #[test] #[should_panic(expected = "oops")] #[decorate(&SEQUENCE)] fn panicking_sequential_test() { let _guard = SEQUENCE_CHECKER.start(); thread::sleep(Duration::from_millis(50)); panic!("oops"); } #[test] #[decorate(&SEQUENCE)] fn other_sequential_test() { let _guard = SEQUENCE_CHECKER.start(); thread::sleep(Duration::from_millis(50)); } #[async_std::test] #[decorate(Retry::times(1), &SEQUENCE)] async fn async_sequential_test() -> Result<(), Box> { static COUNTER: AtomicU32 = AtomicU32::new(0); let _guard = SEQUENCE_CHECKER.start(); task::sleep(Duration::from_millis(50)).await; if COUNTER.fetch_add(1, Ordering::Relaxed) == 0 { Err("oops".into()) } else { Ok(()) } } #[test_casing(3, ["1", "2", "3!"])] #[decorate(Retry::times(1))] fn cases_with_retries(s: &str) { // This is sloppy (the test case ordering is non-deterministic, so we can skip starting cases), // but sort of OK for the purpose. static IGNORE_ERROR: AtomicBool = AtomicBool::new(false); if IGNORE_ERROR.load(Ordering::SeqCst) { return; } let parse_result = s.parse::(); if parse_result.is_err() { IGNORE_ERROR.store(true, Ordering::SeqCst); } parse_result.unwrap(); } test-casing-0.1.3/tests/integration/main.rs000064400000000000000000000005650072674642500170630ustar 00000000000000//! Integration tests for crate functionality. #![cfg_attr(feature = "nightly", feature(test, custom_test_frameworks))] // Enable additional lints to ensure that the code produced by the macro doesn't raise warnings. #![warn(missing_debug_implementations, missing_docs, bare_trait_objects)] #![warn(clippy::all, clippy::pedantic)] mod decorate; mod test_casing; test-casing-0.1.3/tests/integration/test_casing.rs000064400000000000000000000074450072674642500204460ustar 00000000000000//! Integration tests for `test_casing` macro. use async_std::task; use std::error::Error; use test_casing::{cases, test_casing, Product, TestCases}; // Cases can be reused across multiple tests. const CASES: TestCases = cases!([2, 3, 5, 8]); #[test_casing(4, CASES)] #[test] fn numbers_are_small(number: i32) { assert!((0..10).contains(&number)); } #[test] fn another_number_is_small() { numbers_are_small(1); } #[allow(unused_variables)] // should be retained on the target fn #[test_casing(4, CASES)] #[ignore = "testing that `#[ignore]` attr works"] fn numbers_are_large(number: i32) { unimplemented!("implement later"); } #[test_casing(4, CASES)] fn numbers_are_small_with_errors(number: i32) -> Result<(), Box> { if number < 10 { Ok(()) } else { Err("number is too large".into()) } } // It's possible to specify cases with multiple args. The semantics of args // (e.g., whether any of them are expected values) is up to the user. const MULTI_ARG_CASES: TestCases<(i32, &str)> = cases!([(2, "2"), (3, "3"), (5, "5")]); #[test_casing(3, MULTI_ARG_CASES)] #[test] fn number_can_be_converted_to_string(number: i32, expected: &str) { assert_eq!(number.to_string(), expected); } #[test_casing(3, MULTI_ARG_CASES)] fn number_can_be_converted_to_string_with_tuple_input((number, expected): (i32, &str)) { assert_eq!(number.to_string(), expected); } // `Product` allows testing a Cartesian product of the contained cases of arity in 2..8. #[test_casing(12, Product((CASES, ["first", "second", "third"])))] fn cartesian_product(number: i32, s: &str) { assert_ne!(number.to_string(), s); } // If it semantically makes sense, it's possible to borrow some of the returned case args // using a `#[map(ref)]` attr on the arg. An optional transform on the reference in a form // of a path can be specified as well. (Here, the transform is trivial and serves the purpose // of assisting the Rust type inference.) #[test_casing(5, cases!{(0..5).map(|i| (i.to_string(), i))})] fn string_conversion(#[map(ref = String::as_str)] s: &str, expected: i32) { let actual: i32 = s.parse().unwrap(); assert_eq!(actual, expected); } #[test_casing(3, ["not a number", "-", ""])] #[should_panic(expected = "ParseIntError")] fn string_conversion_fail(bogus_str: &str) { string_conversion(bogus_str, 42); } const STRING_CASES: TestCases<(String, i32)> = cases!((0..5).map(|i| (i.to_string(), i))); #[test_casing(5, STRING_CASES)] #[async_std::test] async fn async_string_conversion_without_output(#[map(ref)] s: &str, expected: i32) { let actual: i32 = s.parse().unwrap(); assert_eq!(actual, expected); let expected_string = task::spawn_blocking(move || expected.to_string()).await; assert_eq!(expected_string, s); } #[test_casing(5, STRING_CASES)] #[async_std::test] async fn async_string_conversion(#[map(ref)] s: &str, expected: i32) -> Result<(), Box> { let actual: i32 = s.parse()?; assert_eq!(actual, expected); let expected_string = task::spawn_blocking(move || expected.to_string()).await; assert_eq!(expected_string, s); Ok(()) } #[test] fn unit_test_detection_works() { assert!(option_env!("CARGO_TARGET_TMPDIR").is_some()); } // Tests paths to tests in modules. mod random { use rand::{rngs::StdRng, Rng, SeedableRng}; use std::iter; use test_casing::{cases, test_casing, TestCases}; // The library can be used for randomized tests as well, but it's probably not the best choice // if the number of test cases should be large. const RANDOM_NUMBERS: TestCases = cases!({ let mut rng = StdRng::seed_from_u64(123_456); iter::repeat_with(move || rng.gen()) }); #[test_casing(10, RANDOM_NUMBERS)] fn randomized_tests(value: u32) { assert!(value.to_string().len() <= 10); } } test-casing-0.1.3/tests/ui/bug_in_iter_expr.rs000064400000000000000000000002760072674642500175540ustar 00000000000000use test_casing::{cases, test_casing, TestCases}; const CASES: TestCases = cases!([1, 2]); #[test_casing(2, CASS)] fn tested_function(_arg: i32) { // Does nothing } fn main() {} test-casing-0.1.3/tests/ui/bug_in_iter_expr.stderr000064400000000000000000000005410072674642500204260ustar 00000000000000error[E0425]: cannot find value `CASS` in this scope --> tests/ui/bug_in_iter_expr.rs:5:18 | 3 | const CASES: TestCases = cases!([1, 2]); | --------------------------------------------- similarly named constant `CASES` defined here 4 | 5 | #[test_casing(2, CASS)] | ^^^^ help: a constant with a similar name exists: `CASES` test-casing-0.1.3/tests/ui/fn_with_too_many_args.rs000064400000000000000000000004110072674642500205760ustar 00000000000000use test_casing::test_casing; #[test_casing(1, [(1, 2, 3, 4, 5, 6, 7, 8)])] fn tested_function( _arg0: i32, _arg1: i32, _arg2: i32, _arg3: i32, _arg4: i32, _arg5: i32, _arg6: i32, _arg7: i32, ) { // Does nothing } fn main() {} test-casing-0.1.3/tests/ui/fn_with_too_many_args.stderr000064400000000000000000000003700072674642500214610ustar 00000000000000error: tested function must have no more than 7 args --> tests/ui/fn_with_too_many_args.rs:4:1 | 4 | / fn tested_function( 5 | | _arg0: i32, 6 | | _arg1: i32, 7 | | _arg2: i32, ... | 12 | | _arg7: i32, 13 | | ) { | |_^ test-casing-0.1.3/tests/ui/fn_without_args.rs000064400000000000000000000001760072674642500174310ustar 00000000000000use test_casing::test_casing; #[test_casing(2, ["test", "this"])] fn tested_function() { // Does nothing } fn main() {} test-casing-0.1.3/tests/ui/fn_without_args.stderr000064400000000000000000000002170072674642500203040ustar 00000000000000error: tested function must have at least one arg --> tests/ui/fn_without_args.rs:4:1 | 4 | fn tested_function() { | ^^^^^^^^^^^^^^^^^^^^ test-casing-0.1.3/tests/ui/invalid_case_count.rs000064400000000000000000000003360072674642500200560ustar 00000000000000use test_casing::test_casing; #[test_casing("2", ["test", "this"])] fn tested_function(_arg: &str) { // Does nothing } #[test_casing(0, [])] fn other_tested_function(_arg: &str) { // Does nothing } fn main() {} test-casing-0.1.3/tests/ui/invalid_case_count.stderr000064400000000000000000000004260072674642500207350ustar 00000000000000error: expected integer literal --> tests/ui/invalid_case_count.rs:3:15 | 3 | #[test_casing("2", ["test", "this"])] | ^^^ error: number of test cases must be positive --> tests/ui/invalid_case_count.rs:8:15 | 8 | #[test_casing(0, [])] | ^ test-casing-0.1.3/tests/ui/invalid_iter_expr.rs000064400000000000000000000001700072674642500177300ustar 00000000000000use test_casing::test_casing; #[test_casing(2, 5)] fn tested_function(_arg: i32) { // Does nothing } fn main() {} test-casing-0.1.3/tests/ui/invalid_iter_expr.stderr000064400000000000000000000011200072674642500206030ustar 00000000000000error[E0277]: `{integer}` is not an iterator --> tests/ui/invalid_iter_expr.rs:3:1 | 3 | #[test_casing(2, 5)] | ^^^^^^^^^^^^^^^^^^^^ `{integer}` is not an iterator | = help: the trait `Iterator` is not implemented for `{integer}` = note: if you want to iterate between `start` until a value `end`, use the exclusive range syntax `start..end` or the inclusive range syntax `start..=end` = note: required for `{integer}` to implement `IntoIterator` = note: this error originates in the attribute macro `test_casing` (in Nightly builds, run with -Z macro-backtrace for more info) test-casing-0.1.3/tests/ui/invalid_mapping.rs000064400000000000000000000006660072674642500173740ustar 00000000000000use test_casing::test_casing; #[test_casing(2, ["test", "this"].map(String::from))] fn tested_function(#[map] _arg: &str) { // Does nothing } #[test_casing(2, ["test", "this"].map(String::from))] fn other_tested_function(#[map(mut)] _arg: &str) { // Does nothing } #[test_casing(2, ["test", "this"].map(String::from))] fn another_tested_function(#[map(ref = "String::as_str")] _arg: &str) { // Does nothing } fn main() {} test-casing-0.1.3/tests/ui/invalid_mapping.stderr000064400000000000000000000011050072674642500202400ustar 00000000000000error: expected attribute arguments in parentheses: #[map(...)] --> tests/ui/invalid_mapping.rs:4:22 | 4 | fn tested_function(#[map] _arg: &str) { | ^^^ error: unknown map transform; only `ref` is supported --> tests/ui/invalid_mapping.rs:9:32 | 9 | fn other_tested_function(#[map(mut)] _arg: &str) { | ^^^ error: expected identifier --> tests/ui/invalid_mapping.rs:14:40 | 14 | fn another_tested_function(#[map(ref = "String::as_str")] _arg: &str) { | ^^^^^^^^^^^^^^^^ test-casing-0.1.3/tests/ui/unsupported_item.rs000064400000000000000000000001370072674642500176320ustar 00000000000000use test_casing::test_casing; #[test_casing(2, ["test", "this"])] struct Dummy; fn main() {} test-casing-0.1.3/tests/ui/unsupported_item.stderr000064400000000000000000000002140072674642500205050ustar 00000000000000error: Item is not supported; use `#[test_cases] on functions --> tests/ui/unsupported_item.rs:4:1 | 4 | struct Dummy; | ^^^^^^^^^^^^^ test-casing-0.1.3/tests/ui-nightly/generic_fn.rs000064400000000000000000000002260072674642500177760ustar 00000000000000use test_casing::test_casing; #[test_casing(2, ["test", "this"])] fn tested_function>(_arg: S) { // Does nothing } fn main() {} test-casing-0.1.3/tests/ui-nightly/generic_fn.stderr000064400000000000000000000002710072674642500206550ustar 00000000000000error: generic tested functions are not supported --> tests/ui-nightly/generic_fn.rs:4:20 | 4 | fn tested_function>(_arg: S) { | ^^^^^^^^^^^^^^^ test-casing-0.1.3/tests/ui-nightly/invalid_ignore_attr.rs000064400000000000000000000004310072674642500217200ustar 00000000000000use test_casing::test_casing; #[test_casing(2, ["test", "this"])] #[ignore(_arg > 1)] fn tested_function(_arg: &str) { // Does nothing } #[test_casing(2, ["test", "this"])] #[ignore(message = "???")] fn other_tested_function(_arg: &str) { // Does nothing } fn main() {} test-casing-0.1.3/tests/ui-nightly/invalid_ignore_attr.stderr000064400000000000000000000006220072674642500226010ustar 00000000000000error: unrecognized attribute shape; should have `#[ignore] or `#[ignore = "value"]` form --> tests/ui-nightly/invalid_ignore_attr.rs:4:1 | 4 | #[ignore(_arg > 1)] | ^^^^^^^^^^^^^^^^^^^ error: unrecognized attribute shape; should have `#[ignore] or `#[ignore = "value"]` form --> tests/ui-nightly/invalid_ignore_attr.rs:10:1 | 10 | #[ignore(message = "???")] | ^^^^^^^^^^^^^^^^^^^^^^^^^^ test-casing-0.1.3/tests/ui-nightly/invalid_should_panic_attr.rs000064400000000000000000000004700072674642500231100ustar 00000000000000use test_casing::test_casing; #[test_casing(2, ["test", "this"])] #[should_panic(expected = "!", bogus = true)] fn tested_function(_arg: &str) { // Does nothing } #[test_casing(2, ["test", "this"])] #[should_panic(expected = 777)] fn other_tested_function(_arg: &str) { // Does nothing } fn main() {} test-casing-0.1.3/tests/ui-nightly/invalid_should_panic_attr.stderr000064400000000000000000000005770072674642500237770ustar 00000000000000error: attribute should have a single field `expected = "value"` --> tests/ui-nightly/invalid_should_panic_attr.rs:4:32 | 4 | #[should_panic(expected = "!", bogus = true)] | ^^^^^ error: expected string literal --> tests/ui-nightly/invalid_should_panic_attr.rs:10:27 | 10 | #[should_panic(expected = 777)] | ^^^ test-casing-0.1.3/tests/ui.rs000064400000000000000000000004370072674642500142270ustar 00000000000000//! UI tests for various compilation failures. #[test] fn ui() { let t = trybuild::TestCases::new(); t.compile_fail("tests/ui/*.rs"); } #[cfg(feature = "nightly")] #[test] fn nightly_ui() { let t = trybuild::TestCases::new(); t.compile_fail("tests/ui-nightly/*.rs"); } test-casing-0.1.3/tests/version_match.rs000064400000000000000000000004130072674642500164450ustar 00000000000000use version_sync::{assert_html_root_url_updated, assert_markdown_deps_updated}; #[test] fn readme_is_in_sync() { assert_markdown_deps_updated!("README.md"); } #[test] fn html_root_url_is_in_sync() { assert_html_root_url_updated!("src/lib.rs"); }