From 106268b63bedd252fc285778e3bd99b6e16c8608 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 13 Oct 2025 14:07:14 -0500 Subject: [PATCH 1/3] feat: Create distributed-list --- Cargo.lock | 13 ++ crates/distributed_list/CHANGELOG.md | 11 ++ crates/distributed_list/Cargo.toml | 35 ++++ crates/distributed_list/LICENSE-APACHE | 202 ++++++++++++++++++++++++ crates/distributed_list/LICENSE-MIT | 19 +++ crates/distributed_list/README.md | 26 +++ crates/distributed_list/src/lib.rs | 85 ++++++++++ crates/distributed_list/src/list.rs | 91 +++++++++++ crates/distributed_list/tests/simple.rs | 15 ++ 9 files changed, 497 insertions(+) create mode 100644 crates/distributed_list/CHANGELOG.md create mode 100644 crates/distributed_list/Cargo.toml create mode 100644 crates/distributed_list/LICENSE-APACHE create mode 100644 crates/distributed_list/LICENSE-MIT create mode 100644 crates/distributed_list/README.md create mode 100644 crates/distributed_list/src/lib.rs create mode 100644 crates/distributed_list/src/list.rs create mode 100644 crates/distributed_list/tests/simple.rs diff --git a/Cargo.lock b/Cargo.lock index 5afcf16..d80b513 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,6 +91,19 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "ctor" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59c9b8bdf64ee849747c1b12eb861d21aa47fa161564f48332f1afe2373bf899" + +[[package]] +name = "distributed-list" +version = "0.0.2" +dependencies = [ + "ctor", +] + [[package]] name = "dunce" version = "1.0.5" diff --git a/crates/distributed_list/CHANGELOG.md b/crates/distributed_list/CHANGELOG.md new file mode 100644 index 0000000..2d24313 --- /dev/null +++ b/crates/distributed_list/CHANGELOG.md @@ -0,0 +1,11 @@ +# Change Log +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/) +and this project adheres to [Semantic Versioning](https://semver.org/). + + +## [Unreleased] - ReleaseDate + + +[Unreleased]: https://github.com/epage/pytest-rs/compare/8d68adbaefcd320bc8785d23bcb050c9802fd47a...HEAD diff --git a/crates/distributed_list/Cargo.toml b/crates/distributed_list/Cargo.toml new file mode 100644 index 0000000..06457c6 --- /dev/null +++ b/crates/distributed_list/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "distributed-list" +version = "0.0.2" +description = "Register and iterate through static elements" +categories = [] +keywords = ["linkage", "ctor"] +repository.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true +include.workspace = true + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--generate-link-to-definition"] + +[package.metadata.release] +pre-release-replacements = [ + {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, + {file="CHANGELOG.md", search="\\.\\.\\.HEAD", replace="...{{tag_name}}", exactly=1}, + {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, + {file="CHANGELOG.md", search="", replace="\n## [Unreleased] - ReleaseDate\n", exactly=1}, + {file="CHANGELOG.md", search="", replace="\n[Unreleased]: https://github.com/epage/pytest-rs/compare/{{tag_name}}...HEAD", exactly=1}, +] + +[features] +default = [] + +[dependencies] +ctor = { version = "0.6.0", default-features = false, features = [] } + +[dev-dependencies] + +[lints] +workspace = true diff --git a/crates/distributed_list/LICENSE-APACHE b/crates/distributed_list/LICENSE-APACHE new file mode 100644 index 0000000..8f71f43 --- /dev/null +++ b/crates/distributed_list/LICENSE-APACHE @@ -0,0 +1,202 @@ + 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. + diff --git a/crates/distributed_list/LICENSE-MIT b/crates/distributed_list/LICENSE-MIT new file mode 100644 index 0000000..a2d0108 --- /dev/null +++ b/crates/distributed_list/LICENSE-MIT @@ -0,0 +1,19 @@ +Copyright (c) Individual contributors + +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. diff --git a/crates/distributed_list/README.md b/crates/distributed_list/README.md new file mode 100644 index 0000000..1f94c29 --- /dev/null +++ b/crates/distributed_list/README.md @@ -0,0 +1,26 @@ +# distributed-list + +> Register and iterate through static elements + +[![Documentation](https://img.shields.io/badge/docs-master-blue.svg)][Documentation] +![License](https://img.shields.io/crates/l/distributed-list.svg) +[![Crates Status](https://img.shields.io/crates/v/distributed-list.svg)](https://crates.io/crates/distributed-list) + +## License + +Licensed under either of + +* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or ) +* MIT license ([LICENSE-MIT](LICENSE-MIT) or ) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you, as defined in the Apache-2.0 +license, shall be dual-licensed as above, without any additional terms or +conditions. + +[Crates.io]: https://crates.io/crates/distributed-list +[Documentation]: https://docs.rs/distributed-list diff --git a/crates/distributed_list/src/lib.rs b/crates/distributed_list/src/lib.rs new file mode 100644 index 0000000..04122f2 --- /dev/null +++ b/crates/distributed_list/src/lib.rs @@ -0,0 +1,85 @@ +mod list; + +pub use list::DistributedList; + +#[doc(hidden)] +pub mod _private { + pub use ctor::declarative::ctor; +} + +/// Push `$value` into `$root` +/// +/// # Example +/// +/// ```rust +/// # use distributed_list::DistributedList; +/// # use distributed_list::push; +/// +/// static REGISTRY: DistributedList = DistributedList::root(); +/// +/// push!(REGISTRY, ONE: usize = 1); +/// push!(REGISTRY, TWO: usize = 2); +/// push!(REGISTRY, THREE: usize = 3); +/// +/// for entry in REGISTRY.iter() { +/// println!("{entry}"); +/// } +/// ``` +/// +/// ```rust +/// # use distributed_list::DistributedList; +/// # use distributed_list::push; +/// +/// static REGISTRY: DistributedList = DistributedList::root(); +/// +/// fn foo() { +/// push!(REGISTRY, _: usize = 1); +/// } +/// fn bar() { +/// push!(REGISTRY, _: usize = 2); +/// } +/// fn baz() { +/// push!(REGISTRY, _: usize = 3); +/// } +/// +/// for entry in REGISTRY.iter() { +/// println!("{entry}"); +/// } +/// ``` +#[macro_export] +macro_rules! push { + ($root:path, _ : $ty:ty = $value:expr) => { + $crate::_private::ctor! { + #[ctor] + unsafe fn anonymous_pushes_require_unique_scope() { + static ITEM: $ty = $value; + + // Report type errors + let _: &$crate::DistributedList<$ty> = &$root; + + static ENTRY: $crate::DistributedList<$ty> = $crate::DistributedList::new(&ITEM); + + $root.push(&ENTRY); + } + } + }; + ($root:path, $name:ident : $ty:ty = $value:expr) => { + static $name: $ty = { + { + $crate::_private::ctor! { + #[ctor] + unsafe fn push() { + // Report type errors + let _: &$crate::DistributedList<$ty> = &$root; + + static ENTRY: $crate::DistributedList<$ty> = $crate::DistributedList::new(&$name); + + $root.push(&ENTRY); + } + } + } + + $value + }; + }; +} diff --git a/crates/distributed_list/src/list.rs b/crates/distributed_list/src/list.rs new file mode 100644 index 0000000..dbdd51f --- /dev/null +++ b/crates/distributed_list/src/list.rs @@ -0,0 +1,91 @@ +// Adapted from https://github.com/nvzqz/divan/blob/a773ab9974be97fd0d7d33d7e9fb2d4413dbc797/src/entry/list.rs + +use std::{ + ptr, + sync::atomic::{AtomicPtr, Ordering as AtomicOrdering}, +}; + +/// Linked list of entries. +pub struct DistributedList { + entry: Option<&'static T>, + // This is implemented in a thread-safe way despite the fact that constructors + // are run single-threaded. + next: AtomicPtr, +} + +impl DistributedList { + /// Dereferences the `next` pointer. + #[inline] + fn next(&self) -> Option<&Self> { + // SAFETY: `next` is only assigned by `push`, which always receives a + // 'static lifetime. + unsafe { self.next.load(AtomicOrdering::Relaxed).as_ref() } + } +} + +// Externally used by macros or tests. +#[allow(missing_docs)] +impl DistributedList { + /// Create an empty list + #[inline] + pub const fn root() -> Self { + Self { + entry: None, + next: AtomicPtr::new(ptr::null_mut()), + } + } + + /// Create a new node + /// + /// See [`DistributedList::push`] + #[inline] + pub const fn new(entry: &'static T) -> Self { + Self { + entry: Some(entry), + next: AtomicPtr::new(ptr::null_mut()), + } + } + + /// Iterate over entries starting at `self` + #[inline] + pub fn iter(&self) -> impl Iterator { + let mut list = Some(self); + std::iter::from_fn(move || -> Option> { + let current = list?; + list = current.next(); + Some(current.entry.as_ref().copied()) + }) + .flatten() + } + + /// Inserts `other` to the front of the list. + /// + /// # Safety + /// + /// This function must be safe to call before `main`. + #[inline] + pub fn push(&'static self, other: &'static Self) { + let mut old_next = self.next.load(AtomicOrdering::Relaxed); + loop { + // Each publicly-created instance has `list.next` be null, so we can + // simply store `self.next` there. + other.next.store(old_next, AtomicOrdering::Release); + + // SAFETY: The content of `other` can already be seen, so we don't + // need to strongly order reads into it. + let other = other as *const Self as *mut Self; + match self.next.compare_exchange_weak( + old_next, + other, + AtomicOrdering::AcqRel, + AtomicOrdering::Acquire, + ) { + // Successfully wrote our thread's value to the list. + Ok(_) => return, + + // Lost the race, store winner's value in `other.next`. + Err(new) => old_next = new, + } + } + } +} diff --git a/crates/distributed_list/tests/simple.rs b/crates/distributed_list/tests/simple.rs new file mode 100644 index 0000000..4963ccc --- /dev/null +++ b/crates/distributed_list/tests/simple.rs @@ -0,0 +1,15 @@ +use distributed_list::push; +use distributed_list::DistributedList; + +static REGISTRY: DistributedList = DistributedList::root(); + +push!(REGISTRY, ONE: usize = 1); +push!(REGISTRY, TWO: usize = 2); +push!(REGISTRY, THREE: usize = 3); + +#[test] +fn check_elements() { + let mut elements = REGISTRY.iter().copied().collect::>(); + elements.sort(); + assert_eq!(elements, vec![1, 2, 3]); +} From 4f5e41ba957536c4699bc410ce5a38de1ca2db3e Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 13 Oct 2025 14:18:47 -0500 Subject: [PATCH 2/3] feat(macro): Add proc-macro -> declarative wrapper --- Cargo.lock | 4 + crates/libtest2-proc-macro/CHANGELOG.md | 11 ++ crates/libtest2-proc-macro/Cargo.toml | 33 ++++ crates/libtest2-proc-macro/LICENSE-APACHE | 202 ++++++++++++++++++++++ crates/libtest2-proc-macro/LICENSE-MIT | 19 ++ crates/libtest2-proc-macro/README.md | 26 +++ crates/libtest2-proc-macro/src/lib.rs | 116 +++++++++++++ 7 files changed, 411 insertions(+) create mode 100644 crates/libtest2-proc-macro/CHANGELOG.md create mode 100644 crates/libtest2-proc-macro/Cargo.toml create mode 100644 crates/libtest2-proc-macro/LICENSE-APACHE create mode 100644 crates/libtest2-proc-macro/LICENSE-MIT create mode 100644 crates/libtest2-proc-macro/README.md create mode 100644 crates/libtest2-proc-macro/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index d80b513..00aff82 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -292,6 +292,10 @@ dependencies = [ "snapbox", ] +[[package]] +name = "libtest2-proc-macro" +version = "0.0.3" + [[package]] name = "linux-raw-sys" version = "0.11.0" diff --git a/crates/libtest2-proc-macro/CHANGELOG.md b/crates/libtest2-proc-macro/CHANGELOG.md new file mode 100644 index 0000000..2a20ce7 --- /dev/null +++ b/crates/libtest2-proc-macro/CHANGELOG.md @@ -0,0 +1,11 @@ +# Change Log +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/) +and this project adheres to [Semantic Versioning](https://semver.org/). + + +## [Unreleased] - ReleaseDate + + +[Unreleased]: https://github.com/epage/pytest-rs/compare/9d9263a628ea7d17e39cf5bfa22ee190fb6e3cc7...HEAD diff --git a/crates/libtest2-proc-macro/Cargo.toml b/crates/libtest2-proc-macro/Cargo.toml new file mode 100644 index 0000000..f93a379 --- /dev/null +++ b/crates/libtest2-proc-macro/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "libtest2-proc-macro" +version = "0.0.3" +description = "An experimental replacement for libtest" +categories = ["development-tools::testing"] +keywords = ["libtest"] +repository.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true +include.workspace = true + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--generate-link-to-definition"] + +[package.metadata.release] +pre-release-replacements = [ + {file="CHANGELOG.md", search="Unreleased", replace="{{version}}", min=1}, + {file="CHANGELOG.md", search="\\.\\.\\.HEAD", replace="...{{tag_name}}", exactly=1}, + {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}", min=1}, + {file="CHANGELOG.md", search="", replace="\n## [Unreleased] - ReleaseDate\n", exactly=1}, + {file="CHANGELOG.md", search="", replace="\n[Unreleased]: https://github.com/epage/pytest-rs/compare/{{tag_name}}...HEAD", exactly=1}, +] + +[lib] +proc-macro = true + +[features] +default = [] + +[lints] +workspace = true diff --git a/crates/libtest2-proc-macro/LICENSE-APACHE b/crates/libtest2-proc-macro/LICENSE-APACHE new file mode 100644 index 0000000..8f71f43 --- /dev/null +++ b/crates/libtest2-proc-macro/LICENSE-APACHE @@ -0,0 +1,202 @@ + 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. + diff --git a/crates/libtest2-proc-macro/LICENSE-MIT b/crates/libtest2-proc-macro/LICENSE-MIT new file mode 100644 index 0000000..a2d0108 --- /dev/null +++ b/crates/libtest2-proc-macro/LICENSE-MIT @@ -0,0 +1,19 @@ +Copyright (c) Individual contributors + +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. diff --git a/crates/libtest2-proc-macro/README.md b/crates/libtest2-proc-macro/README.md new file mode 100644 index 0000000..7fee154 --- /dev/null +++ b/crates/libtest2-proc-macro/README.md @@ -0,0 +1,26 @@ +# libtest2-proc-macro + +> An experimental replacement for libtest + +[![Documentation](https://img.shields.io/badge/docs-master-blue.svg)][Documentation] +![License](https://img.shields.io/crates/l/libtest2-proc-macro.svg) +[![Crates Status](https://img.shields.io/crates/v/libtest2-proc-macro.svg)](https://crates.io/crates/libtest2-proc-macro) + +## License + +Licensed under either of + +* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or ) +* MIT license ([LICENSE-MIT](LICENSE-MIT) or ) + +at your option. + +### Contribution + +Unless you explicitly state otherwise, any contribution intentionally +submitted for inclusion in the work by you, as defined in the Apache-2.0 +license, shall be dual-licensed as above, without any additional terms or +conditions. + +[Crates.io]: https://crates.io/crates/libtest2-proc-macro +[Documentation]: https://docs.rs/libtest2-proc-macro diff --git a/crates/libtest2-proc-macro/src/lib.rs b/crates/libtest2-proc-macro/src/lib.rs new file mode 100644 index 0000000..43c7479 --- /dev/null +++ b/crates/libtest2-proc-macro/src/lib.rs @@ -0,0 +1,116 @@ +#![cfg_attr(docsrs, feature(doc_cfg))] +#![warn(clippy::print_stderr)] +#![warn(clippy::print_stdout)] + +use std::iter::FromIterator; + +use proc_macro::{Delimiter, Group, Ident, Punct, Spacing, Span, TokenStream, TokenTree}; + +#[allow(missing_docs)] +#[proc_macro_attribute] +pub fn test(attribute: TokenStream, item: TokenStream) -> TokenStream { + generate("test", "libtest2", attribute, item) +} + +#[allow(missing_docs)] +#[proc_macro_attribute] +pub fn main(attribute: TokenStream, item: TokenStream) -> TokenStream { + generate("main", "libtest2", attribute, item) +} + +/// Generates the equivalent of this Rust code as a `TokenStream`: +/// +/// ```nocompile +/// ::libtest2::__support::test_parse!(#[ctor] fn foo() { ... }); +/// ``` +fn generate( + macro_type: &str, + macro_crate: &str, + attribute: TokenStream, + item: TokenStream, +) -> TokenStream { + let mut inner = TokenStream::new(); + + // Search for crate_path in attributes + let mut crate_path = None; + let mut tokens = attribute.clone().into_iter().peekable(); + + while let Some(token) = tokens.next() { + if let TokenTree::Ident(ident) = &token { + if ident.to_string() == "crate_path" { + // Look for = + if let Some(TokenTree::Punct(punct)) = tokens.next() { + if punct.as_char() == '=' { + // Collect tokens until comma or end + let mut path = TokenStream::new(); + while let Some(token) = tokens.peek() { + match token { + TokenTree::Punct(p) if p.as_char() == ',' => { + tokens.next(); + break; + } + _ => { + path.extend(std::iter::once(tokens.next().unwrap())); + } + } + } + crate_path = Some(path); + break; + } + } + } + } + } + + if attribute.is_empty() { + // #[test] + inner.extend([ + TokenTree::Punct(Punct::new('#', Spacing::Alone)), + TokenTree::Group(Group::new( + Delimiter::Bracket, + TokenStream::from_iter([TokenTree::Ident(Ident::new( + macro_type, + Span::call_site(), + ))]), + )), + ]); + } else { + inner.extend([ + TokenTree::Punct(Punct::new('#', Spacing::Alone)), + TokenTree::Group(Group::new( + Delimiter::Bracket, + TokenStream::from_iter([ + TokenTree::Ident(Ident::new(macro_type, Span::call_site())), + TokenTree::Group(Group::new(Delimiter::Parenthesis, attribute)), + ]), + )), + ]); + } + + inner.extend(item); + + let mut invoke = crate_path.unwrap_or_else(|| { + TokenStream::from_iter([ + TokenTree::Punct(Punct::new(':', Spacing::Joint)), + TokenTree::Punct(Punct::new(':', Spacing::Alone)), + TokenTree::Ident(Ident::new(macro_crate, Span::call_site())), + ]) + }); + + invoke.extend([ + TokenTree::Punct(Punct::new(':', Spacing::Joint)), + TokenTree::Punct(Punct::new(':', Spacing::Alone)), + TokenTree::Ident(Ident::new("_private", Span::call_site())), + TokenTree::Punct(Punct::new(':', Spacing::Joint)), + TokenTree::Punct(Punct::new(':', Spacing::Alone)), + TokenTree::Ident(Ident::new( + &format!("{macro_type}_parse"), + Span::call_site(), + )), + TokenTree::Punct(Punct::new('!', Spacing::Alone)), + TokenTree::Group(Group::new(Delimiter::Parenthesis, inner)), + TokenTree::Punct(Punct::new(';', Spacing::Alone)), + ]); + + invoke +} From 59b83c510b4ef04723d11f8cd9cd3b034e565959 Mon Sep 17 00:00:00 2001 From: Ed Page Date: Mon, 13 Oct 2025 15:03:23 -0500 Subject: [PATCH 3/3] feat(test2): Provide a `#[test]` macro --- Cargo.lock | 2 + crates/libtest2-proc-macro/CHANGELOG.md | 2 +- crates/libtest2/Cargo.toml | 2 + crates/libtest2/examples/simple.rs | 14 +-- crates/libtest2/src/case.rs | 24 ++++ crates/libtest2/src/lib.rs | 21 +++- crates/libtest2/src/macros.rs | 48 ++++++-- .../libtest2/tests/testsuite/all_passing.rs | 6 +- crates/libtest2/tests/testsuite/argfile.rs | 13 +- crates/libtest2/tests/testsuite/mixed_bag.rs | 113 ++++++++++-------- crates/libtest2/tests/testsuite/panic.rs | 5 +- 11 files changed, 172 insertions(+), 78 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 00aff82..bfaae6c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -259,9 +259,11 @@ dependencies = [ name = "libtest2" version = "0.0.3" dependencies = [ + "distributed-list", "dunce", "escargot", "libtest2-harness", + "libtest2-proc-macro", "once_cell_polyfill", "pathdiff", "snapbox", diff --git a/crates/libtest2-proc-macro/CHANGELOG.md b/crates/libtest2-proc-macro/CHANGELOG.md index 2a20ce7..6d9cef6 100644 --- a/crates/libtest2-proc-macro/CHANGELOG.md +++ b/crates/libtest2-proc-macro/CHANGELOG.md @@ -8,4 +8,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/). ## [Unreleased] - ReleaseDate -[Unreleased]: https://github.com/epage/pytest-rs/compare/9d9263a628ea7d17e39cf5bfa22ee190fb6e3cc7...HEAD +[Unreleased]: https://github.com/epage/pytest-rs/compare/106268b63bedd252fc285778e3bd99b6e16c8608...HEAD diff --git a/crates/libtest2/Cargo.toml b/crates/libtest2/Cargo.toml index 468e7e9..419e47c 100644 --- a/crates/libtest2/Cargo.toml +++ b/crates/libtest2/Cargo.toml @@ -29,7 +29,9 @@ color = ["libtest2-harness/color"] threads = ["libtest2-harness/threads"] [dependencies] +distributed-list = { version = "0.0.2", path = "../distributed_list" } libtest2-harness = { version = "0.0.3", path = "../libtest2-harness" } +libtest2-proc-macro = { version = "0.0.3", path = "../libtest2-proc-macro" } [dev-dependencies] dunce = "1.0.4" diff --git a/crates/libtest2/examples/simple.rs b/crates/libtest2/examples/simple.rs index 1ec2b2b..3b65b13 100644 --- a/crates/libtest2/examples/simple.rs +++ b/crates/libtest2/examples/simple.rs @@ -2,31 +2,31 @@ use libtest2::RunError; use libtest2::RunResult; use libtest2::TestContext; -libtest2::main!( - check_toph, - check_katara, - check_sokka, - long_computation, - compile_fail_dummy -); +#[libtest2::main] +fn main() {} // Tests +#[libtest2::test] fn check_toph(_context: &TestContext) -> RunResult { Ok(()) } +#[libtest2::test] fn check_katara(_context: &TestContext) -> RunResult { Ok(()) } +#[libtest2::test] fn check_sokka(_context: &TestContext) -> RunResult { Err(RunError::fail("Sokka tripped and fell :(")) } +#[libtest2::test] fn long_computation(context: &TestContext) -> RunResult { context.ignore_for("slow")?; std::thread::sleep(std::time::Duration::from_secs(1)); Ok(()) } +#[libtest2::test] fn compile_fail_dummy(_context: &TestContext) -> RunResult { Ok(()) } diff --git a/crates/libtest2/src/case.rs b/crates/libtest2/src/case.rs index 914cc4e..079e45b 100644 --- a/crates/libtest2/src/case.rs +++ b/crates/libtest2/src/case.rs @@ -5,6 +5,28 @@ use libtest2_harness::TestKind; use crate::RunResult; use crate::TestContext; +#[derive(Copy, Clone)] +pub struct DynCase(pub &'static dyn Case); + +impl Case for DynCase { + fn name(&self) -> &str { + self.0.name() + } + fn kind(&self) -> TestKind { + self.0.kind() + } + fn source(&self) -> Option<&Source> { + self.0.source() + } + fn exclusive(&self, context: &TestContext) -> bool { + self.0.exclusive(context) + } + + fn run(&self, context: &TestContext) -> RunResult { + self.0.run(context) + } +} + pub struct FnCase { name: String, #[allow(clippy::type_complexity)] @@ -58,6 +80,8 @@ pub fn main(cases: impl IntoIterator) { ::std::process::exit(1); } }; + let mut cases = cases.into_iter().collect::>(); + cases.sort_by_key(|c| c.name().to_owned()); let harness = match harness.discover(cases) { Ok(harness) => harness, Err(err) => { diff --git a/crates/libtest2/src/lib.rs b/crates/libtest2/src/lib.rs index 06254d3..61f5d17 100644 --- a/crates/libtest2/src/lib.rs +++ b/crates/libtest2/src/lib.rs @@ -12,19 +12,21 @@ //! harness = false //! ``` //! -//! And in `tests/mytest.rs` you would call [`main!`], passing it each of your tests: +//! And in `tests/mytest.rs` you would wrap `main` with [`#[main]`]: //! //! ```no_run //! # use libtest2::RunError; //! # use libtest2::RunResult; //! # use libtest2::TestContext; +//! #[libtest2::test] //! fn check_toph(_context: &TestContext) -> RunResult { //! Ok(()) //! } //! -//! libtest2::main!(check_toph); +//! #[libtest2::main] +//! fn main() { +//! } //! ``` -//! #![cfg_attr(docsrs, feature(doc_cfg))] //#![warn(clippy::print_stderr)] @@ -35,15 +37,24 @@ mod macros; #[doc(hidden)] pub mod _private { - pub use crate::_main as main; + pub use distributed_list::push; + pub use distributed_list::DistributedList; + pub use libtest2_harness::Case; + pub use libtest2_harness::Source; + pub use libtest2_harness::TestKind; + + pub use crate::_main_parse as main_parse; + pub use crate::_test_parse as test_parse; + pub use crate::case::DynCase; } -pub use _private::main; pub use case::main; pub use case::FnCase; pub use libtest2_harness::RunError; pub use libtest2_harness::RunResult; pub use libtest2_harness::TestContext; +pub use libtest2_proc_macro::main; +pub use libtest2_proc_macro::test; #[doc = include_str!("../README.md")] #[cfg(doctest)] diff --git a/crates/libtest2/src/macros.rs b/crates/libtest2/src/macros.rs index 82e44ef..6113b33 100644 --- a/crates/libtest2/src/macros.rs +++ b/crates/libtest2/src/macros.rs @@ -1,11 +1,45 @@ -/// Expands to the test harness #[macro_export] -macro_rules! _main { - ( $( $test:path ),* $(,)*) => { +macro_rules! _main_parse { + (#[main] fn main $($item:tt)*) => { + static TESTS: $crate::_private::DistributedList<$crate::_private::DynCase> = $crate::_private::DistributedList::root(); + fn main() { - $crate::main([ - $($crate::FnCase::test(::std::stringify!($test), $test)),* - ]); + fn inner $($item)* + + inner(); + $crate::main(TESTS.iter().copied()); } - } + }; +} + +#[macro_export] +#[allow(clippy::crate_in_macro_def)] // accessing item defined by `_main_parse` +macro_rules! _test_parse { + (#[test] fn $name:ident $($item:tt)*) => { + #[allow(non_camel_case_types)] + struct $name; + + impl $crate::_private::Case for $name { + fn name(&self) -> &str { + $crate::_private::push!(crate::TESTS, _: $crate::_private::DynCase = $crate::_private::DynCase(&$name)); + + stringify!($name) + } + fn kind(&self) -> $crate::_private::TestKind { + Default::default() + } + fn source(&self) -> Option<&$crate::_private::Source> { + None + } + fn exclusive(&self, _: &$crate::TestContext) -> bool { + false + } + + fn run(&self, context: &$crate::TestContext) -> $crate::RunResult { + fn run $($item)* + + run(context) + } + } + }; } diff --git a/crates/libtest2/tests/testsuite/all_passing.rs b/crates/libtest2/tests/testsuite/all_passing.rs index 7937300..4b98cf8 100644 --- a/crates/libtest2/tests/testsuite/all_passing.rs +++ b/crates/libtest2/tests/testsuite/all_passing.rs @@ -7,16 +7,20 @@ fn test_cmd() -> snapbox::cmd::Command { let (bin, current_dir) = BIN.get_or_init(|| { let package_root = crate::util::new_test( r#" -libtest2::main!(foo, bar, barro); +#[libtest2::main] +fn main() {} +#[libtest2::test] fn foo(_context: &libtest2::TestContext) -> libtest2::RunResult { Ok(()) } +#[libtest2::test] fn bar(_context: &libtest2::TestContext) -> libtest2::RunResult { Ok(()) } +#[libtest2::test] fn barro(_context: &libtest2::TestContext) -> libtest2::RunResult { Ok(()) } diff --git a/crates/libtest2/tests/testsuite/argfile.rs b/crates/libtest2/tests/testsuite/argfile.rs index da6b241..c2a3fc0 100644 --- a/crates/libtest2/tests/testsuite/argfile.rs +++ b/crates/libtest2/tests/testsuite/argfile.rs @@ -7,20 +7,25 @@ fn test_cmd() -> snapbox::cmd::Command { let (bin, current_dir) = BIN.get_or_init(|| { let package_root = crate::util::new_test( r#" -libtest2::main!(one, two, three, one_two); +#[libtest2::main] +fn main() {} +#[libtest2::test] fn one(_context: &libtest2::TestContext) -> libtest2::RunResult { Ok(()) } +#[libtest2::test] fn two(_context: &libtest2::TestContext) -> libtest2::RunResult { Ok(()) } +#[libtest2::test] fn three(_context: &libtest2::TestContext) -> libtest2::RunResult { Ok(()) } +#[libtest2::test] fn one_two(_context: &libtest2::TestContext) -> libtest2::RunResult { Ok(()) } @@ -95,9 +100,9 @@ fn list() { 0, str![[r#" one: test -two: test -three: test one_two: test +three: test +two: test 4 tests @@ -105,7 +110,7 @@ one_two: test "#]], str![[r#" one: test -two: test +one_two: test ... 4 tests diff --git a/crates/libtest2/tests/testsuite/mixed_bag.rs b/crates/libtest2/tests/testsuite/mixed_bag.rs index 876f920..26fa68a 100644 --- a/crates/libtest2/tests/testsuite/mixed_bag.rs +++ b/crates/libtest2/tests/testsuite/mixed_bag.rs @@ -7,40 +7,49 @@ fn test_cmd() -> snapbox::cmd::Command { let (bin, current_dir) = BIN.get_or_init(|| { let package_root = crate::util::new_test( r#" -libtest2::main!(cat, dog, fox, bunny, frog, owl, fly, bear); +#[libtest2::main] +fn main() {} +#[libtest2::test] fn cat(_context: &libtest2::TestContext) -> libtest2::RunResult { Ok(()) } +#[libtest2::test] fn dog(_context: &libtest2::TestContext) -> libtest2::RunResult { Err(libtest2::RunError::fail("was not a good boy")) } +#[libtest2::test] fn fox(_context: &libtest2::TestContext) -> libtest2::RunResult { Ok(()) } +#[libtest2::test] fn bunny(context: &libtest2::TestContext) -> libtest2::RunResult { context.ignore_for("fails")?; Err(libtest2::RunError::fail("jumped too high")) } +#[libtest2::test] fn frog(context: &libtest2::TestContext) -> libtest2::RunResult { context.ignore_for("slow")?; Ok(()) } +#[libtest2::test] fn owl(context: &libtest2::TestContext) -> libtest2::RunResult { context.ignore_for("fails")?; Err(libtest2::RunError::fail("broke neck")) } +#[libtest2::test] fn fly(context: &libtest2::TestContext) -> libtest2::RunResult { context.ignore_for("fails")?; Ok(()) } +#[libtest2::test] fn bear(context: &libtest2::TestContext) -> libtest2::RunResult { context.ignore_for("fails")?; Err(libtest2::RunError::fail("no honey")) @@ -227,28 +236,28 @@ fn list() { &["--list"], 0, str![[r#" +bear: test +bunny: test cat: test dog: test +fly: test fox: test -bunny: test frog: test owl: test -fly: test -bear: test 8 tests "#]], str![[r#" +bear: test +bunny: test cat: test dog: test +fly: test fox: test -bunny: test frog: test owl: test -fly: test -bear: test 8 tests @@ -263,28 +272,28 @@ fn list_ignored() { &["--list", "--ignored"], 0, str![[r#" +bear: test +bunny: test cat: test dog: test +fly: test fox: test -bunny: test frog: test owl: test -fly: test -bear: test 8 tests "#]], str![[r#" +bear: test +bunny: test cat: test dog: test +fly: test fox: test -bunny: test frog: test owl: test -fly: test -bear: test 8 tests @@ -299,16 +308,16 @@ fn list_with_filter() { &["--list", "a"], 0, str![[r#" -cat: test bear: test +cat: test 2 tests "#]], str![[r#" -cat: test bear: test +cat: test 2 tests @@ -323,8 +332,8 @@ fn list_with_specified_order() { &["--list", "--exact", "owl", "fox", "bunny", "frog"], 0, str![[r#" -fox: test bunny: test +fox: test frog: test owl: test @@ -333,8 +342,8 @@ owl: test "#]], str![[r#" -fox: test bunny: test +fox: test frog: test owl: test @@ -718,48 +727,48 @@ fn list_json() { { "elapsed_s": "[..]", "event": "discover_case", - "name": "cat" + "name": "bear" }, { "elapsed_s": "[..]", "event": "discover_case", - "name": "dog", + "name": "bunny", "selected": false }, { "elapsed_s": "[..]", "event": "discover_case", - "name": "fox", - "selected": false + "name": "cat" }, { "elapsed_s": "[..]", "event": "discover_case", - "name": "bunny", + "name": "dog", "selected": false }, { "elapsed_s": "[..]", "event": "discover_case", - "name": "frog", + "name": "fly", "selected": false }, { "elapsed_s": "[..]", "event": "discover_case", - "name": "owl", + "name": "fox", "selected": false }, { "elapsed_s": "[..]", "event": "discover_case", - "name": "fly", + "name": "frog", "selected": false }, { "elapsed_s": "[..]", "event": "discover_case", - "name": "bear" + "name": "owl", + "selected": false }, { "elapsed_s": "[..]", @@ -778,48 +787,48 @@ fn list_json() { { "elapsed_s": "[..]", "event": "discover_case", - "name": "cat" + "name": "bear" }, { "elapsed_s": "[..]", "event": "discover_case", - "name": "dog", + "name": "bunny", "selected": false }, { "elapsed_s": "[..]", "event": "discover_case", - "name": "fox", - "selected": false + "name": "cat" }, { "elapsed_s": "[..]", "event": "discover_case", - "name": "bunny", + "name": "dog", "selected": false }, { "elapsed_s": "[..]", "event": "discover_case", - "name": "frog", + "name": "fly", "selected": false }, { "elapsed_s": "[..]", "event": "discover_case", - "name": "owl", + "name": "fox", "selected": false }, { "elapsed_s": "[..]", "event": "discover_case", - "name": "fly", + "name": "frog", "selected": false }, { "elapsed_s": "[..]", "event": "discover_case", - "name": "bear" + "name": "owl", + "selected": false }, { "elapsed_s": "[..]", @@ -846,48 +855,48 @@ fn test_json() { { "elapsed_s": "[..]", "event": "discover_case", - "name": "cat" + "name": "bear" }, { "elapsed_s": "[..]", "event": "discover_case", - "name": "dog", + "name": "bunny", "selected": false }, { "elapsed_s": "[..]", "event": "discover_case", - "name": "fox", - "selected": false + "name": "cat" }, { "elapsed_s": "[..]", "event": "discover_case", - "name": "bunny", + "name": "dog", "selected": false }, { "elapsed_s": "[..]", "event": "discover_case", - "name": "frog", + "name": "fly", "selected": false }, { "elapsed_s": "[..]", "event": "discover_case", - "name": "owl", + "name": "fox", "selected": false }, { "elapsed_s": "[..]", "event": "discover_case", - "name": "fly", + "name": "frog", "selected": false }, { "elapsed_s": "[..]", "event": "discover_case", - "name": "bear" + "name": "owl", + "selected": false }, { "elapsed_s": "[..]", @@ -1120,42 +1129,42 @@ fn fail_fast_json() { { "elapsed_s": "[..]", "event": "discover_case", - "name": "cat" + "name": "bear" }, { "elapsed_s": "[..]", "event": "discover_case", - "name": "dog" + "name": "bunny" }, { "elapsed_s": "[..]", "event": "discover_case", - "name": "fox" + "name": "cat" }, { "elapsed_s": "[..]", "event": "discover_case", - "name": "bunny" + "name": "dog" }, { "elapsed_s": "[..]", "event": "discover_case", - "name": "frog" + "name": "fly" }, { "elapsed_s": "[..]", "event": "discover_case", - "name": "owl" + "name": "fox" }, { "elapsed_s": "[..]", "event": "discover_case", - "name": "fly" + "name": "frog" }, { "elapsed_s": "[..]", "event": "discover_case", - "name": "bear" + "name": "owl" }, { "elapsed_s": "[..]", diff --git a/crates/libtest2/tests/testsuite/panic.rs b/crates/libtest2/tests/testsuite/panic.rs index 6f6fbe1..ad4a6f7 100644 --- a/crates/libtest2/tests/testsuite/panic.rs +++ b/crates/libtest2/tests/testsuite/panic.rs @@ -7,12 +7,15 @@ fn test_cmd() -> snapbox::cmd::Command { let (bin, current_dir) = BIN.get_or_init(|| { let package_root = crate::util::new_test( r#" -libtest2::main!(passes, panics); +#[libtest2::main] +fn main() {} +#[libtest2::test] fn passes(_context: &libtest2::TestContext) -> libtest2::RunResult { Ok(()) } +#[libtest2::test] fn panics(_context: &libtest2::TestContext) -> libtest2::RunResult { panic!("uh oh") }