rstest_macros-0.26.1/.cargo_vcs_info.json0000644000000001530000000000100140110ustar { "git": { "sha1": "971f6ad05232b1fc3ca5a7b0e2830d476d683307" }, "path_in_vcs": "rstest_macros" }rstest_macros-0.26.1/Cargo.lock0000644000000142570000000000100117760ustar # This file is automatically @generated by Cargo. # It is not intended for manual editing. version = 3 [[package]] name = "aho-corasick" version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] [[package]] name = "cfg-if" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "diff" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" [[package]] name = "equivalent" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "glob" version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" [[package]] name = "hashbrown" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" [[package]] name = "indexmap" version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", "hashbrown", ] [[package]] name = "maplit" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" [[package]] name = "memchr" version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" [[package]] name = "pretty_assertions" version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" dependencies = [ "diff", "yansi", ] [[package]] name = "proc-macro-crate" version = "3.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" dependencies = [ "toml_edit", ] [[package]] name = "proc-macro2" version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" dependencies = [ "unicode-ident", ] [[package]] name = "quote" version = "1.0.38" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" dependencies = [ "proc-macro2", ] [[package]] name = "regex" version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", "regex-automata", "regex-syntax", ] [[package]] name = "regex-automata" version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", "regex-syntax", ] [[package]] name = "regex-syntax" version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "relative-path" version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" [[package]] name = "rstest" version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c721f131ce14d37eb6365d6848fe38703fff9b961c83038a926673e91769d1ef" dependencies = [ "rstest_macros 0.26.0", ] [[package]] name = "rstest_macros" version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af1954176ab4f2ced085ad2c7a1b257be46299aa0093dabaa4e078b4af25eb3c" dependencies = [ "cfg-if", "glob", "proc-macro2", "quote", "regex", "relative-path", "rustc_version", "syn", "unicode-ident", ] [[package]] name = "rstest_macros" version = "0.26.1" dependencies = [ "cfg-if", "glob", "maplit", "pretty_assertions", "proc-macro-crate", "proc-macro2", "quote", "regex", "relative-path", "rstest", "rustc_version", "syn", "unicode-ident", ] [[package]] name = "rustc_version" version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ "semver", ] [[package]] name = "semver" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cb6eb87a131f756572d7fb904f6e7b68633f09cca868c5df1c4b8d1a694bbba" [[package]] name = "syn" version = "2.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c786062daee0d6db1132800e623df74274a0a87322d8e183338e01b3d98d058" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] [[package]] name = "toml_datetime" version = "0.6.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" [[package]] name = "toml_edit" version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ "indexmap", "toml_datetime", "winnow", ] [[package]] name = "unicode-ident" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" [[package]] name = "winnow" version = "0.6.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6f5bb5257f2407a5425c6e749bfd9692192a73e70a6060516ac04f889087d68" dependencies = [ "memchr", ] [[package]] name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" rstest_macros-0.26.1/Cargo.toml0000644000000037270000000000100120210ustar # 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.70.0" name = "rstest_macros" version = "0.26.1" authors = ["Michele d'Amico "] build = "build.rs" autolib = false autobins = false autoexamples = false autotests = false autobenches = false description = """ Rust fixture based test framework. It use procedural macro to implement fixtures and table based tests. """ homepage = "https://github.com/la10736/rstest" readme = "README.md" keywords = [ "test", "fixture", ] categories = ["development-tools::testing"] license = "MIT OR Apache-2.0" repository = "https://github.com/la10736/rstest" [features] async-timeout = [] crate-name = ["dep:proc-macro-crate"] default = [ "async-timeout", "crate-name", ] [lib] name = "rstest_macros" path = "src/lib.rs" proc-macro = true [dependencies.cfg-if] version = "1.0.0" [dependencies.glob] version = "0.3.1" [dependencies.proc-macro-crate] version = "3.2.0" optional = true [dependencies.proc-macro2] version = "1.0.86" [dependencies.quote] version = "1.0.37" [dependencies.regex] version = "1.10.6" [dependencies.relative-path] version = "1.9.3" [dependencies.syn] version = "2.0.79" features = [ "full", "parsing", "extra-traits", "visit", "visit-mut", ] [dependencies.unicode-ident] version = "1.0.13" [dev-dependencies.maplit] version = "1.0.2" [dev-dependencies.pretty_assertions] version = "1.4.1" [dev-dependencies.rstest] version = "0.26.0" default-features = false [build-dependencies.rustc_version] version = "0.4.1" rstest_macros-0.26.1/Cargo.toml.orig000064400000000000000000000022621046102023000154730ustar 00000000000000[package] authors = ["Michele d'Amico "] categories = ["development-tools::testing"] description = """ Rust fixture based test framework. It use procedural macro to implement fixtures and table based tests. """ edition = "2021" homepage = "https://github.com/la10736/rstest" keywords = ["test", "fixture"] license = "MIT OR Apache-2.0" name = "rstest_macros" repository = "https://github.com/la10736/rstest" rust-version = "1.70.0" version = "0.26.1" [lib] proc-macro = true [features] async-timeout = [] default = ["async-timeout", "crate-name"] crate-name = ["dep:proc-macro-crate"] [dependencies] cfg-if = "1.0.0" glob = "0.3.1" proc-macro2 = "1.0.86" quote = "1.0.37" regex = "1.10.6" relative-path = "1.9.3" syn = { version = "2.0.79", features = [ "full", "parsing", "extra-traits", "visit", "visit-mut", ] } unicode-ident = "1.0.13" proc-macro-crate = { version = "3.2.0", optional = true } [dev-dependencies] maplit = "1.0.2" pretty_assertions = "1.4.1" rstest = { version = "0.26.0", default-features = false } rstest_reuse = { path = "../rstest_reuse" } rstest_test = { path = "../rstest_test" } [build-dependencies] rustc_version = "0.4.1" rstest_macros-0.26.1/LICENSE-APACHE000064400000000000000000000261301046102023000145300ustar 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 2018-19 Michele d'Amico 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. rstest_macros-0.26.1/LICENSE-MIT000064400000000000000000000020441046102023000142360ustar 00000000000000Copyright 2018-19 Michele d'Amico 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. rstest_macros-0.26.1/README.md000064400000000000000000000017221046102023000140630ustar 00000000000000[![Crate][crate-image]][crate-link] [![Docs][docs-image]][docs-link] [![Status][test-action-image]][test-action-link] [![Apache 2.0 Licensed][license-apache-image]][license-apache-link] [![MIT Licensed][license-mit-image]][license-mit-link] # `rstest`'s Macros Crate See [`rstest`][crate-link]. [crate-image]: https://img.shields.io/crates/v/rstest.svg [crate-link]: https://crates.io/crates/rstest [docs-image]: https://docs.rs/rstest/badge.svg [docs-link]: https://docs.rs/rstest/ [test-action-image]: https://github.com/la10736/rstest/workflows/Test/badge.svg [test-action-link]: https://github.com/la10736/rstest/actions?query=workflow:Test [license-apache-image]: https://img.shields.io/badge/license-Apache2.0-blue.svg [license-mit-image]: https://img.shields.io/badge/license-MIT-blue.svg [license-apache-link]: http://www.apache.org/licenses/LICENSE-2.0 [license-MIT-link]: http://opensource.org/licenses/MIT [reuse-crate-link]: https://crates.io/crates/rstest_reuse rstest_macros-0.26.1/build.rs000064400000000000000000000016041046102023000142500ustar 00000000000000use rustc_version::{version, version_meta, Channel}; fn allow_features() -> Option> { std::env::var("CARGO_ENCODED_RUSTFLAGS").ok().map(|args| { args.split('\u{001f}') .filter(|arg| arg.starts_with("-Zallow-features=")) .map(|arg| arg.split('=').nth(1).unwrap()) .flat_map(|features| features.split(',')) .map(|f| f.to_owned()) .collect() }) } fn can_enable_proc_macro_diagnostic() -> bool { allow_features() .map(|f| f.iter().any(|f| f == "proc_macro_diagnostic")) .unwrap_or(true) } fn main() { let ver = version().unwrap(); assert!(ver.major >= 1); match version_meta().unwrap().channel { Channel::Nightly | Channel::Dev if can_enable_proc_macro_diagnostic() => { println!("cargo:rustc-cfg=use_proc_macro_diagnostic"); } _ => {} } } rstest_macros-0.26.1/src/error.rs000064400000000000000000000311331046102023000150710ustar 00000000000000/// Module for error rendering stuff use std::collections::HashMap; use proc_macro2::TokenStream; use syn::{visit, ItemFn, Pat}; use syn::{spanned::Spanned, visit::Visit}; use crate::parse::{ fixture::FixtureInfo, rstest::{RsTestData, RsTestInfo}, }; use crate::parse::arguments::ArgumentsInfo; use crate::refident::{MaybeIdent, MaybePat}; use crate::utils::attr_is; use super::utils::fn_args_has_pat; pub mod messages { pub const DESTRUCT_WITHOUT_FROM : &str = "To destruct a fixture you should provide a path to resolve it by '#[from(...)]' attribute."; pub fn use_more_than_once(name: &str) -> String { format!("You cannot use '{name}' attribute more than once for the same argument") } } pub(crate) fn rstest(test: &ItemFn, info: &RsTestInfo) -> TokenStream { missed_arguments(test, info.data.items.iter()) .chain(duplicate_arguments(info.data.items.iter())) .chain(invalid_cases(&info.data)) .chain(case_args_without_cases(&info.data)) .chain(destruct_fixture_without_from(test, info)) .chain(test_attributes(test, &info.arguments)) .map(|e| e.to_compile_error()) .collect() } pub(crate) fn fixture(test: &ItemFn, info: &FixtureInfo) -> TokenStream { missed_arguments(test, info.data.items.iter()) .chain(duplicate_arguments(info.data.items.iter())) .chain(async_once(test, info)) .chain(generics_once(test, info)) .chain(destruct_fixture_without_from(test, info)) .map(|e| e.to_compile_error()) .collect() } fn async_once<'a>(test: &'a ItemFn, info: &FixtureInfo) -> Errors<'a> { match (test.sig.asyncness, info.arguments.get_once()) { (Some(_asyncness), Some(once)) => Box::new(std::iter::once(syn::Error::new_spanned( once, "Cannot apply #[once] to async fixture.", ))), _ => Box::new(std::iter::empty()), } } #[derive(Default)] struct SearchImpl(bool); impl<'ast> Visit<'ast> for SearchImpl { fn visit_type(&mut self, i: &'ast syn::Type) { if self.0 { return; } if let syn::Type::ImplTrait(_) = i { self.0 = true } visit::visit_type(self, i); } } impl SearchImpl { fn function_has_some_impl(f: &ItemFn) -> bool { let mut s = SearchImpl::default(); visit::visit_item_fn(&mut s, f); s.0 } } fn has_some_generics(test: &ItemFn) -> bool { !test.sig.generics.params.is_empty() || SearchImpl::function_has_some_impl(test) } fn generics_once<'a>(test: &'a ItemFn, info: &FixtureInfo) -> Errors<'a> { match (has_some_generics(test), info.arguments.get_once()) { (true, Some(once)) => Box::new(std::iter::once(syn::Error::new_spanned( once, "Cannot apply #[once] on generic fixture.", ))), _ => Box::new(std::iter::empty()), } } trait IsImplicitFixture { fn is_implicit_fixture(&self, pat: &Pat) -> bool; } impl IsImplicitFixture for FixtureInfo { fn is_implicit_fixture(&self, pat: &Pat) -> bool { !self.data.fixtures().any(|f| &f.arg == pat) } } impl IsImplicitFixture for RsTestInfo { fn is_implicit_fixture(&self, pat: &Pat) -> bool { !self.data.case_args().any(|a| a == pat) && !self.data.list_values().any(|a| &a.arg == pat) && !self.data.fixtures().any(|f| &f.arg == pat) } } fn destruct_fixture_without_from<'a>( function: &'a ItemFn, info: &'a impl IsImplicitFixture, ) -> Errors<'a> { Box::new( function .sig .inputs .iter() .filter_map(|a| a.maybe_pat().map(|p| (a, p))) .filter(|&(_, p)| p.maybe_ident().is_none()) .filter(|&(_, p)| info.is_implicit_fixture(p)) .map(|(a, _)| syn::Error::new_spanned(a, messages::DESTRUCT_WITHOUT_FROM)), ) } #[derive(Debug, Default)] pub struct ErrorsVec(Vec); pub(crate) fn _merge_errors( r1: Result, r2: Result, ) -> Result<(R1, R2), ErrorsVec> { match (r1, r2) { (Ok(r1), Ok(r2)) => Ok((r1, r2)), (Ok(_), Err(e)) | (Err(e), Ok(_)) => Err(e), (Err(mut e1), Err(mut e2)) => { e1.append(&mut e2); Err(e1) } } } macro_rules! merge_errors { ($e:expr) => { $e }; ($e:expr, $($es:expr), + $(,)?) => { crate::error::_merge_errors($e, merge_errors!($($es),*)) }; } macro_rules! composed_tuple { ($i:ident) => { $i }; ($i:ident, $($is:ident), +) => { ($i, composed_tuple!($($is),*)) }; } impl std::ops::Deref for ErrorsVec { type Target = Vec; fn deref(&self) -> &Self::Target { &self.0 } } impl std::ops::DerefMut for ErrorsVec { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } impl From for ErrorsVec { fn from(errors: syn::Error) -> Self { vec![errors].into() } } impl From> for ErrorsVec { fn from(errors: Vec) -> Self { Self(errors) } } impl From for Vec { fn from(v: ErrorsVec) -> Self { v.0 } } impl quote::ToTokens for ErrorsVec { fn to_tokens(&self, tokens: &mut TokenStream) { tokens.extend(self.0.iter().map(|e| e.to_compile_error())) } } impl From for proc_macro::TokenStream { fn from(v: ErrorsVec) -> Self { use quote::ToTokens; v.into_token_stream().into() } } type Errors<'a> = Box + 'a>; fn missed_arguments<'a, I: MaybePat + Spanned + 'a>( test: &'a ItemFn, args: impl Iterator + 'a, ) -> Errors<'a> { Box::new( args.filter_map(|it| it.maybe_pat().map(|pat| (it, pat))) .filter(move |(_, pat)| !fn_args_has_pat(test, pat)) .map(|(missed, pat)| { syn::Error::new( missed.span(), format!( "Missed argument: '{}' should be a test function argument.", pat.render_type() ), ) }), ) } fn duplicate_arguments<'a, I: MaybePat + Spanned + 'a>( args: impl Iterator + 'a, ) -> Errors<'a> { let mut used = HashMap::new(); Box::new( args.filter_map(|it| it.maybe_pat().map(|pat| (it, pat))) .filter_map(move |(it, pat)| { let is_duplicate = used.contains_key(&pat); used.insert(pat, it); match is_duplicate { true => Some((it, pat)), false => None, } }) .map(|(duplicate, pat)| { syn::Error::new( duplicate.span(), format!( "Duplicate argument: '{}' is already defined.", pat.render_type() ), ) }), ) } fn invalid_cases(params: &RsTestData) -> Errors<'_> { let n_args = params.case_args().count(); Box::new( params .cases() .filter(move |case| case.args.len() != n_args) .map(|case| { syn::Error::new_spanned( case, "Wrong case signature: should match the given parameters list.", ) }), ) } fn case_args_without_cases(params: &RsTestData) -> Errors<'_> { if !params.has_cases() { return Box::new( params .case_args() .map(|a| syn::Error::new(a.span(), "No cases for this argument.")), ); } Box::new(std::iter::empty()) } trait RenderType { fn render_type(&self) -> String; } impl RenderType for syn::Pat { fn render_type(&self) -> String { match self { syn::Pat::Ident(ref i) => i.ident.to_string(), other => format!("{other:?}"), } } } fn test_attributes<'a>(test: &'a ItemFn, arguments: &'a ArgumentsInfo) -> Errors<'a> { Box::new(async_test_without_test_attribute(test, arguments).chain(malformed_explicit_test_attr(test))) } fn async_test_without_test_attribute<'a>(test: &'a ItemFn, arguments: &'a ArgumentsInfo) -> Errors<'a> { if test.sig.asyncness.is_some() && arguments.test_attr().is_none() { let span = test.sig.ident.span(); Box::new(std::iter::once(syn::Error::new(span, "async test requires either explicit `test_attr` or implicit (attribute path ends with `test`)"))) } else { Box::new(std::iter::empty()) } } fn malformed_explicit_test_attr(test: &ItemFn) -> Errors<'_> { let Some(explicit_test_attr) = test.attrs.iter().find(|attr| attr_is(attr, "test_attr")) else { return Box::new(std::iter::empty()); }; match explicit_test_attr.meta { syn::Meta::List(_) => Box::new(std::iter::empty()), syn::Meta::Path(_) | syn::Meta::NameValue(_) => { Box::new(std::iter::once(syn::Error::new_spanned( explicit_test_attr.path(), "invalid `test_attr` syntax; should be `#[test_attr()]`", ))) } } } #[cfg(test)] mod test { use crate::{ parse::ExtendWithFunctionAttrs, test::{assert_eq, *}, }; use rstest_test::assert_in; use super::*; #[rstest] #[case::generics("fn f(){}")] #[case::const_generics("fn f(){}")] #[case::lifetimes("fn f<'a>(){}")] #[case::use_impl_in_answer("fn f() -> impl Iterator{}")] #[case::use_impl_in_arguments("fn f(it: impl Iterator){}")] #[should_panic] #[case::sanity_check_with_no_generics("fn f() {}")] fn generics_once_should_return_error(#[case] f: &str) { let f: ItemFn = f.ast(); let info = FixtureInfo::default().with_once(); let errors = generics_once(&f, &info); let out = errors .map(|e| format!("{:?}", e)) .collect::>() .join("-----------------------\n"); assert_in!(out, "Cannot apply #[once] on generic fixture."); } #[rstest] #[case::generics("fn f(){}")] #[case::const_generics("fn f(){}")] #[case::lifetimes("fn f<'a>(){}")] #[case::use_impl_in_answer("fn f() -> impl Iterator{}")] #[case::use_impl_in_arguments("fn f(it: impl Iterator){}")] fn generics_once_should_not_return_if_no_once(#[case] f: &str) { let f: ItemFn = f.ast(); let info = FixtureInfo::default(); let errors = generics_once(&f, &info); assert_eq!(0, errors.count()); } #[rstest] #[case::base_in_fixture("fn f(T{a}: T){}", FixtureInfo::default(), 1)] #[case::one_of_two("fn f(T{a}: T, #[from(f)] T{a: c}: T){}", FixtureInfo::default(), 1)] #[case::find_all( "fn f(T{a}: T, z: u32, S(a,b): S, x: u32, (f , g, h): (u32, String, f32)){}", FixtureInfo::default(), 3 )] #[case::base_in_test("fn f(T{a}: T){}", RsTestInfo::default(), 1)] #[case::not_case_or_values( "fn f(#[case] T{a}: T, #[values(T::a(),T::b())] T{v}: T, S{e}: S){}", RsTestInfo::default(), 1 )] #[case::mixed_more( r#"fn wrong_destruct_fixture( T(a, b): T, #[case] T(e, f): T, #[values(T(1, 2))] T(g, h): T, ) {}"#, RsTestInfo::default(), 1 )] fn destruct_implicit_from_should_return_error( #[case] f: &str, #[case] mut info: impl ExtendWithFunctionAttrs + IsImplicitFixture, #[case] n: usize, ) { let mut f: ItemFn = f.ast(); info.extend_with_function_attrs(&mut f).unwrap(); let errors = destruct_fixture_without_from(&f, &info); let out = errors .map(|e| format!("{:?}", e)) .collect::>() .join("\n"); assert_in!(out, messages::DESTRUCT_WITHOUT_FROM); assert_eq!(n, out.lines().count()) } #[rstest] #[case::happy_fixture("fn f(#[from(b)] T{a}: T){}", FixtureInfo::default())] #[case::happy_test("fn f(#[from(b)] T{a}: T){}", RsTestInfo::default())] #[case::some_cases_or_values( "fn f(#[case] T{a}: T, #[values(T::a(),T::b())] T{v}: T){}", RsTestInfo::default() )] fn destruct_not_implicit_should_not_return_error( #[case] f: &str, #[case] mut info: impl ExtendWithFunctionAttrs + IsImplicitFixture, ) { let mut f: ItemFn = f.ast(); info.extend_with_function_attrs(&mut f).unwrap(); let errors = destruct_fixture_without_from(&f, &info); assert_eq!(0, errors.count()); } } rstest_macros-0.26.1/src/lib.rs000064400000000000000000000035721046102023000145140ustar 00000000000000#![allow(clippy::test_attr_in_doctest)] #![allow(unexpected_cfgs)] #![cfg_attr(use_proc_macro_diagnostic, feature(proc_macro_diagnostic))] extern crate proc_macro; // Test utility module #[cfg(test)] pub(crate) mod test; #[macro_use] mod error; mod parse; mod refident; mod render; mod resolver; mod utils; use syn::{parse_macro_input, ItemFn}; use crate::parse::{fixture::FixtureInfo, rstest::RsTestInfo}; use parse::ExtendWithFunctionAttrs; use quote::ToTokens; #[allow(missing_docs)] #[proc_macro_attribute] pub fn fixture( args: proc_macro::TokenStream, input: proc_macro::TokenStream, ) -> proc_macro::TokenStream { let mut info: FixtureInfo = parse_macro_input!(args as FixtureInfo); let mut fixture = parse_macro_input!(input as ItemFn); let extend_result = info.extend_with_function_attrs(&mut fixture); let mut errors = error::fixture(&fixture, &info); if let Err(attrs_errors) = extend_result { attrs_errors.to_tokens(&mut errors); } if errors.is_empty() { render::fixture(fixture, info) } else { errors } .into() } #[allow(missing_docs)] #[proc_macro_attribute] pub fn rstest( args: proc_macro::TokenStream, input: proc_macro::TokenStream, ) -> proc_macro::TokenStream { let mut test = parse_macro_input!(input as ItemFn); let mut info = parse_macro_input!(args as RsTestInfo); let extend_result = info.extend_with_function_attrs(&mut test); let mut errors = error::rstest(&test, &info); if let Err(attrs_errors) = extend_result { attrs_errors.to_tokens(&mut errors); } if errors.is_empty() { if info.data.has_list_values() { render::matrix(test, info) } else if info.data.has_cases() { render::parametrize(test, info) } else { render::single(test, info) } } else { errors } .into() } rstest_macros-0.26.1/src/parse/arguments.rs000064400000000000000000000235761046102023000170730ustar 00000000000000use std::collections::{HashMap, HashSet}; use quote::format_ident; use syn::{FnArg, Ident, Pat}; use crate::{ refident::{IntoPat, MaybeIdent, MaybePatType, MaybePatTypeMut}, resolver::pat_invert_mutability, }; #[derive(PartialEq, Debug, Clone, Copy)] #[allow(dead_code)] #[derive(Default)] pub(crate) enum FutureArg { #[default] None, Define, Await, } #[derive(Clone, PartialEq, Default, Debug)] pub(crate) struct ArgumentInfo { future: FutureArg, by_ref: bool, ignore: bool, inner_pat: Option, // Optional pat used to inject data and call test function } impl ArgumentInfo { fn future(future: FutureArg) -> Self { Self { future, ..Default::default() } } fn by_ref() -> Self { Self { by_ref: true, ..Default::default() } } fn ignore() -> Self { Self { ignore: true, ..Default::default() } } fn inner_pat(pat: Pat) -> Self { Self { inner_pat: Some(pat), ..Default::default() } } fn is_future(&self) -> bool { use FutureArg::*; matches!(self.future, Define | Await) } fn is_future_await(&self) -> bool { use FutureArg::*; matches!(self.future, Await) } fn is_by_ref(&self) -> bool { self.by_ref } fn is_ignore(&self) -> bool { self.ignore } } #[derive(Clone, PartialEq, Default, Debug)] struct Args { args: HashMap, } impl Args { fn get(&self, pat: &Pat) -> Option<&ArgumentInfo> { self.args .get(pat) .or_else(|| self.args.get(&pat_invert_mutability(pat))) } fn entry(&mut self, pat: Pat) -> std::collections::hash_map::Entry<'_, Pat, ArgumentInfo> { self.args.entry(pat) } } #[derive(Clone, PartialEq, Debug)] pub enum TestAttr { InAttrs, Explicit(Box), } impl From for TestAttr { fn from(attr: syn::Attribute) -> Self { TestAttr::Explicit(attr.into()) } } #[derive(Clone, PartialEq, Default, Debug)] pub(crate) struct ArgumentsInfo { args: Args, is_global_await: bool, once: Option, contexts: HashSet, test_attr: Option, } impl ArgumentsInfo { pub(crate) fn test_attr(&self) -> Option<&TestAttr> { self.test_attr.as_ref() } } impl ArgumentsInfo { pub(crate) fn set_future(&mut self, pat: Pat, kind: FutureArg) { self.args .entry(pat) .and_modify(|v| v.future = kind) .or_insert_with(|| ArgumentInfo::future(kind)); } pub(crate) fn set_futures(&mut self, futures: impl Iterator) { futures.for_each(|(pat, k)| self.set_future(pat, k)); } pub(crate) fn set_global_await(&mut self, is_global_await: bool) { self.is_global_await = is_global_await; } pub(crate) fn set_test_attr(&mut self, test_attr: Option) { self.test_attr = test_attr; } #[allow(dead_code)] pub(crate) fn add_future(&mut self, pat: Pat) { self.set_future(pat, FutureArg::Define); } pub(crate) fn is_future(&self, pat: &Pat) -> bool { self.args .get(pat) .map(|arg| arg.is_future()) .unwrap_or_default() } pub(crate) fn is_future_await(&self, pat: &Pat) -> bool { match self.args.get(pat) { Some(arg) => arg.is_future_await() || (arg.is_future() && self.is_global_await()), None => false, } } pub(crate) fn is_global_await(&self) -> bool { self.is_global_await } pub(crate) fn set_once(&mut self, once: Option) { self.once = once } pub(crate) fn get_once(&self) -> Option<&syn::Attribute> { self.once.as_ref() } pub(crate) fn is_once(&self) -> bool { self.get_once().is_some() } pub(crate) fn set_by_ref(&mut self, pat: Pat) { self.args .entry(pat) .and_modify(|v| v.by_ref = true) .or_insert_with(ArgumentInfo::by_ref); } pub(crate) fn set_ignore(&mut self, pat: Pat) { self.args .entry(pat) .and_modify(|v| v.ignore = true) .or_insert_with(ArgumentInfo::ignore); } pub(crate) fn set_by_refs(&mut self, by_refs: impl Iterator) { by_refs.for_each(|pat| self.set_by_ref(pat)); } pub(crate) fn set_ignores(&mut self, ignores: impl Iterator) { ignores.for_each(|pat| self.set_ignore(pat)); } pub(crate) fn is_by_refs(&self, id: &Pat) -> bool { self.args .get(id) .map(|arg| arg.is_by_ref()) .unwrap_or_default() } pub(crate) fn is_ignore(&self, pat: &Pat) -> bool { self.args .get(pat) .map(|arg| arg.is_ignore()) .unwrap_or_default() } pub(crate) fn set_inner_pat(&mut self, pat: Pat, inner: Pat) { self.args .entry(pat) .and_modify(|v| v.inner_pat = Some(inner.clone())) .or_insert_with(|| ArgumentInfo::inner_pat(inner)); } pub(crate) fn set_inner_ident(&mut self, pat: Pat, ident: Ident) { self.set_inner_pat(pat, ident.into_pat()); } pub(crate) fn inner_pat<'arguments: 'pat_ref, 'pat_ref>( &'arguments self, id: &'pat_ref Pat, ) -> &'pat_ref Pat { self.args .get(id) .and_then(|arg| arg.inner_pat.as_ref()) .unwrap_or(id) } pub(crate) fn register_inner_destructored_idents_names(&mut self, item_fn: &syn::ItemFn) { let mut anonymous_destruct = 0_usize; // On the signature we remove all destruct arguments and replace them with `__destruct_{id}` // This is just to define the new arguments and local variable that we use in the test // and coll the original signature that should preserve the destruct arguments. for arg in item_fn.sig.inputs.iter() { if let Some(pt) = arg.maybe_pat_type() { if pt.maybe_ident().is_none() { anonymous_destruct += 1; let ident = format_ident!("__destruct_{}", anonymous_destruct); self.set_inner_ident(pt.pat.as_ref().clone(), ident); } } } } pub(crate) fn replace_fn_args_with_related_inner_pat<'a>( &'a self, fn_args: impl Iterator + 'a, ) -> impl Iterator + 'a { fn_args.map(|mut fn_arg| { if let Some(p) = fn_arg.maybe_pat_type_mut() { p.pat = Box::new(self.inner_pat(p.pat.as_ref()).clone()); } fn_arg }) } #[allow(dead_code)] pub(crate) fn add_context(&mut self, pat: Pat) { self.contexts.insert(pat); } pub(crate) fn set_contexts(&mut self, contexts: impl Iterator) { contexts.for_each(|c| self.add_context(c)) } pub(crate) fn contexts(&self) -> impl Iterator + '_ { self.contexts.iter() } } #[cfg(test)] mod should_implement_is_future_await_logic { use super::*; use crate::test::*; #[fixture] fn info() -> ArgumentsInfo { let mut a = ArgumentsInfo::default(); a.set_future(pat("simple"), FutureArg::Define); a.set_future(pat("other_simple"), FutureArg::Define); a.set_future(pat("awaited"), FutureArg::Await); a.set_future(pat("other_awaited"), FutureArg::Await); a.set_future(pat("none"), FutureArg::None); a } #[rstest] fn no_matching_ident(info: ArgumentsInfo) { assert!(!info.is_future_await(&pat("some"))); assert!(!info.is_future_await(&pat("simple"))); assert!(!info.is_future_await(&pat("none"))); } #[rstest] fn matching_ident(info: ArgumentsInfo) { assert!(info.is_future_await(&pat("awaited"))); assert!(info.is_future_await(&pat("other_awaited"))); } #[rstest] fn global_matching_future_ident(mut info: ArgumentsInfo) { info.set_global_await(true); assert!(info.is_future_await(&pat("simple"))); assert!(info.is_future_await(&pat("other_simple"))); assert!(info.is_future_await(&pat("awaited"))); assert!(!info.is_future_await(&pat("some"))); assert!(!info.is_future_await(&pat("none"))); } } #[cfg(test)] mod should_register_inner_destructored_idents_names { use super::*; use crate::test::{assert_eq, *}; #[test] fn implement_the_correct_pat_reolver() { let item_fn = "fn test_function(A(a,b): A, (c,d,e): (u32, u32, u32), none: u32, B{s,d} : B, clean: C) {}".ast(); let mut arguments = ArgumentsInfo::default(); arguments.register_inner_destructored_idents_names(&item_fn); assert_eq!(arguments.inner_pat(&pat("A(a,b)")), &pat("__destruct_1")); assert_eq!(arguments.inner_pat(&pat("(c,d,e)")), &pat("__destruct_2")); assert_eq!(arguments.inner_pat(&pat("none")), &pat("none")); assert_eq!(arguments.inner_pat(&pat("B{s,d}")), &pat("__destruct_3")); assert_eq!(arguments.inner_pat(&pat("clean")), &pat("clean")); } #[test] fn and_replace_them_correctly() { let item_fn = "fn test_function(A(a,b): A, (c,d,e): (u32, u32, u32), none: u32, B{s,d} : B, clean: C) {}".ast(); let mut arguments = ArgumentsInfo::default(); arguments.register_inner_destructored_idents_names(&item_fn); let new_args = arguments .replace_fn_args_with_related_inner_pat(item_fn.sig.inputs.into_iter()) .filter_map(|f| f.maybe_ident().cloned()) .map(|id| id.to_string()) .collect::>() .join(" | "); assert_eq!( new_args, "__destruct_1 | __destruct_2 | none | __destruct_3 | clean" ); } } rstest_macros-0.26.1/src/parse/by_ref.rs000064400000000000000000000040221046102023000163150ustar 00000000000000use syn::{visit_mut::VisitMut, ItemFn, Pat}; use crate::error::ErrorsVec; use super::just_once::JustOnceFnArgAttributeExtractor; pub(crate) fn extract_by_ref(item_fn: &mut ItemFn) -> Result, ErrorsVec> { let mut extractor = JustOnceFnArgAttributeExtractor::from("by_ref"); extractor.visit_item_fn_mut(item_fn); extractor.take() } #[cfg(test)] mod should { use super::*; use crate::test::{assert_eq, *}; use rstest_test::assert_in; #[rstest] #[case("fn simple(a: u32) {}")] #[case("fn more(a: u32, b: &str) {}")] #[case("fn gen>(a: u32, b: S) {}")] #[case("fn attr(#[case] a: u32, #[values(1,2)] b: i32) {}")] fn not_change_anything_if_no_by_ref_attribute_found(#[case] item_fn: &str) { let mut item_fn: ItemFn = item_fn.ast(); let orig = item_fn.clone(); let by_refs = extract_by_ref(&mut item_fn).unwrap(); assert_eq!(orig, item_fn); assert!(by_refs.is_empty()); } #[rstest] #[case::simple("fn f(#[by_ref] a: &u32) {}", "fn f(a: &u32) {}", &["a"])] #[case::more_than_one( "fn f(#[by_ref] a: &u32, #[by_ref] b: &String, #[by_ref] c: &std::collection::HashMap) {}", r#"fn f(a: &u32, b: &String, c: &std::collection::HashMap) {}"#, &["a", "b", "c"])] fn extract(#[case] item_fn: &str, #[case] expected: &str, #[case] expected_refs: &[&str]) { let mut item_fn: ItemFn = item_fn.ast(); let expected: ItemFn = expected.ast(); let by_refs = extract_by_ref(&mut item_fn).unwrap(); assert_eq!(expected, item_fn); assert_eq!(by_refs, to_pats!(expected_refs)); } #[rstest] #[case::no_more_than_one("fn f(#[by_ref] #[by_ref] a: u32) {}", "more than once")] fn raise_error(#[case] item_fn: &str, #[case] message: &str) { let mut item_fn: ItemFn = item_fn.ast(); let err = extract_by_ref(&mut item_fn).unwrap_err(); assert_in!(format!("{:?}", err), message); } } rstest_macros-0.26.1/src/parse/context.rs000064400000000000000000000036671046102023000165510ustar 00000000000000use syn::{visit_mut::VisitMut, ItemFn, Pat}; use crate::error::ErrorsVec; use super::just_once::JustOnceFnArgAttributeExtractor; pub(crate) fn extract_context(item_fn: &mut ItemFn) -> Result, ErrorsVec> { let mut extractor = JustOnceFnArgAttributeExtractor::from("context"); extractor.visit_item_fn_mut(item_fn); extractor.take() } #[cfg(test)] mod should { use super::*; use crate::test::{assert_eq, *}; use rstest_test::assert_in; #[rstest] #[case("fn simple(a: u32) {}")] #[case("fn more(a: u32, b: &str) {}")] #[case("fn gen>(a: u32, b: S) {}")] #[case("fn attr(#[case] a: u32, #[values(1,2)] b: i32) {}")] fn not_change_anything_if_no_ignore_attribute_found(#[case] item_fn: &str) { let mut item_fn: ItemFn = item_fn.ast(); let orig = item_fn.clone(); let by_refs = extract_context(&mut item_fn).unwrap(); assert_eq!(orig, item_fn); assert!(by_refs.is_empty()); } #[rstest] #[case::simple("fn f(#[context] a: u32) {}", "fn f(a: u32) {}", &["a"])] #[case::more_than_one( "fn f(#[context] a: u32, #[context] b: String, #[context] c: std::collection::HashMap) {}", r#"fn f(a: u32, b: String, c: std::collection::HashMap) {}"#, &["a", "b", "c"])] fn extract(#[case] item_fn: &str, #[case] expected: &str, #[case] expected_refs: &[&str]) { let mut item_fn: ItemFn = item_fn.ast(); let expected: ItemFn = expected.ast(); let by_refs = extract_context(&mut item_fn).unwrap(); assert_eq!(expected, item_fn); assert_eq!(by_refs, to_pats!(expected_refs)); } #[test] fn raise_error() { let mut item_fn: ItemFn = "fn f(#[context] #[context] a: u32) {}".ast(); let err = extract_context(&mut item_fn).unwrap_err(); assert_in!(format!("{:?}", err), "more than once"); } } rstest_macros-0.26.1/src/parse/expressions.rs000064400000000000000000000010661046102023000174360ustar 00000000000000use syn::{ parse::{Parse, ParseStream, Result}, Expr, Token, }; pub(crate) struct Expressions(Vec); impl Expressions { pub(crate) fn take(self) -> Vec { self.0 } } impl Parse for Expressions { fn parse(input: ParseStream) -> Result { let values = input .parse_terminated(Parse::parse, Token![,])? .into_iter() .collect(); Ok(Self(values)) } } impl From for Vec { fn from(expressions: Expressions) -> Self { expressions.0 } } rstest_macros-0.26.1/src/parse/fixture.rs000064400000000000000000000573101046102023000165450ustar 00000000000000/// `fixture`'s related data and parsing use syn::{ parse::{Parse, ParseStream}, parse_quote, visit_mut::VisitMut, Expr, FnArg, Ident, ItemFn, Pat, Token, }; use super::{ arguments::ArgumentsInfo, extract_default_return_type, extract_defaults, extract_fixtures, extract_partials_return_type, future::{extract_futures, extract_global_awt}, parse_vector_trailing_till_double_comma, Attributes, ExtendWithFunctionAttrs, Fixture, }; use crate::{ error::ErrorsVec, parse::extract_once, refident::{IntoPat, MaybeIdent, MaybePat, MaybePatTypeMut, RefPat}, utils::attr_is, }; use crate::{parse::Attribute, utils::attr_in}; use proc_macro2::TokenStream; use quote::{format_ident, ToTokens}; #[derive(PartialEq, Debug, Default)] pub(crate) struct FixtureInfo { pub(crate) data: FixtureData, pub(crate) attributes: FixtureModifiers, pub(crate) arguments: ArgumentsInfo, } impl Parse for FixtureModifiers { fn parse(input: ParseStream) -> syn::Result { Ok(input.parse::()?.into()) } } impl Parse for FixtureInfo { fn parse(input: ParseStream) -> syn::Result { Ok(if input.is_empty() { Default::default() } else { Self { data: input.parse()?, attributes: input .parse::() .or_else(|_| Ok(Default::default())) .and_then(|_| input.parse())?, arguments: Default::default(), } }) } } impl ExtendWithFunctionAttrs for FixtureInfo { fn extend_with_function_attrs( &mut self, item_fn: &mut ItemFn, ) -> std::result::Result<(), ErrorsVec> { let composed_tuple!( fixtures, defaults, default_return_type, partials_return_type, once, futures, global_awt ) = merge_errors!( extract_fixtures(item_fn), extract_defaults(item_fn), extract_default_return_type(item_fn), extract_partials_return_type(item_fn), extract_once(item_fn), extract_futures(item_fn), extract_global_awt(item_fn) )?; self.data.items.extend( fixtures .into_iter() .map(|f| f.into()) .chain(defaults.into_iter().map(|d| d.into())), ); if let Some(return_type) = default_return_type { self.attributes.set_default_return_type(return_type); } for (id, return_type) in partials_return_type { self.attributes.set_partial_return_type(id, return_type); } self.arguments.set_once(once); self.arguments.set_global_await(global_awt); self.arguments.set_futures(futures.into_iter()); self.arguments .register_inner_destructored_idents_names(item_fn); Ok(()) } } fn parse_attribute_args_just_once<'a, T: Parse>( attributes: impl Iterator, name: &str, ) -> (Option, Vec) { let mut errors = Vec::new(); let val = attributes .filter(|&a| attr_is(a, name)) .map(|a| (a, a.parse_args::())) .fold(None, |first, (a, res)| match (first, res) { (None, Ok(parsed)) => Some(parsed), (first, Err(err)) => { errors.push(err); first } (first, _) => { errors.push(syn::Error::new_spanned( a, crate::error::messages::use_more_than_once(name), )); first } }); (val, errors) } /// Simple struct used to visit function attributes and extract Fixtures and /// eventually parsing errors #[derive(Default)] pub(crate) struct FixturesFunctionExtractor(pub(crate) Vec, pub(crate) Vec); impl VisitMut for FixturesFunctionExtractor { fn visit_fn_arg_mut(&mut self, node: &mut FnArg) { let arg = match node.maybe_pat_type_mut() { Some(pt) => pt, None => return, }; let (extracted, remain): (Vec<_>, Vec<_>) = std::mem::take(&mut arg.attrs) .into_iter() .partition(|attr| attr_in(attr, &["with", "from"])); arg.attrs = remain; let (pos, errors) = parse_attribute_args_just_once(extracted.iter(), "with"); self.1.extend(errors); let (resolve, errors): (Option, _) = parse_attribute_args_just_once(extracted.iter(), "from"); self.1.extend(errors); match (resolve, arg.pat.maybe_ident()) { (Some(res), _) => self.0.push(Fixture::new( arg.pat.as_ref().clone(), res, pos.unwrap_or_default(), )), (None, Some(ident)) if pos.is_some() => self.0.push(Fixture::new( arg.pat.as_ref().clone(), ident.clone().into(), pos.unwrap_or_default(), )), (None, None) if pos.is_some() => { self.1.push(syn::Error::new_spanned( node, crate::error::messages::DESTRUCT_WITHOUT_FROM, )); } _ => {} } } } #[derive(PartialEq, Debug, Default)] pub(crate) struct FixtureData { pub items: Vec, } impl FixtureData { pub(crate) fn fixtures(&self) -> impl Iterator { self.items.iter().filter_map(|f| match f { FixtureItem::Fixture(ref fixture) => Some(fixture.as_ref()), _ => None, }) } pub(crate) fn values(&self) -> impl Iterator { self.items.iter().filter_map(|f| match f { FixtureItem::ArgumentValue(ref value) => Some(value.as_ref()), _ => None, }) } } impl Parse for FixtureData { fn parse(input: ParseStream) -> syn::Result { if input.peek(Token![::]) { Ok(Default::default()) } else { Ok(Self { items: parse_vector_trailing_till_double_comma::<_, Token![,]>(input)?, }) } } } #[derive(PartialEq, Debug)] pub(crate) struct ArgumentValue { pub arg: Pat, pub expr: Expr, } impl ArgumentValue { pub(crate) fn new(arg: Pat, expr: Expr) -> Self { Self { arg, expr } } } #[derive(PartialEq, Debug)] pub(crate) enum FixtureItem { Fixture(Box), ArgumentValue(Box), } impl From for FixtureItem { fn from(f: Fixture) -> Self { FixtureItem::Fixture(f.into()) } } impl Parse for FixtureItem { fn parse(input: ParseStream) -> syn::Result { if input.peek2(Token![=]) { input.parse::().map(|v| v.into()) } else { input.parse::().map(|v| v.into()) } } } impl RefPat for FixtureItem { fn pat(&self) -> &Pat { match self { FixtureItem::Fixture(ref fix) => &fix.arg, FixtureItem::ArgumentValue(ref av) => &av.arg, } } } impl MaybePat for FixtureItem { fn maybe_pat(&self) -> Option<&syn::Pat> { Some(self.pat()) } } impl ToTokens for FixtureItem { fn to_tokens(&self, tokens: &mut TokenStream) { self.pat().to_tokens(tokens) } } impl From for FixtureItem { fn from(av: ArgumentValue) -> Self { FixtureItem::ArgumentValue(Box::new(av)) } } impl Parse for ArgumentValue { fn parse(input: ParseStream) -> syn::Result { let name: Ident = input.parse()?; let _eq: Token![=] = input.parse()?; let expr = input.parse()?; Ok(ArgumentValue::new(name.into_pat(), expr)) } } wrap_attributes!(FixtureModifiers); impl FixtureModifiers { pub(crate) const DEFAULT_RET_ATTR: &'static str = "default"; pub(crate) const PARTIAL_RET_ATTR: &'static str = "partial_"; pub(crate) fn extract_default_type(&self) -> Option { self.extract_type(Self::DEFAULT_RET_ATTR) } pub(crate) fn extract_partial_type(&self, pos: usize) -> Option { self.extract_type(&format!("{}{}", Self::PARTIAL_RET_ATTR, pos)) } pub(crate) fn set_default_return_type(&mut self, return_type: syn::Type) { self.inner.attributes.push(Attribute::Type( format_ident!("{}", Self::DEFAULT_RET_ATTR), Box::new(return_type), )) } pub(crate) fn set_partial_return_type(&mut self, id: usize, return_type: syn::Type) { self.inner.attributes.push(Attribute::Type( format_ident!("{}{}", Self::PARTIAL_RET_ATTR, id), Box::new(return_type), )) } fn extract_type(&self, attr_name: &str) -> Option { self.iter() .filter_map(|m| match m { Attribute::Type(name, t) if name == attr_name => Some(parse_quote! { -> #t}), _ => None, }) .next() } } #[cfg(test)] mod should { use super::*; use crate::test::{assert_eq, *}; mod parse { use super::{assert_eq, *}; fn parse_fixture>(fixture_data: S) -> FixtureInfo { parse_meta(fixture_data) } #[test] fn happy_path() { let data = parse_fixture( r#"my_fixture(42, "other"), other(vec![42]), value=42, other_value=vec![1.0] :: trace :: no_trace(some)"#, ); let expected = FixtureInfo { data: vec![ fixture("my_fixture", &["42", r#""other""#]).into(), fixture("other", &["vec![42]"]).into(), arg_value("value", "42").into(), arg_value("other_value", "vec![1.0]").into(), ] .into(), attributes: Attributes { attributes: vec![ Attribute::attr("trace"), Attribute::tagged("no_trace", vec!["some"]), ], } .into(), arguments: Default::default(), }; assert_eq!(expected, data); } #[test] fn some_literals() { let args_expressions = literal_expressions_str(); let fixture = parse_fixture(&format!("my_fixture({})", args_expressions.join(", "))); let args = fixture.data.fixtures().next().unwrap().positional.clone(); assert_eq!(to_args!(args_expressions), args.0); } #[test] fn empty_fixtures() { let data = parse_fixture(r#"::trace::no_trace(some)"#); let expected = FixtureInfo { attributes: Attributes { attributes: vec![ Attribute::attr("trace"), Attribute::tagged("no_trace", vec!["some"]), ], } .into(), ..Default::default() }; assert_eq!(expected, data); } #[test] fn empty_attributes() { let data = parse_fixture(r#"my_fixture(42, "other")"#); let expected = FixtureInfo { data: vec![fixture("my_fixture", &["42", r#""other""#]).into()].into(), ..Default::default() }; assert_eq!(expected, data); } #[rstest] #[case("first(42),", 1)] #[case("first(42), second=42,", 2)] #[case(r#"fixture(42, "other"), :: trace"#, 1)] #[case(r#"second=42, fixture(42, "other"), :: trace"#, 2)] fn should_accept_trailing_comma(#[case] input: &str, #[case] expected: usize) { let info: FixtureInfo = input.ast(); assert_eq!( expected, info.data.fixtures().count() + info.data.values().count() ); } } } #[cfg(test)] mod extend { use super::*; use crate::test::{assert_eq, *}; use syn::ItemFn; mod should { use super::{assert_eq, *}; #[test] fn use_with_attributes() { let to_parse = r#" fn my_fix(#[with(2)] f1: &str, #[with(vec![1,2], "s")] f2: u32) {} "#; let mut item_fn: ItemFn = to_parse.ast(); let mut info = FixtureInfo::default(); info.extend_with_function_attrs(&mut item_fn).unwrap(); let expected = FixtureInfo { data: vec![ fixture("f1", &["2"]).into(), fixture("f2", &["vec![1,2]", r#""s""#]).into(), ] .into(), ..Default::default() }; assert!(!format!("{:?}", item_fn).contains("with")); assert_eq!(expected, info); } #[test] fn rename_with_attributes() { let mut item_fn = r#" fn test_fn( #[from(long_fixture_name)] #[with(42, "other")] short: u32, #[from(sub_module::fix)] f: u32, #[from(simple)] s: &str, no_change: i32) { } "# .ast(); let expected = FixtureInfo { data: vec![ fixture("short", &["42", r#""other""#]) .with_resolve("long_fixture_name") .into(), fixture("f", &[]).with_resolve("sub_module::fix").into(), fixture("s", &[]).with_resolve("simple").into(), ] .into(), ..Default::default() }; let mut data = FixtureInfo::default(); data.extend_with_function_attrs(&mut item_fn).unwrap(); assert_eq!(expected, data); } #[test] fn use_default_values_attributes() { let to_parse = r#" fn my_fix(#[default(2)] f1: &str, #[default((vec![1,2], "s"))] f2: (Vec, &str)) {} "#; let mut item_fn: ItemFn = to_parse.ast(); let mut info = FixtureInfo::default(); info.extend_with_function_attrs(&mut item_fn).unwrap(); let expected = FixtureInfo { data: vec![ arg_value("f1", "2").into(), arg_value("f2", r#"(vec![1,2], "s")"#).into(), ] .into(), ..Default::default() }; assert!(!format!("{:?}", item_fn).contains("default")); assert_eq!(expected, info); } #[test] fn find_default_return_type() { let mut item_fn: ItemFn = r#" #[simple] #[first(comp)] #[second::default] #[default(impl Iterator)] #[last::more] fn my_fix(f1: I, f2: J) -> impl Iterator {} "# .ast(); let mut info = FixtureInfo::default(); info.extend_with_function_attrs(&mut item_fn).unwrap(); assert_eq!( info.attributes.extract_default_type(), Some(parse_quote! { -> impl Iterator }) ); assert_eq!( attrs("#[simple]#[first(comp)]#[second::default]#[last::more]"), item_fn.attrs ); } #[test] fn find_partials_return_type() { let mut item_fn: ItemFn = r#" #[simple] #[first(comp)] #[second::default] #[partial_1(impl Iterator)] #[partial_2(impl Iterator)] #[last::more] fn my_fix(f1: I, f2: J, f3: K) -> impl Iterator {} "# .ast(); let mut info = FixtureInfo::default(); info.extend_with_function_attrs(&mut item_fn).unwrap(); assert_eq!( info.attributes.extract_partial_type(1), Some(parse_quote! { -> impl Iterator }) ); assert_eq!( info.attributes.extract_partial_type(2), Some(parse_quote! { -> impl Iterator }) ); assert_eq!( attrs("#[simple]#[first(comp)]#[second::default]#[last::more]"), item_fn.attrs ); } #[test] fn find_once_attribute() { let mut item_fn: ItemFn = r#" #[simple] #[first(comp)] #[second::default] #[once] #[last::more] fn my_fix(f1: I, f2: J, f3: K) -> impl Iterator {} "# .ast(); let mut info = FixtureInfo::default(); info.extend_with_function_attrs(&mut item_fn).unwrap(); assert!(info.arguments.is_once()); } #[test] fn no_once_attribute() { let mut item_fn: ItemFn = r#" fn my_fix(f1: I, f2: J, f3: K) -> impl Iterator {} "# .ast(); let mut info = FixtureInfo::default(); info.extend_with_function_attrs(&mut item_fn).unwrap(); assert!(!info.arguments.is_once()); } #[rstest] fn extract_future() { let mut item_fn = "fn f(#[future] a: u32, b: u32) {}".ast(); let expected = "fn f(a: u32, b: u32) {}".ast(); let mut info = FixtureInfo::default(); info.extend_with_function_attrs(&mut item_fn).unwrap(); assert_eq!(item_fn, expected); assert!(info.arguments.is_future(&pat("a"))); assert!(!info.arguments.is_future(&pat("b"))); } mod raise_error { use super::{assert_eq, *}; use rstest_test::assert_in; #[test] fn for_invalid_expressions() { let mut item_fn: ItemFn = r#" fn my_fix(#[with(valid)] f1: &str, #[with(with(,.,))] f2: u32, #[with(with(use))] f3: u32) {} "# .ast(); let errors = FixtureInfo::default() .extend_with_function_attrs(&mut item_fn) .unwrap_err(); assert_eq!(2, errors.len()); } #[test] fn for_invalid_default_type() { let mut item_fn: ItemFn = r#" #[default(notype)] fn my_fix() -> I {} "# .ast(); let errors = FixtureInfo::default() .extend_with_function_attrs(&mut item_fn) .unwrap_err(); assert_eq!(1, errors.len()); } #[test] fn with_used_more_than_once() { let mut item_fn: ItemFn = r#" fn my_fix(#[with(1)] #[with(2)] fixture1: &str, #[with(1)] #[with(2)] #[with(3)] fixture2: &str) {} "# .ast(); let errors = FixtureInfo::default() .extend_with_function_attrs(&mut item_fn) .err() .unwrap_or_default(); assert_eq!(3, errors.len()); } #[test] fn fixture_destruct_without_from() { let mut item_fn: ItemFn = r#" fn my_fix(#[with(1)] T{a}: T) {} "# .ast(); let errors = FixtureInfo::default() .extend_with_function_attrs(&mut item_fn) .err() .unwrap_or_default(); assert_in!(errors[0].to_string(), "destruct"); } #[test] fn from_used_more_than_once() { let mut item_fn: ItemFn = r#" fn my_fix(#[from(a)] #[from(b)] fixture1: &str, #[from(c)] #[from(d)] #[from(e)] fixture2: &str) {} "# .ast(); let errors = FixtureInfo::default() .extend_with_function_attrs(&mut item_fn) .err() .unwrap_or_default(); assert_eq!(3, errors.len()); } #[test] fn future_is_used_more_than_once() { let mut item_fn: ItemFn = r#" fn my_fix(#[future] #[future] fixture1: u32) {} "# .ast(); let errors = FixtureInfo::default() .extend_with_function_attrs(&mut item_fn) .err() .unwrap_or_default(); assert_eq!(1, errors.len()); assert_in!(errors[0].to_string(), "more than once"); } #[test] fn default_used_more_than_once() { let mut item_fn: ItemFn = r#" fn my_fix(#[default(2)] #[default(3)] f1: u32) {} "# .ast(); let errors = FixtureInfo::default() .extend_with_function_attrs(&mut item_fn) .err() .unwrap_or_default(); assert_eq!(1, errors.len()); assert_in!(errors[0].to_string(), "more than once"); } #[test] fn if_once_is_defined_more_than_once() { let mut item_fn: ItemFn = r#" #[once] #[once] fn my_fix() -> I {} "# .ast(); let mut info = FixtureInfo::default(); let error = info.extend_with_function_attrs(&mut item_fn).unwrap_err(); assert_in!( format!("{:?}", error).to_lowercase(), "cannot use #[once] more than once" ); } #[test] fn if_default_is_defined_more_than_once() { let mut item_fn: ItemFn = r#" #[default(u32)] #[default(u32)] fn my_fix() -> I {} "# .ast(); let mut info = FixtureInfo::default(); let error = info.extend_with_function_attrs(&mut item_fn).unwrap_err(); assert_in!( format!("{:?}", error).to_lowercase(), "cannot use #[default] more than once" ); } #[test] fn for_invalid_partial_type() { let mut item_fn: ItemFn = r#" #[partial_1(notype)] fn my_fix(x: I, y: u32) -> I {} "# .ast(); let errors = FixtureInfo::default() .extend_with_function_attrs(&mut item_fn) .unwrap_err(); assert_eq!(1, errors.len()); } #[test] fn if_partial_is_not_correct() { let mut item_fn: ItemFn = r#" #[partial_not_a_number(u32)] fn my_fix(f1: I, f2: &str) -> I {} "# .ast(); let mut info = FixtureInfo::default(); let error = info.extend_with_function_attrs(&mut item_fn).unwrap_err(); assert_in!( format!("{:?}", error).to_lowercase(), "invalid partial syntax" ); } } } } rstest_macros-0.26.1/src/parse/future.rs000064400000000000000000000165571046102023000164010ustar 00000000000000use quote::{format_ident, ToTokens}; use syn::{visit_mut::VisitMut, FnArg, Ident, ItemFn, Pat, PatType, Type}; use crate::{error::ErrorsVec, refident::MaybeType}; use super::{ arguments::FutureArg, just_once::{ AttrBuilder, JustOnceFnArgAttributeExtractor, JustOnceFnAttributeExtractor, Validator, }, }; pub(crate) fn extract_futures(item_fn: &mut ItemFn) -> Result, ErrorsVec> { let mut extractor = JustOnceFnArgAttributeExtractor::::new("future"); extractor.visit_item_fn_mut(item_fn); extractor.take() } pub(crate) fn extract_global_awt(item_fn: &mut ItemFn) -> Result { let mut extractor = JustOnceFnAttributeExtractor::::new("awt"); extractor.visit_item_fn_mut(item_fn); extractor.take().map(|inner| inner.is_some()) } struct GlobalAwtBuilder; impl AttrBuilder for GlobalAwtBuilder { type Out = (); fn build(_attr: syn::Attribute, _ident: &ItemFn) -> syn::Result { Ok(()) } } impl Validator for GlobalAwtBuilder {} struct FutureBuilder; impl AttrBuilder for FutureBuilder { type Out = (Pat, FutureArg); fn build(attr: syn::Attribute, pat: &Pat) -> syn::Result { Self::compute_arguments_kind(&attr).map(|kind| (pat.clone(), kind)) } } impl Validator for FutureBuilder { fn validate(arg: &FnArg) -> syn::Result<()> { arg.as_future_impl_type().map(|_| ()).ok_or_else(|| { syn::Error::new_spanned( arg.maybe_type().unwrap().into_token_stream(), "This type cannot used to generate impl Future.".to_owned(), ) }) } } impl FutureBuilder { fn compute_arguments_kind(arg: &syn::Attribute) -> syn::Result { if matches!(arg.meta, syn::Meta::Path(_)) { Ok(FutureArg::Define) } else { match arg.parse_args::>()? { Some(awt) if awt == format_ident!("awt") => Ok(FutureArg::Await), None => Ok(FutureArg::Define), Some(invalid) => Err(syn::Error::new_spanned( arg.parse_args::>()?.into_token_stream(), format!("Invalid '{invalid}' #[future(...)] arg."), )), } } } } pub(crate) trait MaybeFutureImplType { fn as_future_impl_type(&self) -> Option<&Type>; fn as_mut_future_impl_type(&mut self) -> Option<&mut Type>; } impl MaybeFutureImplType for FnArg { fn as_future_impl_type(&self) -> Option<&Type> { match self { FnArg::Typed(PatType { ty, .. }) if can_impl_future(ty.as_ref()) => Some(ty.as_ref()), _ => None, } } fn as_mut_future_impl_type(&mut self) -> Option<&mut Type> { match self { FnArg::Typed(PatType { ty, .. }) if can_impl_future(ty.as_ref()) => Some(ty.as_mut()), _ => None, } } } fn can_impl_future(ty: &Type) -> bool { use Type::*; !matches!( ty, Group(_) | ImplTrait(_) | Infer(_) | Macro(_) | Never(_) | Slice(_) | TraitObject(_) | Verbatim(_) ) } #[cfg(test)] mod should { use super::*; use crate::test::{assert_eq, *}; use rstest_test::assert_in; #[rstest] #[case("fn simple(a: u32) {}")] #[case("fn more(a: u32, b: &str) {}")] #[case("fn gen>(a: u32, b: S) {}")] #[case("fn attr(#[case] a: u32, #[values(1,2)] b: i32) {}")] fn not_change_anything_if_no_future_attribute_found(#[case] item_fn: &str) { let mut item_fn: ItemFn = item_fn.ast(); let orig = item_fn.clone(); let composed_tuple!(futures, awt) = merge_errors!( extract_futures(&mut item_fn), extract_global_awt(&mut item_fn) ) .unwrap(); assert_eq!(orig, item_fn); assert!(futures.is_empty()); assert!(!awt); } #[rstest] #[case::simple("fn f(#[future] a: u32) {}", "fn f(a: u32) {}", &[("a", FutureArg::Define)], false)] #[case::global_awt("#[awt] fn f(a: u32) {}", "fn f(a: u32) {}", &[], true)] #[case::global_awt_with_inner_function("#[awt] fn f(a: u32) { fn g(){} }", "fn f(a: u32) { fn g(){} }", &[], true)] #[case::simple_awaited("fn f(#[future(awt)] a: u32) {}", "fn f(a: u32) {}", &[("a", FutureArg::Await)], false)] #[case::simple_awaited_and_global("#[awt] fn f(#[future(awt)] a: u32) {}", "fn f(a: u32) {}", &[("a", FutureArg::Await)], true)] #[case::more_than_one( "fn f(#[future] a: u32, #[future(awt)] b: String, #[future()] c: std::collection::HashMap) {}", r#"fn f(a: u32, b: String, c: std::collection::HashMap) {}"#, &[("a", FutureArg::Define), ("b", FutureArg::Await), ("c", FutureArg::Define)], false, )] #[case::just_one( "fn f(a: u32, #[future] b: String) {}", r#"fn f(a: u32, b: String) {}"#, &[("b", FutureArg::Define)], false, )] #[case::just_one_awaited( "fn f(a: u32, #[future(awt)] b: String) {}", r#"fn f(a: u32, b: String) {}"#, &[("b", FutureArg::Await)], false, )] fn extract( #[case] item_fn: &str, #[case] expected: &str, #[case] expected_futures: &[(&str, FutureArg)], #[case] expected_awt: bool, ) { let mut item_fn: ItemFn = item_fn.ast(); let expected: ItemFn = expected.ast(); let composed_tuple!(futures, awt) = merge_errors!( extract_futures(&mut item_fn), extract_global_awt(&mut item_fn) ) .unwrap(); assert_eq!(expected, item_fn); assert_eq!( futures, expected_futures .into_iter() .map(|(id, a)| (pat(id), *a)) .collect::>() ); assert_eq!(expected_awt, awt); } #[rstest] #[case::base(r#"#[awt] fn f(a: u32) {}"#, r#"fn f(a: u32) {}"#)] #[case::two( r#" #[awt] #[awt] fn f(a: u32) {} "#, r#"fn f(a: u32) {}"# )] #[case::inner( r#" #[one] #[awt] #[two] fn f(a: u32) {} "#, r#" #[one] #[two] fn f(a: u32) {} "# )] fn remove_all_awt_attributes(#[case] item_fn: &str, #[case] expected: &str) { let mut item_fn: ItemFn = item_fn.ast(); let expected: ItemFn = expected.ast(); let _ = extract_global_awt(&mut item_fn); assert_eq!(item_fn, expected); } #[rstest] #[case::no_more_than_one("fn f(#[future] #[future] a: u32) {}", "more than once")] #[case::no_impl("fn f(#[future] a: impl AsRef) {}", "generate impl Future")] #[case::no_slice("fn f(#[future] a: [i32]) {}", "generate impl Future")] #[case::invalid_arg("fn f(#[future(other)] a: [i32]) {}", "Invalid 'other'")] #[case::no_more_than_one_awt("#[awt] #[awt] fn f(a: u32) {}", "more than once")] fn raise_error(#[case] item_fn: &str, #[case] message: &str) { let mut item_fn: ItemFn = item_fn.ast(); let err = merge_errors!( extract_futures(&mut item_fn), extract_global_awt(&mut item_fn) ) .unwrap_err(); assert_in!(format!("{:?}", err), message); } } rstest_macros-0.26.1/src/parse/ignore.rs000064400000000000000000000036601046102023000163410ustar 00000000000000use syn::{visit_mut::VisitMut, ItemFn, Pat}; use crate::error::ErrorsVec; use super::just_once::JustOnceFnArgAttributeExtractor; pub(crate) fn extract_ignores(item_fn: &mut ItemFn) -> Result, ErrorsVec> { let mut extractor = JustOnceFnArgAttributeExtractor::from("ignore"); extractor.visit_item_fn_mut(item_fn); extractor.take() } #[cfg(test)] mod should { use super::*; use crate::test::{assert_eq, *}; use rstest_test::assert_in; #[rstest] #[case("fn simple(a: u32) {}")] #[case("fn more(a: u32, b: &str) {}")] #[case("fn gen>(a: u32, b: S) {}")] #[case("fn attr(#[case] a: u32, #[values(1,2)] b: i32) {}")] fn not_change_anything_if_no_ignore_attribute_found(#[case] item_fn: &str) { let mut item_fn: ItemFn = item_fn.ast(); let orig = item_fn.clone(); let by_refs = extract_ignores(&mut item_fn).unwrap(); assert_eq!(orig, item_fn); assert!(by_refs.is_empty()); } #[rstest] #[case::simple("fn f(#[ignore] a: u32) {}", "fn f(a: u32) {}", &["a"])] #[case::more_than_one( "fn f(#[ignore] a: u32, #[ignore] b: String, #[ignore] c: std::collection::HashMap) {}", r#"fn f(a: u32, b: String, c: std::collection::HashMap) {}"#, &["a", "b", "c"])] fn extract(#[case] item_fn: &str, #[case] expected: &str, #[case] expected_refs: &[&str]) { let mut item_fn: ItemFn = item_fn.ast(); let expected: ItemFn = expected.ast(); let by_refs = extract_ignores(&mut item_fn).unwrap(); assert_eq!(expected, item_fn); assert_eq!(by_refs, to_pats!(expected_refs)); } #[test] fn raise_error() { let mut item_fn: ItemFn = "fn f(#[ignore] #[ignore] a: u32) {}".ast(); let err = extract_ignores(&mut item_fn).unwrap_err(); assert_in!(format!("{:?}", err), "more than once"); } } rstest_macros-0.26.1/src/parse/just_once.rs000064400000000000000000000135361046102023000170520ustar 00000000000000use std::marker::PhantomData; use quote::ToTokens; use syn::{visit_mut::VisitMut, Attribute, FnArg, ItemFn, Pat}; use crate::{error::ErrorsVec, refident::MaybePat, utils::attr_is}; pub trait AttrBuilder { type Out; fn build(attr: Attribute, extra: &E) -> syn::Result; } pub trait Validator { fn validate(_arg: &T) -> syn::Result<()> { Ok(()) } } impl AttrBuilder for () { type Out = Pat; fn build(_attr: Attribute, pat: &Pat) -> syn::Result { Ok(pat.clone()) } } impl AttrBuilder for () { type Out = Attribute; fn build(attr: Attribute, _item_fn: &ItemFn) -> syn::Result { Ok(attr.clone()) } } impl Validator for () {} /// Simple struct used to visit function argument attributes and extract attributes that match /// the `name`: Only one attribute is allowed for arguments. pub struct JustOnceFnArgAttributeExtractor<'a, B = ()> where B: AttrBuilder, { name: &'a str, elements: Vec, errors: Vec, _phantom: PhantomData, } impl<'a> From<&'a str> for JustOnceFnArgAttributeExtractor<'a, ()> { fn from(value: &'a str) -> Self { Self::new(value) } } impl<'a, B> JustOnceFnArgAttributeExtractor<'a, B> where B: AttrBuilder, { pub fn new(name: &'a str) -> Self { Self { name, elements: Default::default(), errors: Default::default(), _phantom: PhantomData, } } pub fn take(self) -> Result, ErrorsVec> { if self.errors.is_empty() { Ok(self.elements) } else { Err(self.errors.into()) } } } impl VisitMut for JustOnceFnArgAttributeExtractor<'_, B> where B: AttrBuilder, B: Validator, { fn visit_fn_arg_mut(&mut self, node: &mut FnArg) { let pat = match node.maybe_pat() { Some(pat) => pat.clone(), None => return, }; if let FnArg::Typed(ref mut arg) = node { // Extract interesting attributes let attrs = std::mem::take(&mut arg.attrs); let (extracted, remain): (Vec<_>, Vec<_>) = attrs.into_iter().partition(|a| attr_is(a, self.name)); arg.attrs = remain; let parsed = extracted .into_iter() .map(|attr| B::build(attr.clone(), &pat).map(|t| (attr, t))) .collect::, _>>(); match parsed { Ok(data) => match data.len() { 1 => match B::validate(node) { Ok(_) => self.elements.extend(data.into_iter().map(|(_attr, t)| t)), Err(e) => { self.errors.push(e); } }, 0 => {} _ => { self.errors .extend(data.into_iter().skip(1).map(|(attr, _t)| { syn::Error::new_spanned( attr.into_token_stream(), format!("Cannot use #[{}] more than once.", self.name), ) })); } }, Err(e) => { self.errors.push(e); } } } } } /// Simple struct used to visit function attributes and extract attributes that match /// the `name`: Only one attribute is allowed for arguments. pub struct JustOnceFnAttributeExtractor<'a, B = ()> where B: AttrBuilder, { name: &'a str, inner: Result, Vec>, _phantom: PhantomData, } impl<'a> From<&'a str> for JustOnceFnAttributeExtractor<'a, ()> { fn from(value: &'a str) -> Self { Self::new(value) } } impl<'a, B> JustOnceFnAttributeExtractor<'a, B> where B: AttrBuilder, { pub fn new(name: &'a str) -> Self { Self { name, inner: Ok(Default::default()), _phantom: PhantomData, } } pub fn take(self) -> Result, ErrorsVec> { self.inner.map_err(Into::into) } } impl VisitMut for JustOnceFnAttributeExtractor<'_, B> where B: AttrBuilder, B: Validator, { fn visit_item_fn_mut(&mut self, item_fn: &mut ItemFn) { // Extract interesting attributes let attrs = std::mem::take(&mut item_fn.attrs); let (extracted, remain): (Vec<_>, Vec<_>) = attrs.into_iter().partition(|a| attr_is(a, self.name)); item_fn.attrs = remain; let parsed = extracted .into_iter() .map(|attr| B::build(attr.clone(), item_fn).map(|t| (attr, t))) .collect::, _>>(); let mut errors = Vec::default(); let mut out = None; match parsed { Ok(data) => match data.len() { 1 => match B::validate(item_fn) { Ok(_) => { out = data.into_iter().next().map(|(_attr, t)| t); } Err(e) => { errors.push(e); } }, 0 => {} _ => { errors.extend(data.into_iter().skip(1).map(|(attr, _t)| { syn::Error::new_spanned( attr.into_token_stream(), format!("Cannot use #[{}] more than once.", self.name), ) })); } }, Err(e) => { errors.push(e); } }; if errors.is_empty() { self.inner = Ok(out); } else { self.inner = Err(errors); } } } rstest_macros-0.26.1/src/parse/macros.rs000064400000000000000000000012621046102023000163360ustar 00000000000000macro_rules! wrap_attributes { ($ident:ident) => { #[derive(Default, Debug, PartialEq, Clone)] pub(crate) struct $ident { inner: Attributes, } impl From for $ident { fn from(inner: Attributes) -> Self { $ident { inner } } } impl $ident { fn iter(&self) -> impl Iterator { self.inner.attributes.iter() } } impl $ident { #[allow(dead_code)] pub(crate) fn append(&mut self, attr: Attribute) { self.inner.attributes.push(attr) } } }; } rstest_macros-0.26.1/src/parse/mod.rs000064400000000000000000000441011046102023000156300ustar 00000000000000use proc_macro2::TokenStream; use syn::{ parse::{Parse, ParseStream}, parse_quote, punctuated::Punctuated, token::{self, Async, Paren}, visit_mut::VisitMut, FnArg, Ident, ItemFn, Pat, Token, }; use crate::{ error::ErrorsVec, parse::just_once::{AttrBuilder, JustOnceFnAttributeExtractor, Validator}, refident::{IntoPat, MaybeIdent, MaybePat}, utils::{attr_is, attr_starts_with}, }; use fixture::{ArgumentValue, FixtureModifiers, FixturesFunctionExtractor}; use quote::ToTokens; use testcase::TestCase; use self::{ expressions::Expressions, just_once::JustOnceFnArgAttributeExtractor, vlist::ValueList, }; // To use the macros this should be the first one module #[macro_use] pub(crate) mod macros; pub(crate) mod arguments; pub(crate) mod by_ref; pub(crate) mod context; pub(crate) mod expressions; pub(crate) mod fixture; pub(crate) mod future; pub(crate) mod ignore; pub(crate) mod just_once; pub(crate) mod rstest; pub(crate) mod testcase; pub(crate) mod vlist; pub(crate) trait ExtendWithFunctionAttrs { fn extend_with_function_attrs( &mut self, item_fn: &mut ItemFn, ) -> std::result::Result<(), ErrorsVec>; } #[derive(Default, Debug, PartialEq, Clone)] pub(crate) struct Attributes { pub(crate) attributes: Vec, } impl Parse for Attributes { fn parse(input: ParseStream) -> syn::Result { let vars = Punctuated::::parse_terminated(input)?; Ok(Attributes { attributes: vars.into_iter().collect(), }) } } #[derive(Debug, PartialEq, Clone)] pub(crate) enum Attribute { Attr(Ident), Tagged(Ident, Vec), Type(Ident, Box), } impl Parse for Attribute { fn parse(input: ParseStream) -> syn::Result { if input.peek2(Token![<]) { let tag = input.parse()?; let _open = input.parse::()?; let inner = input.parse()?; let _close = input.parse::]>()?; Ok(Attribute::Type(tag, inner)) } else if input.peek2(Token![::]) { let inner = input.parse()?; Ok(Attribute::Attr(inner)) } else if input.peek2(token::Paren) { let tag = input.parse()?; let content; let _ = syn::parenthesized!(content in input); let args = Punctuated::::parse_terminated(&content)? .into_iter() .map(IntoPat::into_pat) .collect(); Ok(Attribute::Tagged(tag, args)) } else { Ok(Attribute::Attr(input.parse()?)) } } } fn parse_vector_trailing_till_double_comma(input: ParseStream) -> syn::Result> where T: Parse, P: syn::token::Token + Parse, { Ok( Punctuated::, P>::parse_separated_nonempty_with(input, |input_tokens| { if input_tokens.is_empty() || input_tokens.peek(Token![::]) { Ok(None) } else { T::parse(input_tokens).map(Some) } })? .into_iter() .flatten() .collect(), ) } #[allow(dead_code)] pub(crate) fn drain_stream(input: ParseStream) { // JUST TO SKIP ALL let _ = input.step(|cursor| { let mut rest = *cursor; while let Some((_, next)) = rest.token_tree() { rest = next } Ok(((), rest)) }); } #[derive(PartialEq, Debug, Clone, Default)] pub(crate) struct Positional(pub(crate) Vec); impl Parse for Positional { fn parse(input: ParseStream) -> syn::Result { Ok(Self( Punctuated::::parse_terminated(input)? .into_iter() .collect(), )) } } #[derive(PartialEq, Debug, Clone)] pub(crate) struct Fixture { pub(crate) arg: Pat, pub(crate) resolve: syn::Path, pub(crate) positional: Positional, } impl Fixture { pub(crate) fn new(arg: Pat, resolve: syn::Path, positional: Positional) -> Self { Self { arg, resolve, positional, } } } impl Parse for Fixture { fn parse(input: ParseStream) -> syn::Result { let resolve: syn::Path = input.parse()?; if input.peek(Paren) || input.peek(Token![as]) { let positional = if input.peek(Paren) { let content; let _ = syn::parenthesized!(content in input); content.parse()? } else { Default::default() }; if input.peek(Token![as]) { let _: Token![as] = input.parse()?; let ident: Ident = input.parse()?; Ok(Self::new(ident.into_pat(), resolve, positional)) } else { let name = resolve.get_ident().ok_or_else(|| { syn::Error::new_spanned( resolve.to_token_stream(), "Should be an ident".to_string(), ) })?; Ok(Self::new( name.clone().into_pat(), name.clone().into(), positional, )) } } else { Err(syn::Error::new( input.span(), "fixture need arguments or 'as new_name' format", )) } } } impl ToTokens for Fixture { fn to_tokens(&self, tokens: &mut TokenStream) { self.arg.to_tokens(tokens) } } pub(crate) fn extract_fixtures(item_fn: &mut ItemFn) -> Result, ErrorsVec> { let mut fixtures_extractor = FixturesFunctionExtractor::default(); fixtures_extractor.visit_item_fn_mut(item_fn); if fixtures_extractor.1.is_empty() { Ok(fixtures_extractor.0) } else { Err(fixtures_extractor.1.into()) } } pub(crate) fn extract_defaults(item_fn: &mut ItemFn) -> Result, ErrorsVec> { struct DefaultBuilder; impl AttrBuilder for DefaultBuilder { type Out = ArgumentValue; fn build(attr: syn::Attribute, name: &Pat) -> syn::Result { attr.parse_args::() .map(|e| ArgumentValue::new(name.clone(), e)) } } impl Validator for DefaultBuilder {} let mut extractor = JustOnceFnArgAttributeExtractor::::new("default"); extractor.visit_item_fn_mut(item_fn); extractor.take() } pub(crate) fn extract_default_return_type( item_fn: &mut ItemFn, ) -> Result, ErrorsVec> { struct DefaultTypeBuilder; impl AttrBuilder for DefaultTypeBuilder { type Out = syn::Type; fn build(attr: syn::Attribute, _extra: &ItemFn) -> syn::Result { attr.parse_args::() } } impl Validator for DefaultTypeBuilder {} let mut extractor = JustOnceFnAttributeExtractor::::new(FixtureModifiers::DEFAULT_RET_ATTR); extractor.visit_item_fn_mut(item_fn); extractor.take() } pub(crate) fn extract_partials_return_type( item_fn: &mut ItemFn, ) -> Result, ErrorsVec> { let mut partials_type_extractor = PartialsTypeFunctionExtractor::default(); partials_type_extractor.visit_item_fn_mut(item_fn); partials_type_extractor.take() } pub(crate) fn extract_once(item_fn: &mut ItemFn) -> Result, ErrorsVec> { let mut extractor = JustOnceFnAttributeExtractor::from("once"); extractor.visit_item_fn_mut(item_fn); extractor.take() } pub(crate) fn extract_argument_attrs<'a, B: 'a + std::fmt::Debug>( node: &mut FnArg, is_valid_attr: fn(&syn::Attribute) -> bool, build: impl Fn(syn::Attribute) -> syn::Result + 'a, ) -> Box> + 'a> { let name = node.maybe_ident().cloned(); if name.is_none() { return Box::new(std::iter::empty()); } if let FnArg::Typed(ref mut arg) = node { // Extract interesting attributes let attrs = std::mem::take(&mut arg.attrs); let (extracted, remain): (Vec<_>, Vec<_>) = attrs.into_iter().partition(is_valid_attr); arg.attrs = remain; // Parse attrs Box::new(extracted.into_iter().map(build)) } else { Box::new(std::iter::empty()) } } /// Simple struct used to visit function attributes and extract default return /// type struct PartialsTypeFunctionExtractor(Result, ErrorsVec>); impl PartialsTypeFunctionExtractor { fn take(self) -> Result, ErrorsVec> { self.0 } } impl Default for PartialsTypeFunctionExtractor { fn default() -> Self { Self(Ok(Vec::default())) } } impl VisitMut for PartialsTypeFunctionExtractor { fn visit_item_fn_mut(&mut self, node: &mut ItemFn) { let attrs = std::mem::take(&mut node.attrs); let (partials, remain): (Vec<_>, Vec<_>) = attrs .into_iter() .partition(|attr| match attr.path().get_ident() { Some(name) => name .to_string() .starts_with(FixtureModifiers::PARTIAL_RET_ATTR), None => false, }); node.attrs = remain; let mut errors = ErrorsVec::default(); let mut data: Vec<(usize, syn::Type)> = Vec::default(); for attr in partials { match attr.parse_args::() { Ok(t) => { match attr.path().get_ident().unwrap().to_string() [FixtureModifiers::PARTIAL_RET_ATTR.len()..] .parse() { Ok(id) => data.push((id, t)), Err(_) => errors.push(syn::Error::new_spanned( attr, "Invalid partial syntax: should be partial_", )), } } Err(e) => errors.push(e), } } self.0 = if !errors.is_empty() { Err(errors) } else { Ok(data) }; } } pub(crate) fn extract_case_args(item_fn: &mut ItemFn) -> Result, ErrorsVec> { let mut extractor = JustOnceFnArgAttributeExtractor::from("case"); extractor.visit_item_fn_mut(item_fn); extractor.take() } /// Simple struct used to visit function attributes and extract cases and /// eventualy parsing errors #[derive(Default)] struct CasesFunctionExtractor(Vec, Vec); impl VisitMut for CasesFunctionExtractor { fn visit_item_fn_mut(&mut self, node: &mut ItemFn) { let attrs = std::mem::take(&mut node.attrs); let mut attrs_buffer = Default::default(); let case: syn::PathSegment = parse_quote! { case }; for attr in attrs.into_iter() { if attr_starts_with(&attr, &case) { match attr.parse_args::() { Ok(expressions) => { let description = attr.path().segments.iter().nth(1).map(|p| p.ident.clone()); self.0.push(TestCase { args: expressions.into(), attrs: std::mem::take(&mut attrs_buffer), description, }); } Err(err) => self.1.push(err), }; } else { attrs_buffer.push(attr) } } node.attrs = std::mem::take(&mut attrs_buffer); } } pub(crate) fn extract_cases(item_fn: &mut ItemFn) -> Result, ErrorsVec> { let mut cases_extractor = CasesFunctionExtractor::default(); cases_extractor.visit_item_fn_mut(item_fn); if cases_extractor.1.is_empty() { Ok(cases_extractor.0) } else { Err(cases_extractor.1.into()) } } pub(crate) fn extract_value_list(item_fn: &mut ItemFn) -> Result, ErrorsVec> { struct ValueListBuilder; impl AttrBuilder for ValueListBuilder { type Out = ValueList; fn build(attr: syn::Attribute, extra: &Pat) -> syn::Result { attr.parse_args::().map(|v| ValueList { arg: extra.clone(), values: v.take().into_iter().map(|e| e.into()).collect(), }) } } impl Validator for ValueListBuilder {} let mut extractor = JustOnceFnArgAttributeExtractor::::new("values"); extractor.visit_item_fn_mut(item_fn); extractor.take() } /// Simple struct used to visit function args attributes to extract the /// excluded ones and eventualy parsing errors struct ExcludedTraceAttributesFunctionExtractor(Result, ErrorsVec>); impl From, ErrorsVec>> for ExcludedTraceAttributesFunctionExtractor { fn from(inner: Result, ErrorsVec>) -> Self { Self(inner) } } impl ExcludedTraceAttributesFunctionExtractor { pub(crate) fn take(self) -> Result, ErrorsVec> { self.0 } fn update_error(&mut self, mut errors: ErrorsVec) { match &mut self.0 { Ok(_) => self.0 = Err(errors), Err(err) => err.append(&mut errors), } } fn update_excluded(&mut self, value: Pat) { if let Some(inner) = self.0.iter_mut().next() { inner.push(value); } } } impl Default for ExcludedTraceAttributesFunctionExtractor { fn default() -> Self { Self(Ok(Default::default())) } } impl VisitMut for ExcludedTraceAttributesFunctionExtractor { fn visit_fn_arg_mut(&mut self, node: &mut FnArg) { let pat = match node.maybe_pat().cloned() { Some(pat) => pat, None => return, }; for r in extract_argument_attrs(node, |a| attr_is(a, "notrace"), |_a| Ok(())) { match r { Ok(_) => self.update_excluded(pat.clone()), Err(err) => self.update_error(err.into()), } } syn::visit_mut::visit_fn_arg_mut(self, node); } } pub(crate) fn extract_excluded_trace(item_fn: &mut ItemFn) -> Result, ErrorsVec> { let mut excluded_trace_extractor = ExcludedTraceAttributesFunctionExtractor::default(); excluded_trace_extractor.visit_item_fn_mut(item_fn); excluded_trace_extractor.take() } /// Simple struct used to visit function args attributes to check timeout syntax struct CheckTimeoutAttributesFunction(Result<(), ErrorsVec>); impl From for CheckTimeoutAttributesFunction { fn from(errors: ErrorsVec) -> Self { Self(Err(errors)) } } impl CheckTimeoutAttributesFunction { pub(crate) fn take(self) -> Result<(), ErrorsVec> { self.0 } fn check_if_can_implement_timeous( &self, timeouts: &[&syn::Attribute], asyncness: Option<&Async>, ) -> Option { if cfg!(feature = "async-timeout") || timeouts.is_empty() { None } else { asyncness.map(|a| { syn::Error::new( a.span, "Enable async-timeout feature to use timeout in async tests", ) }) } } } impl Default for CheckTimeoutAttributesFunction { fn default() -> Self { Self(Ok(())) } } impl VisitMut for CheckTimeoutAttributesFunction { fn visit_item_fn_mut(&mut self, node: &mut ItemFn) { let timeouts = node .attrs .iter() .filter(|&a| attr_is(a, "timeout")) .collect::>(); let mut errors = timeouts .iter() .map(|&attr| attr.parse_args::()) .filter_map(Result::err) .collect::>(); if let Some(e) = self.check_if_can_implement_timeous(timeouts.as_slice(), node.sig.asyncness.as_ref()) { errors.push(e); } if !errors.is_empty() { *self = Self(Err(errors.into())); } } } pub(crate) fn check_timeout_attrs(item_fn: &mut ItemFn) -> Result<(), ErrorsVec> { let mut checker = CheckTimeoutAttributesFunction::default(); checker.visit_item_fn_mut(item_fn); checker.take() } #[cfg(test)] mod should { use super::*; use crate::test::*; mod parse_attributes { use super::assert_eq; use super::*; fn parse_attributes>(attributes: S) -> Attributes { parse_meta(attributes) } #[test] fn one_simple_ident() { let attributes = parse_attributes("my_ident"); let expected = Attributes { attributes: vec![Attribute::attr("my_ident")], }; assert_eq!(expected, attributes); } #[test] fn one_simple_group() { let attributes = parse_attributes("group_tag(first, second)"); let expected = Attributes { attributes: vec![Attribute::tagged("group_tag", vec!["first", "second"])], }; assert_eq!(expected, attributes); } #[test] fn one_simple_type() { let attributes = parse_attributes("type_tag<(u32, T, (String, i32))>"); let expected = Attributes { attributes: vec![Attribute::typed("type_tag", "(u32, T, (String, i32))")], }; assert_eq!(expected, attributes); } #[test] fn integrated() { let attributes = parse_attributes( r#" simple :: tagged(first, second) :: type_tag<(u32, T, (std::string::String, i32))> :: more_tagged(a,b)"#, ); let expected = Attributes { attributes: vec![ Attribute::attr("simple"), Attribute::tagged("tagged", vec!["first", "second"]), Attribute::typed("type_tag", "(u32, T, (std::string::String, i32))"), Attribute::tagged("more_tagged", vec!["a", "b"]), ], }; assert_eq!(expected, attributes); } } } rstest_macros-0.26.1/src/parse/rstest/files.rs000064400000000000000000001206361046102023000175070ustar 00000000000000use std::{env, path::PathBuf}; use crate::{ error::ErrorsVec, parse::{ extract_argument_attrs, vlist::{Value, ValueList}, }, refident::{IntoPat, MaybeIdent}, utils::attr_is, }; use glob::glob; use quote::ToTokens; use regex::Regex; use relative_path::{PathExt, RelativePath}; use syn::{ parse::Parse, parse_quote, visit_mut::VisitMut, Attribute, Expr, FnArg, Ident, ItemFn, LitStr, MetaNameValue, }; #[derive(Debug, Clone, PartialEq)] pub(crate) struct FilesGlobReferences { base_dir: Option, glob: Vec, exclude: Vec, ignore_directories: bool, ignore_dot_files: bool, ignore_missing_env_vars: bool, files_mode: FilesMode, } impl FilesGlobReferences { /// Replace environment variables in a string. fn replace_env_vars( &self, attr: &LitStrAttr, env: impl EnvironmentResolver, ) -> Result { let re = Regex::new(r"\$([a-zA-Z_][a-zA-Z0-9_]+)|\$\{([^}]+)}") .expect("Could not build the regex"); let ignore_missing = self.ignore_missing_env_vars; let path = attr.value(); let haystack = path.as_str(); let mut result = String::with_capacity(attr.value().len()); let mut last_match = 0; for caps in re.captures_iter(haystack) { let match_all = caps.get(0).expect("The regex should have matched"); // Match the name of the variable and its default value (if any). let (var_name, default) = if let Some(m) = caps.get(1) { // In the first case ($VAR), the default value is None. (m.as_str(), None) } else { // In the second case we have to split the variable name and the default value // if there's a `:-` separator. let m = caps.get(2).expect("The regex should have matched"); let (var_name, default) = if let Some((var_name, default)) = m.as_str().split_once(":-") { (var_name, Some(default)) } else { (m.as_str(), None) }; var_name .chars() .all(|c| c.is_alphanumeric() || c == '_') .then_some((var_name, default)) .ok_or_else(|| { attr.error(&format!( "The variable \"{}\" name does not match [a-zA-Z0-9_]+", m.as_str() )) })? }; let v = env.get(var_name); let replacement = match (v.as_deref(), default) { (Some(""), Some(default)) => default.to_string(), (Some(val), _) => val.to_string(), (None, Some(default)) => default.to_string(), (None, _) if ignore_missing => String::new(), (None, _) => { return Err(attr.error(&format!( "Could not find the environment variable {var_name:?}" ))) } }; result.push_str(&haystack[last_match..match_all.start()]); result.push_str(&replacement); last_match = match_all.end(); } result.push_str(&haystack[last_match..]); Ok(result) } /// Return the base_dir attribute if set, with variables replaced. fn base_dir(&self) -> Result, syn::Error> { self.base_dir .as_ref() .map(|attr| { self.replace_env_vars(attr, StdEnvironmentResolver) .map(PathBuf::from) }) .transpose() } /// Return the tuples attribute, path string if they are valid relative paths fn paths(&self, base_dir: &PathBuf) -> Result, syn::Error> { self.glob .iter() .map(|attr| { let path = self.replace_env_vars(attr, StdEnvironmentResolver)?; RelativePath::from_path(&path) .map_err(|e| attr.error(&format!("Invalid glob path: {e}"))) .map(|p| p.to_logical_path(base_dir)) .map(|p| (attr, p.to_string_lossy().into_owned())) }) .collect::, _>>() } } #[derive(Default, Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum FilesMode { #[default] Path, IncludeStr, IncludeBytes, } pub(crate) mod files_mode_keywords { syn::custom_keyword!(path); syn::custom_keyword!(str); syn::custom_keyword!(bytes); } impl Parse for FilesMode { fn parse(input: syn::parse::ParseStream) -> syn::Result { if Option::::parse(input)?.is_some() { Ok(Self::Path) } else if Option::::parse(input)?.is_some() { Ok(Self::IncludeStr) } else if Option::::parse(input)?.is_some() { Ok(Self::IncludeBytes) } else { Err(input.error("Expected one of the following keywords: path, str or bytes")) } } } trait RaiseError: ToTokens { fn error(&self, msg: &str) -> syn::Error { syn::Error::new_spanned(self, msg) } } impl RaiseError for Attribute {} impl RaiseError for LitStrAttr {} impl ToTokens for LitStrAttr { fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { self.attr.to_tokens(tokens) } } impl FilesGlobReferences { fn new( glob: Vec, exclude: Vec, ignore_directories: bool, ignore_dot_files: bool, ignore_missing_env_vars: bool, files_mode: FilesMode, ) -> Self { Self { glob, base_dir: None, exclude, ignore_directories, ignore_dot_files, ignore_missing_env_vars, files_mode, } } fn with_base_dir_opt(mut self, base_dir: Option) -> Self { self.base_dir = base_dir; self } fn is_valid(&self, p: &RelativePath) -> bool { if self.ignore_dot_files && p.components() .any(|c| matches!(c, relative_path::Component::Normal(c) if c.starts_with('.'))) { return false; } !self.exclude.iter().any(|e| e.r.is_match(p.as_ref())) } } /// An attribute in the form `#[name("some string")]` #[derive(Debug, Clone, PartialEq)] struct LitStrAttr { attr: Attribute, value: LitStr, } impl LitStrAttr { fn parse_meta_name_value(attr: &Attribute) -> Result { let meta = attr.meta.require_name_value()?; if let syn::Expr::Lit(expr) = &meta.value { if let syn::Lit::Str(value) = expr.lit.clone() { return Ok(Self { attr: attr.clone(), value, }); } } Err(syn::Error::new_spanned( attr, "expected a string literal value", )) } fn value(&self) -> String { self.value.value() } } impl TryFrom for LitStrAttr { type Error = syn::Error; fn try_from(attr: Attribute) -> Result { let value = attr.parse_args::()?; Ok(Self { attr, value }) } } /// The `#[exclude("regex")]` attribute #[derive(Debug, Clone)] struct Exclude { attr: LitStrAttr, r: Regex, } impl PartialEq for Exclude { fn eq(&self, other: &Self) -> bool { self.attr.value == other.attr.value } } impl TryFrom for Exclude { type Error = syn::Error; fn try_from(attr: Attribute) -> Result { let attr: LitStrAttr = attr.try_into()?; let r = Regex::new(&attr.value()).map_err(|e| { syn::Error::new_spanned( &attr, format!(r#""{}" Should be a valid regex: {e}"#, attr.value()), ) })?; Ok(Self { attr, r }) } } impl From> for FilesGlobReferences { fn from(value: Vec) -> Self { Self::new(value, Default::default(), true, true, false, Default::default()) } } /// Entry point function to extract files attributes pub(crate) fn extract_files( item_fn: &mut ItemFn, ) -> Result, ErrorsVec> { let mut extractor = ValueFilesExtractor::default(); extractor.visit_item_fn_mut(item_fn); extractor.take() } /// Simple struct used to visit function attributes and extract future args to /// implement the boilerplate. #[derive(Default)] struct ValueFilesExtractor { files: Vec<(Ident, FilesGlobReferences)>, errors: Vec, } impl ValueFilesExtractor { pub(crate) fn take(self) -> Result, ErrorsVec> { if self.errors.is_empty() { Ok(self.files) } else { Err(self.errors.into()) } } fn collect_errors(&mut self, result: Result) -> T { match result { Ok(v) => v, Err(e) => { self.errors.push(e); T::default() } } } fn extract_argument_attrs<'a, B: 'a + std::fmt::Debug>( &mut self, node: &mut FnArg, is_valid_attr: fn(&syn::Attribute) -> bool, build: impl Fn(syn::Attribute) -> syn::Result + 'a, ) -> Vec { self.collect_errors( extract_argument_attrs(node, is_valid_attr, build).collect::, _>>(), ) } fn extract_files(&mut self, node: &mut FnArg) -> Vec { self.extract_argument_attrs(node, |a| attr_is(a, "files"), |attr| attr.try_into()) } fn extract_exclude(&mut self, node: &mut FnArg) -> Vec { self.extract_argument_attrs(node, |a| attr_is(a, "exclude"), Exclude::try_from) } fn extract_include_dot_files(&mut self, node: &mut FnArg) -> Vec { self.extract_argument_attrs( node, |a| attr_is(a, "include_dot_files"), |attr| { attr.meta .require_path_only() .map_err(|_| attr.error("Use #[include_dot_files] to include dot files"))?; Ok(attr) }, ) } fn extract_ignore_missing_env_vars(&mut self, node: &mut FnArg) -> Vec { self.extract_argument_attrs( node, |a| attr_is(a, "ignore_missing_env_vars"), |attr| { attr.meta.require_path_only().map_err(|_| { attr.error( "Use #[ignore_missing_env_vars] to ignore missing environment variables", ) })?; Ok(attr) }, ) } fn extract_dirs(&mut self, node: &mut FnArg) -> Vec { self.extract_argument_attrs( node, |a| attr_is(a, "dirs"), |attr| { attr.meta.require_path_only().map_err(|_| { attr.error( "Use #[dirs] to include matched directories", ) })?; Ok(attr) }, ) } fn extract_base_dir(&mut self, node: &mut FnArg) -> Vec { self.extract_argument_attrs( node, |a| attr_is(a, "base_dir"), |attr| { LitStrAttr::parse_meta_name_value(&attr).map_err(|_| { attr.error( "Use #[base_dir = \"...\"] to define the base directory for the glob path", ) }) }, ) } fn extract_mode(&mut self, node: &mut FnArg) -> Vec<(MetaNameValue, FilesMode)> { self.extract_argument_attrs( node, |a| attr_is(a, "mode"), |attr| { attr.meta .require_name_value() .map_err(|_| { attr.error("Use #[mode = ...] to define the argument of the file input") }) .and_then(|attr| { syn::parse2(attr.value.to_token_stream()) .map(|file_mode| (attr.clone(), file_mode)) }) }, ) } } impl VisitMut for ValueFilesExtractor { fn visit_fn_arg_mut(&mut self, node: &mut FnArg) { let name = node.maybe_ident().cloned(); if matches!(node, FnArg::Receiver(_)) || name.is_none() { return; } let name = name.unwrap(); let files = self.extract_files(node); let excludes = self.extract_exclude(node); let include_dot_files = self.extract_include_dot_files(node); let ignore_missing_env_vars = self.extract_ignore_missing_env_vars(node); let ignore_directories = self.extract_dirs(node); if !include_dot_files.is_empty() { include_dot_files.iter().skip(1).for_each(|attr| { self.errors .push(attr.error("Cannot use #[include_dot_files] more than once")) }) } if !ignore_missing_env_vars.is_empty() { ignore_missing_env_vars.iter().skip(1).for_each(|attr| { self.errors .push(attr.error("Cannot use #[ignore_missing_env_vars] more than once")) }) } if !ignore_directories.is_empty() { ignore_directories.iter().skip(1).for_each(|attr| { self.errors .push(attr.error("Cannot use #[dirs] more than once")) }) } let base_dir = self.extract_base_dir(node); if base_dir.len() > 1 { base_dir.iter().skip(1).for_each(|attr| { self.errors .push(attr.error(r#"Cannot use #[base_dir = "..."] more than once"#)) }) } let mode_attr = self.extract_mode(node); let mode = if let Some(value) = mode_attr.first() { mode_attr.iter().skip(1).for_each(|attr| { self.errors.push(syn::Error::new_spanned( &attr.0, r#"Cannot use #[mode = ...] more than once"#, )) }); value.1 } else { Default::default() }; if !files.is_empty() { self.files.push(( name, FilesGlobReferences::new( files, excludes, ignore_directories.is_empty(), include_dot_files.is_empty(), !ignore_missing_env_vars.is_empty(), mode, ) .with_base_dir_opt(base_dir.into_iter().next()), )) } else { excludes.into_iter().for_each(|e| { self.errors.push( e.attr .error("You cannot use #[exclude(...)] without #[files(...)]"), ) }); include_dot_files.into_iter().for_each(|attr| { self.errors .push(attr.error("You cannot use #[include_dot_files] without #[files(...)]")) }); ignore_missing_env_vars.into_iter().for_each(|attr| { self.errors.push( attr.error("You cannot use #[ignore_missing_env_vars] without #[files(...)]"), ) }); ignore_directories.into_iter().for_each(|attr| { self.errors .push(attr.error("You cannot use #[dirs] without #[files(...)]")) }); base_dir.into_iter().for_each(|attr| { self.errors .push(attr.error(r#"You cannot use #[base_dir = "..."] without #[files(...)]"#)) }); } } } trait BaseDir { fn base_dir(&self) -> Result { env::var("CARGO_MANIFEST_DIR") .map(PathBuf::from) .map_err(|_| "Rstest's #[files(...)] requires that CARGO_MANIFEST_DIR is defined to define glob the relative path".to_string() ) } } struct DefaultBaseDir; impl BaseDir for DefaultBaseDir {} trait EnvironmentResolver { fn get(&self, var: &str) -> Option; } struct StdEnvironmentResolver; impl EnvironmentResolver for StdEnvironmentResolver { fn get(&self, var: &str) -> Option { env::var(var).ok() } } trait GlobResolver { fn glob(&self, pattern: &str) -> Result, String> { let globs = glob(pattern).map_err(|e| format!("glob failed for whole path `{pattern}` due {e}"))?; globs .into_iter() .map(|p| p.map_err(|e| format!("glob failed for file due {e}"))) .map(|r| { r.and_then(|p| { p.canonicalize() .map_err(|e| format!("failed to canonicalize {} due {e}", p.display())) }) }) .collect() } } struct DefaultGlobResolver; impl GlobResolver for DefaultGlobResolver {} /// The struct used to gel te values from the files attributes. You can inject /// the base dir resolver and glob resolver implementation. pub(crate) struct ValueListFromFiles<'a> { base_dir: Box, g_resolver: Box, } impl Default for ValueListFromFiles<'_> { fn default() -> Self { Self { g_resolver: Box::new(DefaultGlobResolver), base_dir: Box::new(DefaultBaseDir), } } } impl ValueListFromFiles<'_> { pub fn to_value_list( &self, files: Vec<(Ident, FilesGlobReferences)>, ) -> Result, syn::Error> { files .into_iter() .map(|(arg, refs)| { self.file_list_values(refs).map(|values| ValueList { arg: arg.into_pat(), values, }) }) .collect::, _>>() } fn file_list_values(&self, refs: FilesGlobReferences) -> Result, syn::Error> { let default_base_dir = self .base_dir .base_dir() .map_err(|msg| refs.glob[0].error(&msg))?; let base_dir = match refs.base_dir()? { Some(p) if p.is_relative() => default_base_dir.join(p), Some(p) => p, None => default_base_dir, }; let base_dir = match base_dir.canonicalize() { Ok(base_dir) => base_dir, Err(e) if e.kind() == std::io::ErrorKind::NotFound => base_dir, Err(e) => { let msg = format!("Cannot canonicalize base dir {base_dir:?}: {e}"); return Err(refs.glob[0].error(&msg)); } }; let resolved_paths = refs.paths(&base_dir)?; let mut values: Vec<(Expr, String)> = vec![]; for (attr, abs_path) in self.all_files_path(resolved_paths)? { let relative_path = abs_path.relative_to(&base_dir).map_err(|e| { attr.error(&format!( "Cannot make {} relative to {}: {e}", abs_path.display(), base_dir.display() )) })?; if (refs.ignore_directories && abs_path.is_dir()) || !refs.is_valid(&relative_path) { continue; } let path_str = abs_path.to_string_lossy(); let value = match refs.files_mode { FilesMode::Path => parse_quote! { <::std::path::PathBuf as std::str::FromStr>::from_str(#path_str).unwrap() }, FilesMode::IncludeStr => parse_quote! { include_str!(#path_str) }, FilesMode::IncludeBytes => parse_quote! { include_bytes!(#path_str) }, }; values.push((value, render_file_description(&relative_path))); } if values.is_empty() { Err(refs.glob[0].error("No file found"))?; } Ok(values .into_iter() .map(|(e, desc)| Value::new(e, Some(desc))) .collect()) } /// Return the tuples of attribute, file path resolved via glob resolver, sorted by path and without duplications. fn all_files_path<'b>( &self, resolved_paths: Vec<(&'b LitStrAttr, String)>, ) -> Result, syn::Error> { let mut paths = resolved_paths .iter() .map(|(attr, pattern)| { self.g_resolver .glob(pattern.as_ref()) .map_err(|msg| attr.error(&msg)) .map(|p| (attr, p)) }) .collect::, _>>()? .into_iter() .flat_map(|(&attr, inner)| inner.into_iter().map(move |p| (attr, p))) .collect::>(); paths.sort_by(|(_, a), (_, b)| a.cmp(b)); paths.dedup_by(|(_, a), (_, b)| a.eq(&b)); Ok(paths) } } fn render_file_description(file: &RelativePath) -> String { let mut description = String::new(); for c in file.components() { match c { relative_path::Component::CurDir => continue, relative_path::Component::ParentDir => description.push_str("_UP"), relative_path::Component::Normal(segment) => description.push_str(segment), } description.push('/') } description.pop(); description } #[cfg(test)] mod should { use std::collections::HashMap; use super::*; use crate::test::{assert_eq, *}; use maplit::hashmap; use rstest_test::assert_in; fn lit_str_attr(name: &str, value: impl AsRef) -> LitStrAttr { attrs(&format!(r#"#[{name}("{}")]"#, value.as_ref())) .into_iter() .next() .unwrap() .try_into() .unwrap() } fn name_value_attr(name: &str, value: impl AsRef) -> LitStrAttr { LitStrAttr::parse_meta_name_value( &attrs(&format!(r#"#[{name} = "{}"]"#, value.as_ref())) .into_iter() .next() .unwrap(), ) .expect("Could not parse attribute") } fn files_attr(lstr: impl AsRef) -> LitStrAttr { lit_str_attr("files", lstr) } fn base_dir_attr(lstr: impl AsRef) -> LitStrAttr { name_value_attr("base_dir", lstr) } fn exclude_attr(lstr: impl AsRef) -> LitStrAttr { lit_str_attr("exclude", lstr) } impl Exclude { fn fake(value: &str, r: Option) -> Self { let r = r.unwrap_or_else(|| regex::Regex::new(value).unwrap()); Self { attr: exclude_attr(value), r, } } } impl From<&str> for Exclude { fn from(value: &str) -> Self { Self { attr: exclude_attr(value), r: Regex::new(value).unwrap(), } } } #[rstest] #[case::simple(r#"fn f(#[files("some_glob")] a: PathBuf) {}"#, "fn f(a: PathBuf) {}", &[("a", None, &["some_glob"], &[], true, true, false)])] #[case::more_than_one( r#"fn f(#[files("first")] a: PathBuf, b: u32, #[files("third")] c: PathBuf) {}"#, r#"fn f(a: PathBuf, b: u32, c: PathBuf) {}"#, &[("a", None, &["first"], &[], true, true, false), ("c", None, &["third"], &[], true, true, false)], )] #[case::more_globs_on_the_same_var( r#"fn f(#[files("first")] #[files("second")] a: PathBuf) {}"#, r#"fn f(a: PathBuf) {}"#, &[("a", None, &["first", "second"], &[], true, true, false)], )] #[case::exclude(r#"fn f(#[files("some_glob")] #[exclude("exclude")] a: PathBuf) {}"#, "fn f(a: PathBuf) {}", &[("a", None, &["some_glob"], &["exclude"], true, true, false)])] #[case::exclude_more(r#"fn f(#[files("some_glob")] #[exclude("first")] #[exclude("second")] a: PathBuf) {}"#, "fn f(a: PathBuf) {}", &[("a", None, &["some_glob"], &["first", "second"], true, true, false)])] #[case::include_dot_files(r#"fn f(#[files("some_glob")] #[include_dot_files] a: PathBuf) {}"#, "fn f(a: PathBuf) {}", &[("a", None, &["some_glob"], &[], true, false, false)])] #[case::base_dir(r#"fn f(#[files("some_glob")] #[base_dir = "/base/"] a: PathBuf) {}"#, "fn f(a: PathBuf) {}", &[("a", Some("/base/"), &["some_glob"], &[], true, true, false)])] #[case::ignore_env_vars(r#"fn f(#[files("some_glob")] #[ignore_missing_env_vars] a: PathBuf) {}"#, "fn f(a: PathBuf) {}", &[("a", None, &["some_glob"], &[], true, true, true)])] #[case::ignore_env_vars_unknown(r#"fn f(#[files("some${SOME_VAR}_glob")] #[ignore_missing_env_vars] a: PathBuf) {}"#, "fn f(a: PathBuf) {}", &[("a", None, &["some${SOME_VAR}_glob"], &[], true, true, true)])] #[case::ignore_directories(r#"fn f(#[files("some_glob")] #[dirs] a: PathBuf) {}"#, "fn f(a: PathBuf) {}", &[("a", None, &["some_glob"], &[], false, true, false)])] #[case::env_vars_default(r#"fn f(#[files("some${__UNKNOWN__:-default/value}_glob")] a: PathBuf) {}"#, "fn f(a: PathBuf) {}", &[("a", None, &["some${__UNKNOWN__:-default/value}_glob"], &[], true, true, false)])] fn extract<'a, G: AsRef<[&'a str]>, E: AsRef<[&'a str]>>( #[case] item_fn: &str, #[case] expected: &str, #[case] expected_files: &[(&str, Option<&str>, G, E, bool, bool, bool)], ) { let mut item_fn: ItemFn = item_fn.ast(); let expected: ItemFn = expected.ast(); let files = extract_files(&mut item_fn).unwrap(); assert_eq!(expected, item_fn); assert_eq!( files, expected_files .into_iter() .map(|(id, base_dir, globs, ex, ignore_dirs, ignore, ignore_envvars)| { ( ident(id), FilesGlobReferences::new( globs.as_ref().iter().map(files_attr).collect(), ex.as_ref().iter().map(|&ex| ex.into()).collect(), *ignore_dirs, *ignore, *ignore_envvars, Default::default(), ) .with_base_dir_opt(base_dir.map(base_dir_attr)), ) }) .collect::>() ); } #[rstest] #[case::no_files_arg("fn f(#[files] a: PathBuf) {}", "#[files(...)]")] #[case::invalid_files_inner("fn f(#[files(a::b::c)] a: PathBuf) {}", "string literal")] #[case::no_exclude_args( r#"fn f(#[files("some")] #[exclude] a: PathBuf) {}"#, "#[exclude(...)]" )] #[case::invalid_exclude_inner( r#"fn f(#[files("some")] #[exclude(a::b)] a: PathBuf) {}"#, "string literal" )] #[case::invalid_exclude_regex( r#"fn f(#[files("some")] #[exclude("invalid(reg(ex")] a: PathBuf) {}"#, "valid regex" )] #[case::include_dot_files_with_args( r#"fn f(#[files("some")] #[include_dot_files(some)] a: PathBuf) {}"#, "#[include_dot_files]" )] #[case::exclude_without_files( r#"fn f(#[exclude("some")] a: PathBuf) {}"#, "#[exclude(...)] without #[files(...)]" )] #[case::include_dot_files_without_files( r#"fn f(#[include_dot_files] a: PathBuf) {}"#, "#[include_dot_files] without #[files(...)]" )] #[case::include_dot_files_more_than_once( r#"fn f(#[files("some")] #[include_dot_files] #[include_dot_files] a: PathBuf) {}"#, "more than once" )] #[case::dirs_without_files( r#"fn f(#[dirs] a: PathBuf) {}"#, "#[dirs] without #[files(...)]" )] #[case::dirs_more_than_once( r#"fn f(#[files("some")] #[dirs] #[dirs] a: PathBuf) {}"#, "more than once" )] #[case::ignore_missing_env_vars_without_files( r#"fn f(#[ignore_missing_env_vars] a: PathBuf) {}"#, "#[ignore_missing_env_vars] without #[files(...)]" )] #[case::ignore_missing_env_vars_more_than_once( r#"fn f(#[files("some")] #[ignore_missing_env_vars] #[ignore_missing_env_vars] a: PathBuf) {}"#, "more than once" )] #[case::override_base_dir_more_than_once( r#"fn f(#[files("some")] #[base_dir = "/"] #[base_dir = "/"] a: PathBuf) {}"#, "more than once" )] #[case::invalid_base_dir( r#"fn f(#[files("some")] #[base_dir = 123] a: PathBuf) {}"#, "base directory for the glob path" )] #[case::multiple_file_modes( r#"fn f(#[files("some")] #[mode = str] #[mode = str] a: PathBuf) {}"#, r#"Cannot use #[mode = ...] more than once"# )] #[case::unknown_file_mode( r#"fn f(#[files("some")] #[mode = blabla] a: PathBuf) {}"#, r#"Expected one of the following keywords: path, str or bytes"# )] #[case::wrong_attr_file_mode( r#"fn f(#[files("some")] #[mode(blabla)] a: PathBuf) {}"#, "Use #[mode = ...] to define the argument of the file input" )] fn raise_error(#[case] item_fn: &str, #[case] message: &str) { let mut item_fn: ItemFn = item_fn.ast(); let err = extract_files(&mut item_fn).unwrap_err(); assert_in!(format!("{:?}", err), message); } #[rstest] #[case::unknown_env_var( r#"fn f(#[files("so${__UNKNOWN__}me")] a: PathBuf) {}"#, "Could not find the environment variable \\\"__UNKNOWN__\\\"" )] #[case::invalid_env_var( r#"fn f(#[files("so${__INVALID%%__}me")] a: PathBuf) {}"#, "The variable \\\"__INVALID%%__\\\" name does not match [a-zA-Z0-9_]+" )] #[case::base_dir_unknown_env_var( r#"fn f(#[base_dir = "${__UNKNOWN__}"] #[files("some")] a: PathBuf) {}"#, "Could not find the environment variable \\\"__UNKNOWN__\\\"" )] fn raise_error_on_paths(#[case] item_fn: &str, #[case] message: &str) { let mut item_fn: ItemFn = item_fn.ast(); let ok = extract_files(&mut item_fn).unwrap(); let err: syn::Error = ok .into_iter() .map(|(_id, refs)| { let base_dir = refs.base_dir()?; let x = refs.paths(base_dir.as_ref().unwrap_or(&PathBuf::from("/default")))?; eprintln!("x {:?}", x); Ok(()) }) .collect::>() .unwrap_err(); assert_in!(format!("{:?}", err), message); } #[derive(Default)] struct FakeBaseDir(PathBuf); impl From<&str> for FakeBaseDir { fn from(value: &str) -> Self { Self(PathBuf::from(value)) } } impl BaseDir for FakeBaseDir { fn base_dir(&self) -> Result { Ok(self.0.clone()) } } impl<'a> ValueListFromFiles<'a> { fn new(bdir: impl BaseDir + 'a, g_resover: impl GlobResolver + 'a) -> Self { Self { base_dir: Box::new(bdir), g_resolver: Box::new(g_resover), } } } #[derive(Default)] struct FakeResolver(Vec); impl From<&[&str]> for FakeResolver { fn from(value: &[&str]) -> Self { Self(value.iter().map(ToString::to_string).collect()) } } impl GlobResolver for FakeResolver { fn glob(&self, _pattern: &str) -> Result, String> { Ok(self.0.iter().map(PathBuf::from).collect()) } } #[derive(Default)] struct FakeMapResolver(String, HashMap>); impl From<(&str, &HashMap<&str, &[&str]>)> for FakeMapResolver { fn from(value: (&str, &HashMap<&str, &[&str]>)) -> Self { Self( value.0.to_string(), value .1 .iter() .map(|(&key, &values)| { ( key.to_string(), values .iter() .map(|&v| PathBuf::from(format!("{}/{v}", value.0))) .collect::>(), ) }) .collect(), ) } } impl GlobResolver for FakeMapResolver { fn glob(&self, pattern: &str) -> Result, String> { let pattern = pattern .strip_prefix(&format!("{}{}", self.0, std::path::MAIN_SEPARATOR)) .unwrap(); Ok(self.1.get(pattern).cloned().unwrap_or_default()) } } #[rstest] #[case::simple("/base", None, FakeResolver::from(["/base/first", "/base/second"].as_slice()), vec![], true, &["first", "second"])] #[case::more_glob("/base", Some(["path1", "path2"].as_slice()), FakeMapResolver::from( ("/base", &hashmap!( "path1" => ["first", "second"].as_slice(), "path2" => ["third", "zzzz"].as_slice() )) ), vec![], true, &["first", "second", "third", "zzzz"])] #[case::should_remove_duplicates("/base", Some(["path1", "path2"].as_slice()), FakeMapResolver::from( ("/base", &hashmap!( "path1" => ["first", "second"].as_slice(), "path2" => ["second", "third"].as_slice() )) ), vec![], true, &["first", "second", "third"])] #[case::should_sort("/base", None, FakeResolver::from(["/base/second", "/base/first"].as_slice()), vec![], true, &["first", "second"])] #[case::exclude("/base", None, FakeResolver::from([ "/base/first", "/base/rem_1", "/base/other/rem_2", "/base/second"].as_slice()), vec![Exclude::fake("no_mater", Some(Regex::new("rem_").unwrap()))], true, &["first", "second"])] #[case::exclude_more("/base", None, FakeResolver::from([ "/base/first", "/base/rem_1", "/base/other/rem_2", "/base/some/other", "/base/second"].as_slice()), vec![ Exclude::fake("no_mater", Some(Regex::new("rem_").unwrap())), Exclude::fake("no_mater", Some(Regex::new("some").unwrap())), ], true, &["first", "second"])] #[case::ignore_dot_files("/base", None, FakeResolver::from([ "/base/first", "/base/.ignore", "/base/.ignore_dir/a", "/base/second/.not", "/base/second/but_include", "/base/in/.out/other/ignored"].as_slice()), vec![], true, &["first", "second/but_include"])] #[case::include_dot_files("/base", None, FakeResolver::from([ "/base/first", "/base/.ignore", "/base/.ignore_dir/a", "/base/second/.not", "/base/second/but_include", "/base/in/.out/other/ignored"].as_slice()), vec![], false, &[".ignore", ".ignore_dir/a", "first", "in/.out/other/ignored", "second/.not", "second/but_include"])] #[case::relative_path("/base/some/other/folders", None, FakeResolver::from(["/base/first", "/base/second"].as_slice()), vec![], true, &["../../../first", "../../../second"])] fn generate_a_variable_with_the_glob_resolved_path( #[case] bdir: &str, #[case] paths: Option<&[&str]>, #[case] resolver: impl GlobResolver, #[case] exclude: Vec, #[case] ignore_dot_files: bool, #[case] expected: &[&str], ) { let paths = paths .map(|inner| inner.into_iter().map(files_attr).collect()) .unwrap_or(vec![files_attr("no_mater")]); let values = ValueListFromFiles::new(FakeBaseDir::from(bdir), resolver) .to_value_list(vec![( ident("a"), FilesGlobReferences::new( paths, exclude, true, ignore_dot_files, false, Default::default(), ), )]) .unwrap(); let mut v_list = values_list( "a", &expected .iter() .map(|&p| RelativePath::from_path(p).unwrap()) .map(|r| r.to_logical_path(bdir)) .map(|p| { let p = p.as_os_str().to_str().unwrap(); #[cfg(windows)] let p = p.replace('\\', "/"); format!( r#"<::std::path::PathBuf as std::str::FromStr>::from_str("{p}").unwrap()"#, ) }) .collect::>(), ); v_list .values .iter_mut() .zip(expected.iter()) .for_each(|(v, &ex)| { v.description = Some(render_file_description( &RelativePath::from_path(ex).unwrap(), )) }); assert_eq!(vec![v_list], values); } #[rstest] #[case::file("name.txt", "name.txt")] #[case::in_folder("some/folder/name.txt", "some/folder/name.txt")] #[case::no_extension("name", "name")] #[case::parent("../../name.txt", "_UP/_UP/name.txt")] #[case::ignore_current("./../other/name.txt", "_UP/other/name.txt")] fn render_file_description_should(#[case] path: &str, #[case] expected: &str) { assert_eq!( render_file_description(&RelativePath::from_path(path).unwrap()), expected ); } #[test] #[should_panic(expected = "Fake error")] fn raise_error_if_fail_to_get_root() { #[derive(Default)] struct ErrorBaseDir; impl BaseDir for ErrorBaseDir { fn base_dir(&self) -> Result { Err("Fake error".to_string()) } } ValueListFromFiles::new(ErrorBaseDir::default(), FakeResolver::default()) .to_value_list(vec![( ident("a"), FilesGlobReferences::new( vec![files_attr("no_mater")], Default::default(), true, true, false, Default::default(), ), )]) .unwrap(); } #[test] #[should_panic(expected = "No file found")] fn raise_error_if_no_files_found() { ValueListFromFiles::new(FakeBaseDir::default(), FakeResolver::default()) .to_value_list(vec![( ident("a"), FilesGlobReferences::new( vec![files_attr("no_mater")], Default::default(), true, true, false, Default::default(), ), )]) .unwrap(); } #[test] #[should_panic(expected = "glob failed")] fn default_glob_resolver_raise_error_if_invalid_glob_path() { DefaultGlobResolver.glob("/invalid/path/***").unwrap(); } struct MapEnvironmentResolver(HashMap); impl EnvironmentResolver for MapEnvironmentResolver { fn get(&self, var: &str) -> Option { self.0.get(var).cloned() } } #[rstest] #[case::works(&[("VALUE", "VVV")], false, "some${VALUE}_glob", Ok("someVVV_glob"))] #[case::works_simple(&[("VALUE", "VVV")], false, "some/$VALUE/glob", Ok("some/VVV/glob"))] #[case::unknown_err(&[], false, "some${__UNKNOWN__}_glob", Err("__UNKNOWN__"))] #[case::ignore_unknown(&[], true, "some${__UNKNOWN__}_glob", Ok("some_glob"))] #[case::invalid(&[], false, "some${__%$!@__}_glob", Err("__%$!@__"))] #[case::default_unknown(&[], false, "some${__UNKNOWN__:-VVV}_glob", Ok("someVVV_glob"))] #[case::default_empty(&[("EMPTY", "")], false, "some${EMPTY:-EEE}_glob", Ok("someEEE_glob"))] fn replace_env_vars( #[case] env_vars: &[(&str, &str)], #[case] ignore_missing_env_vars: bool, #[case] glob: &str, #[case] expected: Result<&str, &str>, ) { let resolver = MapEnvironmentResolver( env_vars .iter() .map(|(k, v)| (k.to_string(), v.to_string())) .collect(), ); let refs = FilesGlobReferences::new( vec![], Default::default(), true, true, ignore_missing_env_vars, Default::default(), ); let result = refs.replace_env_vars(&files_attr(glob), resolver); match (&result, expected) { (Ok(r), Ok(e)) => assert_eq!(r, e), (Err(r), Err(e)) => assert_in!(format!("{:?}", r), e), _ => panic!( "Expected and result should be the same. Expected: {:?}, Result: {:?}", expected, result ), } } } rstest_macros-0.26.1/src/parse/rstest/test_attr.rs000064400000000000000000000125371046102023000204160ustar 00000000000000use crate::error::ErrorsVec; use crate::parse::arguments::TestAttr; use crate::parse::just_once::{AttrBuilder, JustOnceFnAttributeExtractor, Validator}; use crate::utils::attr_ends_with; use quote::quote; use syn::visit_mut::VisitMut; use syn::{parse2, parse_quote, Attribute, ItemFn}; pub(crate) fn extract_test_attr(item_fn: &mut ItemFn) -> Result, ErrorsVec> { let mut extractor = JustOnceFnAttributeExtractor::::new("test_attr"); extractor.visit_item_fn_mut(item_fn); extractor.take().map(|extracted| match extracted { Some(a) => Some(a.into()), None => { if first_valid_test_attrs(item_fn.attrs.as_slice()).is_some() { Some(TestAttr::InAttrs) } else { None } } }) } pub fn first_valid_test_attrs(attributes: &[Attribute]) -> Option<&Attribute> { attributes .iter() .find(|attr| attr_ends_with(attr, &parse_quote! { test })) } struct TestAttrBuilder; impl AttrBuilder for TestAttrBuilder { type Out = syn::Attribute; fn build(attr: syn::Attribute, _ident: &ItemFn) -> syn::Result { match &attr.meta { syn::Meta::List(meta_list) => { let tokens = &meta_list.tokens; let item_fn: Option = parse2(quote! {#[#tokens] fn ____f() {}}).ok(); item_fn.and_then(|mut f| f.attrs.pop()) } _ => None, } .ok_or_else(|| { syn::Error::new_spanned( attr, "invalid `test_attr` syntax; should be `#[test_attr()]`", ) }) } } impl Validator for TestAttrBuilder { fn validate(item_fn: &ItemFn) -> syn::Result<()> { match first_valid_test_attrs(item_fn.attrs.as_slice()) { None => Ok(()), Some(t) => Err(syn::Error::new_spanned( t, "You cannot use both explicit `test_attr` and a valid test attribute", )), } } } #[cfg(test)] mod should { use super::*; use crate::test::{assert_eq, *}; use crate::test::{attr, ToAst}; fn explicit(a: &str) -> TestAttr { TestAttr::Explicit(attr(format!("#[{a}]")).into()) } fn in_attrs() -> TestAttr { TestAttr::InAttrs } #[rstest] #[case::base_explicit( "#[test_attr(great_attr_test)]fn test(){}", "fn test(){}", Some(explicit("great_attr_test")) )] #[case::base_in_attr("#[test] fn test(){}", "#[test] fn test(){}", Some(in_attrs()))] #[case::base_no_one("#[a] #[b] #[c] fn test(){}", "#[a] #[b] #[c] fn test(){}", None)] #[case::explicit_with_others( "#[a] #[test_attr(great_attr_test)] #[b] #[c] fn test(){}", "#[a] #[b] #[c] fn test(){}", Some(explicit("great_attr_test")) )] #[case::in_attr_with_others( "#[aa] #[bb] #[test] #[cc] fn test(){}", "#[aa] #[bb] #[test] #[cc] fn test(){}", Some(in_attrs()) )] #[case::in_attr_segment_path( "#[aa] #[bb] #[a::b::c::test] #[cc] fn test(){}", "#[aa] #[bb] #[a::b::c::test] #[cc] fn test(){}", Some(in_attrs()) )] #[case::structured_explicit( "#[test_attr(macro_rules_attribute::apply(smol_macros::test!))] fn test(){}", "fn test(){}", Some(explicit("macro_rules_attribute::apply(smol_macros::test!)")) )] #[case::in_attr_not_guessing( "#[a_test] fn test(){}", "#[a_test] fn test(){}", None )] #[case::in_attr_not_guessing( "#[test_a] fn test(){}", "#[test_a] fn test(){}", None )] fn extract( #[case] item_fn: &str, #[case] expected_fn: &str, #[case] extracted: Option, ) { let mut item_fn: ItemFn = item_fn.ast(); let expected_fn: ItemFn = expected_fn.ast(); let test_attr = extract_test_attr(&mut item_fn).unwrap(); assert_eq!(expected_fn, item_fn); assert_eq!(extracted, test_attr) } mod raise_error_if { use super::*; #[test] #[should_panic(expected = "more than once")] fn explicit_attribute_is_present_more_than_one() { let mut item_fn: ItemFn = r#" #[aa] #[test_attr(great_attr_test)] #[bb] #[test_attr(other)] #[c] fn test(){}"# .ast(); extract_test_attr(&mut item_fn).unwrap(); } #[test] #[should_panic(expected = "cannot use both")] fn have_both_explicit_and_in_attrs() { let mut item_fn: ItemFn = r#" #[test_attr(great_attr_test)] #[my::great::test] fn test(){}"# .ast(); extract_test_attr(&mut item_fn).unwrap(); } #[rstest] #[case::no_value("#[test_attr]")] #[case::name_value(r#"#[test_attr = "some"]"#)] #[case::invalid_list(r#"#[test_attr(a,b)]"#)] #[should_panic(expected = "syntax")] fn the_test_attr_has_wrong_syntax(#[case] attr: &str) { let mut item_fn: ItemFn = format!( r#" {attr} fn test(){{}}"# ) .ast(); extract_test_attr(&mut item_fn).unwrap(); } } } rstest_macros-0.26.1/src/parse/rstest.rs000064400000000000000000001043561046102023000164060ustar 00000000000000use syn::{parse::{Parse, ParseStream}, Ident, ItemFn, Pat, Token}; use self::files::{extract_files, ValueListFromFiles}; use super::{ arguments::ArgumentsInfo, by_ref::extract_by_ref, check_timeout_attrs, context::extract_context, extract_case_args, extract_cases, extract_excluded_trace, extract_fixtures, extract_value_list, future::{extract_futures, extract_global_awt}, ignore::extract_ignores, parse_vector_trailing_till_double_comma, testcase::TestCase, Attribute, Attributes, ExtendWithFunctionAttrs, Fixture, }; use crate::{error::ErrorsVec, refident::IntoPat}; use crate::{parse::vlist::ValueList, refident::MaybePat}; use proc_macro2::{Span, TokenStream}; use quote::{format_ident, ToTokens}; use crate::parse::rstest::test_attr::extract_test_attr; pub(crate) mod files; mod test_attr; #[derive(PartialEq, Debug, Default)] pub(crate) struct RsTestInfo { pub(crate) data: RsTestData, pub(crate) attributes: RsTestAttributes, pub(crate) arguments: ArgumentsInfo, } impl Parse for RsTestInfo { fn parse(input: ParseStream) -> syn::Result { Ok(if input.is_empty() { Default::default() } else { Self { data: input.parse()?, attributes: input .parse::() .or_else(|_| Ok(Default::default())) .and_then(|_| input.parse())?, arguments: Default::default(), } }) } } impl ExtendWithFunctionAttrs for RsTestInfo { fn extend_with_function_attrs(&mut self, item_fn: &mut ItemFn) -> Result<(), ErrorsVec> { let (composed_tuple!( excluded, _timeout, futures, global_awt, by_refs, ignores, contexts, test_attr // Append here new extraction ), _inner) = merge_errors!( merge_errors!( extract_excluded_trace(item_fn), check_timeout_attrs(item_fn), extract_futures(item_fn), extract_global_awt(item_fn), extract_by_ref(item_fn), extract_ignores(item_fn), extract_context(item_fn), extract_test_attr(item_fn), // Append here new extraction ), // This one should be always the last one! self.data.extend_with_function_attrs(item_fn) )?; self.attributes.add_notraces(excluded); self.arguments.set_global_await(global_awt); self.arguments.set_futures(futures.into_iter()); self.arguments.set_by_refs(by_refs.into_iter()); self.arguments.set_ignores(ignores.into_iter()); self.arguments.set_contexts(contexts.into_iter()); self.arguments.set_test_attr(test_attr); self.arguments .register_inner_destructored_idents_names(item_fn); Ok(()) } } #[derive(PartialEq, Debug, Default)] pub(crate) struct RsTestData { pub(crate) items: Vec, } impl RsTestData { pub(crate) fn case_args(&self) -> impl Iterator { self.items.iter().filter_map(|it| match it { RsTestItem::CaseArgName(ref arg) => Some(arg), _ => None, }) } #[allow(dead_code)] pub(crate) fn has_case_args(&self) -> bool { self.case_args().next().is_some() } pub(crate) fn cases(&self) -> impl Iterator { self.items.iter().filter_map(|it| match it { RsTestItem::TestCase(ref case) => Some(case), _ => None, }) } pub(crate) fn has_cases(&self) -> bool { self.cases().next().is_some() } pub(crate) fn fixtures(&self) -> impl Iterator { self.items.iter().filter_map(|it| match it { RsTestItem::Fixture(ref fixture) => Some(fixture), _ => None, }) } #[allow(dead_code)] pub(crate) fn has_fixtures(&self) -> bool { self.fixtures().next().is_some() } pub(crate) fn list_values(&self) -> impl Iterator { self.items.iter().filter_map(|mv| match mv { RsTestItem::ValueList(ref value_list) => Some(value_list), _ => None, }) } pub(crate) fn has_list_values(&self) -> bool { self.list_values().next().is_some() } } impl Parse for RsTestData { fn parse(input: ParseStream) -> syn::Result { if input.peek(Token![::]) { Ok(Default::default()) } else { Ok(Self { items: parse_vector_trailing_till_double_comma::<_, Token![,]>(input)?, }) } } } impl ExtendWithFunctionAttrs for RsTestData { fn extend_with_function_attrs(&mut self, item_fn: &mut ItemFn) -> Result<(), ErrorsVec> { let composed_tuple!(fixtures, case_args, cases, value_list, files) = merge_errors!( extract_fixtures(item_fn), extract_case_args(item_fn), extract_cases(item_fn), extract_value_list(item_fn), extract_files(item_fn) )?; self.items.extend(fixtures.into_iter().map(|f| f.into())); self.items.extend(case_args.into_iter().map(|f| f.into())); self.items.extend(cases.into_iter().map(|f| f.into())); self.items.extend(value_list.into_iter().map(|f| f.into())); self.items.extend( ValueListFromFiles::default() .to_value_list(files)? .into_iter() .map(|f| f.into()), ); Ok(()) } } #[derive(PartialEq, Debug)] pub(crate) enum RsTestItem { Fixture(Fixture), CaseArgName(Pat), TestCase(TestCase), ValueList(ValueList), } impl MaybePat for Fixture { fn maybe_pat(&self) -> Option<&syn::Pat> { Some(&self.arg) } } impl MaybePat for RsTestItem { fn maybe_pat(&self) -> Option<&syn::Pat> { match self { RsTestItem::Fixture(f) => f.maybe_pat(), RsTestItem::CaseArgName(c) => Some(c), RsTestItem::TestCase(_) => None, RsTestItem::ValueList(vl) => Some(&vl.arg), } } } impl From for RsTestItem { fn from(f: Fixture) -> Self { RsTestItem::Fixture(f) } } impl From for RsTestItem { fn from(pat: Pat) -> Self { RsTestItem::CaseArgName(pat) } } impl From for RsTestItem { fn from(ident: Ident) -> Self { RsTestItem::CaseArgName(ident.into_pat()) } } impl From for RsTestItem { fn from(case: TestCase) -> Self { RsTestItem::TestCase(case) } } impl From for RsTestItem { fn from(value_list: ValueList) -> Self { RsTestItem::ValueList(value_list) } } impl Parse for RsTestItem { fn parse(input: ParseStream) -> syn::Result { if input.fork().parse::().is_ok() { input.parse::().map(RsTestItem::TestCase) } else if input.peek2(Token![=>]) { input.parse::().map(RsTestItem::ValueList) } else if input.fork().parse::().is_ok() { input.parse::().map(RsTestItem::Fixture) } else if input.fork().parse::().is_ok() { input .parse::() .map(IntoPat::into_pat) .map(RsTestItem::CaseArgName) } else { Err(syn::Error::new(Span::call_site(), "Cannot parse it")) } } } impl ToTokens for RsTestItem { fn to_tokens(&self, tokens: &mut TokenStream) { use RsTestItem::*; match self { Fixture(ref fixture) => fixture.to_tokens(tokens), CaseArgName(ref case_arg) => case_arg.to_tokens(tokens), TestCase(ref case) => case.to_tokens(tokens), ValueList(ref list) => list.to_tokens(tokens), } } } wrap_attributes!(RsTestAttributes); impl RsTestAttributes { const TRACE_VARIABLE_ATTR: &'static str = "trace"; const NOTRACE_VARIABLE_ATTR: &'static str = "notrace"; pub(crate) fn trace_me(&self, pat: &Pat) -> bool { if self.should_trace() { !self.iter().any(|m| Self::is_notrace(pat, m)) } else { false } } fn is_notrace(pat: &Pat, m: &Attribute) -> bool { match m { Attribute::Tagged(i, args) if i == Self::NOTRACE_VARIABLE_ATTR => { args.iter().any(|a| a == pat) } _ => false, } } pub(crate) fn should_trace(&self) -> bool { self.iter().any(Self::is_trace) } pub(crate) fn add_trace(&mut self, trace: Ident) { self.inner.attributes.push(Attribute::Attr(trace)); } pub(crate) fn add_notraces(&mut self, notraces: Vec) { if notraces.is_empty() { return; } self.inner.attributes.push(Attribute::Tagged( format_ident!("{}", Self::NOTRACE_VARIABLE_ATTR), notraces, )); } fn is_trace(m: &Attribute) -> bool { matches!(m, Attribute::Attr(i) if i == Self::TRACE_VARIABLE_ATTR) } } impl Parse for RsTestAttributes { fn parse(input: ParseStream) -> syn::Result { Ok(input.parse::()?.into()) } } #[cfg(test)] mod test { use super::*; use crate::test::{assert_eq, *}; use rstest_test::assert_in; mod parse_rstest_data { use super::assert_eq; use super::*; fn parse_rstest_data>(fixtures: S) -> RsTestData { parse_meta(fixtures) } #[test] fn one_arg() { let fixtures = parse_rstest_data("my_fixture(42)"); let expected = RsTestData { items: vec![fixture("my_fixture", &["42"]).into()], }; assert_eq!(expected, fixtures); } } #[test] fn should_check_all_timeout_to_catch_the_right_errors() { let mut item_fn = r#" #[timeout()] #[timeout(42)] #[timeout] #[timeout(Duration::from_millis(20))] fn test_fn(#[case] arg: u32) { } "# .ast(); let mut info = RsTestInfo::default(); let errors = info.extend_with_function_attrs(&mut item_fn).unwrap_err(); assert_eq!(2, errors.len()); } #[cfg(feature = "async-timeout")] #[test] fn should_parse_async_timeout() { let mut item_fn = r#" #[timeout(Duration::from_millis(20))] async fn test_fn(#[case] arg: u32) { } "# .ast(); let mut info = RsTestInfo::default(); info.extend_with_function_attrs(&mut item_fn).unwrap(); } #[cfg(not(feature = "async-timeout"))] #[test] fn should_return_error_for_async_timeout() { let mut item_fn = r#" #[timeout(Duration::from_millis(20))] async fn test_fn(#[case] arg: u32) { } "# .ast(); let mut info = RsTestInfo::default(); let errors = info.extend_with_function_attrs(&mut item_fn).unwrap_err(); assert_eq!(1, errors.len()); assert!(format!("{:?}", errors).contains("async-timeout feature")) } fn parse_rstest>(rstest_data: S) -> RsTestInfo { parse_meta(rstest_data) } mod no_cases { use std::collections::HashSet; use super::{assert_eq, *}; #[test] fn happy_path() { let data = parse_rstest( r#"my_fixture(42, "other"), other(vec![42]) :: trace :: no_trace(some)"#, ); let expected = RsTestInfo { data: vec![ fixture("my_fixture", &["42", r#""other""#]).into(), fixture("other", &["vec![42]"]).into(), ] .into(), attributes: Attributes { attributes: vec![ Attribute::attr("trace"), Attribute::tagged("no_trace", vec!["some"]), ], } .into(), ..Default::default() }; assert_eq!(expected, data); } mod fixture_extraction { use super::{assert_eq, *}; #[test] fn rename() { let data = parse_rstest( r#"long_fixture_name(42, "other") as short, sub_module::fix as f, simple as s, no_change()"#, ); let expected = RsTestInfo { data: vec![ fixture("short", &["42", r#""other""#]) .with_resolve("long_fixture_name") .into(), fixture("f", &[]).with_resolve("sub_module::fix").into(), fixture("s", &[]).with_resolve("simple").into(), fixture("no_change", &[]).into(), ] .into(), ..Default::default() }; assert_eq!(expected, data); } #[test] fn rename_with_attributes() { let mut item_fn = r#" fn test_fn( #[from(long_fixture_name)] #[with(42, "other")] short: u32, #[from(simple)] s: &str, #[from(sub_module::fix)] f: u32, no_change: i32) { } "# .ast(); let expected = RsTestInfo { data: vec![ fixture("short", &["42", r#""other""#]) .with_resolve("long_fixture_name") .into(), fixture("s", &[]).with_resolve("simple").into(), fixture("f", &[]).with_resolve("sub_module::fix").into(), ] .into(), ..Default::default() }; let mut data = RsTestInfo::default(); data.extend_with_function_attrs(&mut item_fn).unwrap(); assert_eq!(expected, data); } #[test] fn defined_via_with_attributes() { let mut item_fn = r#" fn test_fn(#[with(42, "other")] my_fixture: u32, #[with(vec![42])] other: &str) { } "# .ast(); let expected = RsTestInfo { data: vec![ fixture("my_fixture", &["42", r#""other""#]).into(), fixture("other", &["vec![42]"]).into(), ] .into(), ..Default::default() }; let mut data = RsTestInfo::default(); data.extend_with_function_attrs(&mut item_fn).unwrap(); assert_eq!(expected, data); } } #[test] fn empty_fixtures() { let data = parse_rstest(r#"::trace::no_trace(some)"#); let expected = RsTestInfo { attributes: Attributes { attributes: vec![ Attribute::attr("trace"), Attribute::tagged("no_trace", vec!["some"]), ], } .into(), ..Default::default() }; assert_eq!(expected, data); } #[test] fn empty_attributes() { let data = parse_rstest(r#"my_fixture(42, "other")"#); let expected = RsTestInfo { data: vec![fixture("my_fixture", &["42", r#""other""#]).into()].into(), ..Default::default() }; assert_eq!(expected, data); } #[test] fn extract_notrace_args_attribute() { let mut item_fn = r#" fn test_fn(#[notrace] a: u32, #[something_else] b: &str, #[notrace] c: i32) { } "# .ast(); let mut info = RsTestInfo::default(); info.extend_with_function_attrs(&mut item_fn).unwrap(); info.attributes.add_trace(ident("trace")); assert!(!info.attributes.trace_me(&pat("a"))); assert!(info.attributes.trace_me(&pat("b"))); assert!(!info.attributes.trace_me(&pat("c"))); let b_args = item_fn .sig .inputs .into_iter() .nth(1) .and_then(|id| match id { syn::FnArg::Typed(arg) => Some(arg.attrs), _ => None, }) .unwrap(); assert_eq!(attrs("#[something_else]"), b_args); } #[rstest] fn extract_future() { let mut item_fn = "fn f(#[future] a: u32, b: u32) {}".ast(); let expected = "fn f(a: u32, b: u32) {}".ast(); let mut info = RsTestInfo::default(); info.extend_with_function_attrs(&mut item_fn).unwrap(); assert_eq!(item_fn, expected); assert!(info.arguments.is_future(&pat("a"))); assert!(!info.arguments.is_future(&pat("b"))); } #[rstest] fn extract_context() { let mut item_fn = "fn f(#[context] c: Context, #[context] other: Context, more: u32) {}".ast(); let expected = "fn f(c: Context, other: Context, more: u32) {}".ast(); let mut info = RsTestInfo::default(); info.extend_with_function_attrs(&mut item_fn).unwrap(); assert_eq!(item_fn, expected); assert_eq!( info.arguments.contexts().cloned().collect::>(), vec![pat("c"), pat("other")] .into_iter() .collect::>() ); } #[test] fn extract_test_attr() { let mut item_fn = "#[test_attr(some_valid_data)] fn f() {}".ast(); let expected = "fn f() {}".ast(); let mut info = RsTestInfo::default(); info.extend_with_function_attrs(&mut item_fn).unwrap(); assert_eq!(item_fn, expected); assert_eq!( info.arguments.test_attr().cloned(), Some(attr("#[some_valid_data]").into()), ); } } mod parametrize_cases { use super::{assert_eq, *}; #[test] fn one_simple_case_one_arg() { let data = parse_rstest(r#"arg, case(42)"#).data; let args = data.case_args().collect::>(); let cases = data.cases().collect::>(); assert_eq!(1, args.len()); assert_eq!(1, cases.len()); assert_eq!("arg", &args[0].display_code()); assert_eq!(to_args!(["42"]), cases[0].args()) } #[test] fn happy_path() { let info = parse_rstest( r#" my_fixture(42,"foo"), arg1, arg2, arg3, case(1,2,3), case(11,12,13), case(21,22,23) "#, ); let data = info.data; let fixtures = data.fixtures().cloned().collect::>(); assert_eq!(vec![fixture("my_fixture", &["42", r#""foo""#])], fixtures); assert_eq!( to_strs!(vec!["arg1", "arg2", "arg3"]), data.case_args() .map(DisplayCode::display_code) .collect::>() ); let cases = data.cases().collect::>(); assert_eq!(3, cases.len()); assert_eq!(to_args!(["1", "2", "3"]), cases[0].args()); assert_eq!(to_args!(["11", "12", "13"]), cases[1].args()); assert_eq!(to_args!(["21", "22", "23"]), cases[2].args()); } mod defined_via_with_attributes { use super::{assert_eq, *}; #[test] fn one_case() { let mut item_fn = r#" #[case::first(42, "first")] fn test_fn(#[case] arg1: u32, #[case] arg2: &str) { } "# .ast(); let mut info = RsTestInfo::default(); info.extend_with_function_attrs(&mut item_fn).unwrap(); let case_args = info.data.case_args().cloned().collect::>(); let cases = info.data.cases().cloned().collect::>(); assert_eq!(to_pats!(["arg1", "arg2"]), case_args); assert_eq!( vec![ TestCase::from_iter(["42", r#""first""#].iter()).with_description("first"), ], cases ); } #[test] fn destruct_case() { let mut item_fn: ItemFn = r#" #[case::destruct(T::new(2, 21))] fn test_fn(#[case] T{a, b}: T) { } "# .ast(); let mut info = RsTestInfo::default(); info.extend_with_function_attrs(&mut item_fn).unwrap(); let case_args = info.data.case_args().cloned().collect::>(); let cases = info.data.cases().cloned().collect::>(); // Should just remove attributes assert_eq!( to_fnargs!(["T{a, b}: T"]), item_fn.sig.inputs.into_iter().collect::>() ); assert_eq!(to_pats!(["T{a, b}"]), case_args); assert_eq!( vec![ TestCase::from_iter(["T::new(2, 21)"].iter()).with_description("destruct"), ], cases ); assert_eq!( info.arguments.inner_pat(&pat("T{a, b}")), &pat("__destruct_1") ); } #[test] fn parse_tuple_value() { let mut item_fn = r#" #[case(42, (24, "first"))] fn test_fn(#[case] arg1: u32, #[case] tupled: (u32, &str)) { } "# .ast(); let mut info = RsTestInfo::default(); info.extend_with_function_attrs(&mut item_fn).unwrap(); let cases = info.data.cases().cloned().collect::>(); assert_eq!( vec![TestCase::from_iter(["42", r#"(24, "first")"#].iter()),], cases ); } #[test] fn more_cases() { let mut item_fn = r#" #[case::first(42, "first")] #[case(24, "second")] #[case::third(0, "third")] fn test_fn(#[case] arg1: u32, #[case] arg2: &str) { } "# .ast(); let mut info = RsTestInfo::default(); info.extend_with_function_attrs(&mut item_fn).unwrap(); let case_args = info.data.case_args().cloned().collect::>(); let cases = info.data.cases().cloned().collect::>(); assert_eq!(to_pats!(["arg1", "arg2"]), case_args); assert_eq!( vec![ TestCase::from_iter(["42", r#""first""#].iter()).with_description("first"), TestCase::from_iter(["24", r#""second""#].iter()), TestCase::from_iter(["0", r#""third""#].iter()).with_description("third"), ], cases ); } #[test] fn should_collect_attributes() { let mut item_fn = r#" #[first] #[first2(42)] #[case(42)] #[second] #[case(24)] #[global] fn test_fn(#[case] arg: u32) { } "# .ast(); let mut info = RsTestInfo::default(); info.extend_with_function_attrs(&mut item_fn).unwrap(); let cases = info.data.cases().cloned().collect::>(); assert_eq!( vec![ TestCase::from_iter(["42"].iter()).with_attrs(attrs( " #[first] #[first2(42)] " )), TestCase::from_iter(["24"].iter()).with_attrs(attrs( " #[second] " )), ], cases ); } #[test] fn should_consume_all_used_attributes() { let mut item_fn = r#" #[first] #[first2(42)] #[case(42)] #[second] #[case(24)] #[global] fn test_fn(#[case] arg: u32) { } "# .ast(); let mut info = RsTestInfo::default(); info.extend_with_function_attrs(&mut item_fn).unwrap(); assert_eq!( item_fn.attrs, attrs( " #[global] " ) ); assert!(!format!("{:?}", item_fn).contains("case")); } #[test] fn should_report_all_errors() { let mut item_fn = r#" #[case(#case_error#)] fn test_fn(#[case] arg: u32, #[with(#fixture_error#)] err_fixture: u32) { } "# .ast(); let mut info = RsTestInfo::default(); let errors = info.extend_with_function_attrs(&mut item_fn).unwrap_err(); assert_eq!(2, errors.len()); } } #[test] fn should_accept_comma_at_the_end_of_cases() { let data = parse_rstest( r#" arg, case(42), "#, ) .data; let args = data.case_args().collect::>(); let cases = data.cases().collect::>(); assert_eq!(1, args.len()); assert_eq!(1, cases.len()); assert_eq!("arg", &args[0].display_code()); assert_eq!(to_args!(["42"]), cases[0].args()) } #[test] #[should_panic] fn should_not_accept_invalid_separator_from_args_and_cases() { parse_rstest( r#" ret case::should_success(Ok(())), case::should_fail(Err("Return Error")) "#, ); } #[test] fn case_could_be_arg_name() { let data = parse_rstest( r#" case, case(42) "#, ) .data; assert_eq!("case", &data.case_args().next().unwrap().display_code()); let cases = data.cases().collect::>(); assert_eq!(1, cases.len()); assert_eq!(to_args!(["42"]), cases[0].args()); } #[test] fn should_reject_case_args_marked_more_than_once() { let mut item_fn = r#" #[case(42)] fn test_fn(#[case] #[case] arg: u32) { } "# .ast(); let mut info = RsTestInfo::default(); let errors = info.extend_with_function_attrs(&mut item_fn).unwrap_err(); assert_eq!(1, errors.len()); assert_in!(errors[0].to_string(), "more than once"); } } mod matrix_cases { use super::{assert_eq, *}; #[test] fn happy_path() { let info = parse_rstest( r#" expected => [12, 34 * 2], input => [format!("aa_{}", 2), "other"], "#, ); let value_ranges = info.data.list_values().collect::>(); assert_eq!(2, value_ranges.len()); assert_eq!(to_args!(["12", "34 * 2"]), value_ranges[0].args()); assert_eq!( to_args!([r#"format!("aa_{}", 2)"#, r#""other""#]), value_ranges[1].args() ); assert_eq!(info.attributes, Default::default()); } #[test] fn should_parse_attributes_too() { let info = parse_rstest( r#" a => [12, 24, 42] ::trace "#, ); assert_eq!( info.attributes, Attributes { attributes: vec![Attribute::attr("trace")] } .into() ); } #[test] fn should_parse_injected_fixtures_too() { let info = parse_rstest( r#" a => [12, 24, 42], fixture_1(42, "foo"), fixture_2("bar") "#, ); let fixtures = info.data.fixtures().cloned().collect::>(); assert_eq!( vec![ fixture("fixture_1", &["42", r#""foo""#]), fixture("fixture_2", &[r#""bar""#]) ], fixtures ); } #[test] #[should_panic(expected = "should not be empty")] fn should_not_compile_if_empty_expression_slice() { parse_rstest( r#" invalid => [] "#, ); } mod defined_via_with_attributes { use super::{assert_eq, *}; #[test] fn one_arg() { let mut item_fn = r#" fn test_fn(#[values(1, 2, 1+2)] arg1: u32, #[values(format!("a"), "b b".to_owned(), String::new())] arg2: String) { } "# .ast(); let mut info = RsTestInfo::default(); info.extend_with_function_attrs(&mut item_fn).unwrap(); let list_values = info.data.list_values().cloned().collect::>(); assert_eq!(2, list_values.len()); assert_eq!(to_args!(["1", "2", "1+2"]), list_values[0].args()); assert_eq!( to_args!([r#"format!("a")"#, r#""b b".to_owned()"#, "String::new()"]), list_values[1].args() ); } #[test] fn destruct() { let mut item_fn = r#" fn test_fn(#[values(S(1,2), S(3,4))] S(a,b): S, #[values(T::new("a", "b"), T{s: "a" ,t: "c" })] T{s, t}: T) { } "# .ast(); let mut info = RsTestInfo::default(); info.extend_with_function_attrs(&mut item_fn).unwrap(); let list_values = info.data.list_values().cloned().collect::>(); // Should just remove attributes assert_eq!( to_fnargs!(["S(a, b): S", "T{s, t}: T"]), item_fn.sig.inputs.into_iter().collect::>() ); assert_eq!(2, list_values.len()); assert_eq!(list_values[0].arg, pat("S(a, b)")); assert_eq!(to_args!(["S(1,2)", "S(3,4)"]), list_values[0].args()); assert_eq!(list_values[1].arg, pat("T{s, t}")); assert_eq!( to_args!([r#"T::new("a", "b")"#, r#"T{s: "a" ,t: "c" }"#]), list_values[1].args() ); assert_eq!( info.arguments.inner_pat(&pat("S(a, b)")), &pat("__destruct_1") ); assert_eq!( info.arguments.inner_pat(&pat("T{s, t}")), &pat("__destruct_2") ); } } #[test] fn should_reject_values_attribute_marked_more_than_once() { let mut item_fn = r#" fn test_fn(#[values(1, 2, 1+2)] #[values(1, 2, 1+2)] arg1: u32, ) { } "# .ast(); let mut info = RsTestInfo::default(); let errors = info.extend_with_function_attrs(&mut item_fn).unwrap_err(); assert_eq!(1, errors.len()); assert_in!(errors[0].to_string(), "more than once"); } } mod integrated { use super::{assert_eq, *}; #[test] fn should_parse_fixture_cases_and_matrix_in_any_order() { let data = parse_rstest( r#" u, m => [1, 2], case(42, A{}, D{}), a, case(43, A{}, D{}), the_fixture(42), mm => ["f", "oo", "BAR"], d "#, ) .data; let fixtures = data.fixtures().cloned().collect::>(); assert_eq!(vec![fixture("the_fixture", &["42"])], fixtures); assert_eq!( to_strs!(vec!["u", "a", "d"]), data.case_args() .map(DisplayCode::display_code) .collect::>() ); let cases = data.cases().collect::>(); assert_eq!(2, cases.len()); assert_eq!(to_args!(["42", "A{}", "D{}"]), cases[0].args()); assert_eq!(to_args!(["43", "A{}", "D{}"]), cases[1].args()); let value_ranges = data.list_values().collect::>(); assert_eq!(2, value_ranges.len()); assert_eq!(to_args!(["1", "2"]), value_ranges[0].args()); assert_eq!( to_args!([r#""f""#, r#""oo""#, r#""BAR""#]), value_ranges[1].args() ); } } } rstest_macros-0.26.1/src/parse/testcase.rs000064400000000000000000000104431046102023000166660ustar 00000000000000use syn::{ parse::{Error, Parse, ParseStream, Result}, punctuated::Punctuated, Attribute, Expr, Ident, Token, }; use proc_macro2::TokenStream; use quote::ToTokens; #[derive(PartialEq, Debug, Clone)] /// A test case instance data. Contains a list of arguments. It is parsed by parametrize /// attributes. pub(crate) struct TestCase { pub(crate) args: Vec, pub(crate) attrs: Vec, pub(crate) description: Option, } impl Parse for TestCase { fn parse(input: ParseStream) -> Result { let attrs = Attribute::parse_outer(input)?; let case: Ident = input.parse()?; if case == "case" { let mut description = None; if input.peek(Token![::]) { let _ = input.parse::(); description = Some(input.parse()?); } let content; let _ = syn::parenthesized!(content in input); let args = Punctuated::::parse_terminated(&content)? .into_iter() .collect(); Ok(TestCase { args, attrs, description, }) } else { Err(Error::new(case.span(), "expected a test case")) } } } impl ToTokens for TestCase { fn to_tokens(&self, tokens: &mut TokenStream) { self.args.iter().for_each(|c| c.to_tokens(tokens)) } } #[cfg(test)] mod should { use super::*; use crate::test::{assert_eq, *}; fn parse_test_case>(test_case: S) -> TestCase { parse_meta(test_case) } #[test] fn two_literal_args() { let test_case = parse_test_case(r#"case(42, "value")"#); let args = test_case.args(); let expected = to_args!(["42", r#""value""#]); assert_eq!(expected, args); } #[test] fn some_literals() { let args_expressions = literal_expressions_str(); let test_case = parse_test_case(&format!("case({})", args_expressions.join(", "))); let args = test_case.args(); assert_eq!(to_args!(args_expressions), args); } #[test] fn accept_arbitrary_rust_code() { let test_case = parse_test_case(r#"case(vec![1,2,3])"#); let args = test_case.args(); assert_eq!(to_args!(["vec![1, 2, 3]"]), args); } #[test] #[should_panic] fn raise_error_on_invalid_rust_code() { parse_test_case(r#"case(some:<>(1,2,3))"#); } #[test] fn get_description_if_any() { let test_case = parse_test_case(r#"case::this_test_description(42)"#); let args = test_case.args(); assert_eq!( "this_test_description", &test_case.description.unwrap().to_string() ); assert_eq!(to_args!(["42"]), args); } #[test] fn get_description_also_with_more_args() { let test_case = parse_test_case(r#"case :: this_test_description (42, 24)"#); let args = test_case.args(); assert_eq!( "this_test_description", &test_case.description.unwrap().to_string() ); assert_eq!(to_args!(["42", "24"]), args); } #[test] fn parse_arbitrary_rust_code_as_expression() { let test_case = parse_test_case( r##" case(42, -42, pippo("pluto"), Vec::new(), String::from(r#"prrr"#), { let mut sum=0; for i in 1..3 { sum += i; } sum }, vec![1,2,3] )"##, ); let args = test_case.args(); assert_eq!( to_args!([ "42", "-42", r#"pippo("pluto")"#, "Vec::new()", r##"String::from(r#"prrr"#)"##, r#"{let mut sum=0;for i in 1..3 {sum += i;}sum}"#, "vec![1,2,3]" ]), args ); } #[test] fn save_attributes() { let test_case = parse_test_case(r#"#[should_panic]#[other_attr(x)]case(42)"#); let content = format!("{:?}", test_case.attrs); assert_eq!(2, test_case.attrs.len()); assert!(content.contains("should_panic")); assert!(content.contains("other_attr")); } } rstest_macros-0.26.1/src/parse/vlist.rs000064400000000000000000000060021046102023000162100ustar 00000000000000use proc_macro2::TokenStream; use quote::ToTokens; use syn::{ parse::{Parse, ParseStream, Result}, Expr, Ident, Pat, Token, }; use crate::refident::IntoPat; use super::expressions::Expressions; #[derive(Debug, PartialEq, Clone)] pub(crate) struct Value { pub(crate) expr: Expr, pub(crate) description: Option, } impl Value { pub(crate) fn new(expr: Expr, description: Option) -> Self { Self { expr, description } } pub(crate) fn description(&self) -> String { self.description .clone() .unwrap_or_else(|| self.expr.to_token_stream().to_string()) } } impl From for Value { fn from(expr: Expr) -> Self { Self::new(expr, None) } } #[derive(Debug, PartialEq, Clone)] pub(crate) struct ValueList { pub(crate) arg: Pat, pub(crate) values: Vec, } impl Parse for ValueList { fn parse(input: ParseStream) -> Result { let ident: Ident = input.parse()?; let _to: Token![=>] = input.parse()?; let content; let paren = syn::bracketed!(content in input); let values: Expressions = content.parse()?; let ret = Self { arg: ident.into_pat(), values: values.take().into_iter().map(|e| e.into()).collect(), }; if ret.values.is_empty() { Err(syn::Error::new( paren.span.join(), "Values list should not be empty", )) } else { Ok(ret) } } } impl ToTokens for ValueList { fn to_tokens(&self, tokens: &mut TokenStream) { self.arg.to_tokens(tokens) } } #[cfg(test)] mod should { use crate::test::{assert_eq, *}; use super::*; mod parse_values_list { use super::assert_eq; use super::*; fn parse_values_list>(values_list: S) -> ValueList { parse_meta(values_list) } #[test] fn some_literals() { let literals = literal_expressions_str(); let name = "argument"; let values_list = parse_values_list(format!( r#"{} => [{}]"#, name, literals .iter() .map(ToString::to_string) .collect::>() .join(", ") )); assert_eq!(name, &values_list.arg.display_code()); assert_eq!(values_list.args(), to_args!(literals)); } #[test] fn raw_code() { let values_list = parse_values_list(r#"no_mater => [vec![1,2,3]]"#); assert_eq!(values_list.args(), to_args!(["vec![1, 2, 3]"])); } #[test] #[should_panic] fn raw_code_with_parsing_error() { parse_values_list(r#"other => [some:<>(1,2,3)]"#); } #[test] #[should_panic(expected = r#"expected square brackets"#)] fn forget_brackets() { parse_values_list(r#"other => 42"#); } } } rstest_macros-0.26.1/src/refident.rs000064400000000000000000000106561046102023000155470ustar 00000000000000/// Provide `RefIdent` and `MaybeIdent` traits that give a shortcut to extract identity reference /// (`syn::Ident` struct). use proc_macro2::Ident; use syn::{FnArg, Pat, PatType, Type}; pub trait RefIdent { /// Return the reference to ident if any fn ident(&self) -> &Ident; } pub trait MaybeIdent { /// Return the reference to ident if any fn maybe_ident(&self) -> Option<&Ident>; } impl MaybeIdent for I { fn maybe_ident(&self) -> Option<&Ident> { Some(self.ident()) } } impl RefIdent for Ident { fn ident(&self) -> &Ident { self } } impl RefIdent for &'_ Ident { fn ident(&self) -> &Ident { self } } impl MaybeIdent for FnArg { fn maybe_ident(&self) -> Option<&Ident> { match self { FnArg::Typed(pat) => pat.maybe_ident(), _ => None, } } } impl MaybeIdent for PatType { fn maybe_ident(&self) -> Option<&Ident> { self.pat.maybe_ident() } } impl MaybeIdent for Pat { fn maybe_ident(&self) -> Option<&Ident> { match self { Pat::Ident(ident) => Some(&ident.ident), _ => None, } } } impl MaybeIdent for Type { fn maybe_ident(&self) -> Option<&Ident> { match self { Type::Path(tp) if tp.qself.is_none() => tp.path.get_ident(), _ => None, } } } pub trait MaybeType { /// Return the reference to type if any fn maybe_type(&self) -> Option<&Type>; } impl MaybeType for FnArg { fn maybe_type(&self) -> Option<&Type> { match self { FnArg::Typed(PatType { ty, .. }) => Some(ty.as_ref()), _ => None, } } } impl MaybeIdent for syn::GenericParam { fn maybe_ident(&self) -> Option<&Ident> { match self { syn::GenericParam::Type(syn::TypeParam { ident, .. }) | syn::GenericParam::Const(syn::ConstParam { ident, .. }) => Some(ident), syn::GenericParam::Lifetime(syn::LifetimeParam { lifetime, .. }) => { Some(&lifetime.ident) } } } } impl MaybeIdent for crate::parse::Attribute { fn maybe_ident(&self) -> Option<&Ident> { use crate::parse::Attribute::*; match self { Attr(ident) | Tagged(ident, _) | Type(ident, _) => Some(ident), } } } pub trait RefPat { /// Return the reference to ident if any fn pat(&self) -> &Pat; } pub trait MaybePatIdent { fn maybe_patident(&self) -> Option<&syn::PatIdent>; } impl MaybePatIdent for FnArg { fn maybe_patident(&self) -> Option<&syn::PatIdent> { match self { FnArg::Typed(PatType { pat, .. }) => match pat.as_ref() { Pat::Ident(ident) => Some(ident), _ => None, }, _ => None, } } } impl MaybePatIdent for Pat { fn maybe_patident(&self) -> Option<&syn::PatIdent> { match self { Pat::Ident(ident) => Some(ident), _ => None, } } } pub trait MaybePatType { fn maybe_pat_type(&self) -> Option<&syn::PatType>; } impl MaybePatType for FnArg { fn maybe_pat_type(&self) -> Option<&syn::PatType> { match self { FnArg::Typed(pt) => Some(pt), _ => None, } } } pub trait MaybePatTypeMut { fn maybe_pat_type_mut(&mut self) -> Option<&mut syn::PatType>; } impl MaybePatTypeMut for FnArg { fn maybe_pat_type_mut(&mut self) -> Option<&mut syn::PatType> { match self { FnArg::Typed(pt) => Some(pt), _ => None, } } } pub trait MaybePat { fn maybe_pat(&self) -> Option<&syn::Pat>; } impl MaybePat for FnArg { fn maybe_pat(&self) -> Option<&syn::Pat> { match self { FnArg::Typed(PatType { pat, .. }) => Some(pat.as_ref()), _ => None, } } } pub trait RemoveMutability { fn remove_mutability(&mut self); } impl RemoveMutability for FnArg { fn remove_mutability(&mut self) { if let FnArg::Typed(PatType { pat, .. }) = self { if let Pat::Ident(ident) = pat.as_mut() { ident.mutability = None } }; } } pub trait IntoPat { fn into_pat(self) -> Pat; } impl IntoPat for Ident { fn into_pat(self) -> Pat { Pat::Ident(syn::PatIdent { attrs: vec![], by_ref: None, mutability: None, ident: self, subpat: None, }) } } rstest_macros-0.26.1/src/render/apply_arguments.rs000064400000000000000000000222741046102023000204370ustar 00000000000000use quote::{format_ident, ToTokens}; use syn::{parse_quote, FnArg, Generics, Ident, ItemFn, Lifetime, Signature, Type, TypeReference}; use crate::{ parse::{arguments::ArgumentsInfo, future::MaybeFutureImplType}, refident::{MaybeIdent, MaybePat, MaybePatIdent, RemoveMutability}, }; pub(crate) trait ApplyArguments { type Output: Sized; type Context; fn apply_arguments( &mut self, arguments: &mut ArgumentsInfo, ctx: &mut Self::Context, ) -> Self::Output; } impl ApplyArguments for FnArg { type Output = Option; type Context = usize; fn apply_arguments( &mut self, arguments: &mut ArgumentsInfo, anoymous_id: &mut usize, ) -> Self::Output { if self .maybe_pat() .map(|id| arguments.is_future(id)) .unwrap_or_default() { self.impl_future_arg(anoymous_id) } else { None } } } fn move_generic_list(data: &mut Generics, other: Generics) { data.lt_token = data.lt_token.or(other.lt_token); data.params = other.params; data.gt_token = data.gt_token.or(other.gt_token); } fn extend_generics_with_lifetimes<'a, 'b>( generics: impl Iterator, lifetimes: impl Iterator, ) -> Generics { let all = lifetimes .map(|lt| lt as &dyn ToTokens) .chain(generics.map(|gp| gp as &dyn ToTokens)); parse_quote! { <#(#all),*> } } impl ApplyArguments for Signature { type Output = (); type Context = (); fn apply_arguments(&mut self, arguments: &mut ArgumentsInfo, _: &mut ()) { let mut anonymous_lt = 0_usize; let new_lifetimes = self .inputs .iter_mut() .filter_map(|arg| arg.apply_arguments(arguments, &mut anonymous_lt)) .collect::>(); if !new_lifetimes.is_empty() || !self.generics.params.is_empty() { let new_generics = extend_generics_with_lifetimes(self.generics.params.iter(), new_lifetimes.iter()); move_generic_list(&mut self.generics, new_generics); } } } impl ApplyArguments for ItemFn { type Output = (); type Context = (); fn apply_arguments(&mut self, arguments: &mut ArgumentsInfo, _: &mut ()) { let args = self.sig.inputs.iter().cloned().collect::>(); self.sig.apply_arguments(arguments, &mut ()); let rebound_awaited_args = args .iter() .filter_map(MaybePat::maybe_pat) .filter(|p| arguments.is_future_await(p)) .filter_map(MaybePatIdent::maybe_patident) .map(|p| { let a = &p.ident; quote::quote! { let #p = #a.await; } }); let orig_block_impl = self.block.clone(); self.block = parse_quote! { { #(#rebound_awaited_args)* #orig_block_impl } }; } } pub(crate) trait ImplFutureArg { fn impl_future_arg(&mut self, anonymous_lt: &mut usize) -> Option; } impl ImplFutureArg for FnArg { fn impl_future_arg(&mut self, anonymous_lt: &mut usize) -> Option { let lifetime_id = self .maybe_ident() .map(|id| format_ident!("_{}", id)) .unwrap_or_else(|| { *anonymous_lt += 1; format_ident!("_anonymous_lt_{}", anonymous_lt) }); match self.as_mut_future_impl_type() { Some(ty) => { let lifetime = update_type_with_lifetime(ty, lifetime_id); *ty = parse_quote! { impl core::future::Future }; self.remove_mutability(); lifetime } None => None, } } } fn update_type_with_lifetime(ty: &mut Type, ident: Ident) -> Option { if let Type::Reference(ty_ref @ TypeReference { lifetime: None, .. }) = ty { let lifetime = Some(syn::Lifetime { apostrophe: ident.span(), ident, }); ty_ref.lifetime.clone_from(&lifetime); lifetime } else { None } } #[cfg(test)] mod should { use super::*; use crate::test::{assert_eq, *}; use syn::ItemFn; #[rstest] #[case("fn simple(a: u32) {}")] #[case("fn more(a: u32, b: &str) {}")] #[case("fn gen>(a: u32, b: S) {}")] #[case("fn attr(#[case] a: u32, #[values(1,2)] b: i32) {}")] fn no_change(#[case] item_fn: &str) { let mut item_fn: ItemFn = item_fn.ast(); let orig = item_fn.clone(); let mut args = ArgumentsInfo::default(); item_fn.sig.apply_arguments(&mut args, &mut ()); assert_eq!(orig, item_fn) } #[rstest] #[case::simple( "fn f(a: u32) {}", &["a"], "fn f(a: impl core::future::Future) {}" )] #[case::more_than_one( "fn f(a: u32, b: String, c: std::collection::HashMap) {}", &["a", "b", "c"], r#"fn f(a: impl core::future::Future, b: impl core::future::Future, c: impl core::future::Future>) {}"#, )] #[case::just_one( "fn f(a: u32, b: String) {}", &["b"], r#"fn f(a: u32, b: impl core::future::Future) {}"# )] #[case::generics( "fn f>(a: S) {}", &["a"], "fn f>(a: impl core::future::Future) {}" )] #[case::remove_mut( "fn f(mut a: u32) {}", &["a"], r#"fn f(a: impl core::future::Future) {}"# )] fn replace_future_basic_type( #[case] item_fn: &str, #[case] futures: &[&str], #[case] expected: &str, ) { let mut item_fn: ItemFn = item_fn.ast(); let expected: ItemFn = expected.ast(); let mut arguments = ArgumentsInfo::default(); futures .into_iter() .for_each(|&f| arguments.add_future(pat(f))); item_fn.sig.apply_arguments(&mut arguments, &mut ()); assert_eq!(expected, item_fn) } #[rstest] #[case::base( "fn f(ident_name: &u32) {}", &["ident_name"], "fn f<'_ident_name>(ident_name: impl core::future::Future) {}" )] #[case::lifetime_already_exists( "fn f<'b>(a: &'b u32) {}", &["a"], "fn f<'b>(a: impl core::future::Future) {}" )] #[case::some_other_generics( "fn f<'b, IT: Iterator>(a: &u32, it: IT) {}", &["a"], "fn f<'_a, 'b, IT: Iterator>(a: impl core::future::Future, it: IT) {}" )] fn replace_reference_type( #[case] item_fn: &str, #[case] futures: &[&str], #[case] expected: &str, ) { let mut item_fn: ItemFn = item_fn.ast(); let expected: ItemFn = expected.ast(); let mut arguments = ArgumentsInfo::default(); futures .into_iter() .for_each(|&f| arguments.add_future(pat(f))); item_fn.sig.apply_arguments(&mut arguments, &mut ()); assert_eq!(expected, item_fn) } mod await_future_args { use rstest_test::{assert_in, assert_not_in}; use crate::parse::arguments::FutureArg; use super::*; #[test] fn with_global_await() { let mut item_fn: ItemFn = r#"fn test(a: i32, b:i32, c:i32) {} "#.ast(); let mut arguments: ArgumentsInfo = Default::default(); arguments.set_global_await(true); arguments.add_future(pat("a")); arguments.add_future(pat("b")); item_fn.apply_arguments(&mut arguments, &mut ()); let code = item_fn.block.display_code(); assert_in!(code, await_argument_code_string("a")); assert_in!(code, await_argument_code_string("b")); assert_not_in!(code, await_argument_code_string("c")); } #[test] fn with_selective_await() { let mut item_fn: ItemFn = r#"fn test(a: i32, b:i32, c:i32) {} "#.ast(); let mut arguments: ArgumentsInfo = Default::default(); arguments.set_future(pat("a"), FutureArg::Define); arguments.set_future(pat("b"), FutureArg::Await); item_fn.apply_arguments(&mut arguments, &mut ()); let code = item_fn.block.display_code(); assert_not_in!(code, await_argument_code_string("a")); assert_in!(code, await_argument_code_string("b")); assert_not_in!(code, await_argument_code_string("c")); } #[test] fn with_mut_await() { let mut item_fn: ItemFn = r#"fn test(mut a: i32) {} "#.ast(); let mut arguments: ArgumentsInfo = Default::default(); arguments.set_future(pat("a").with_mut(), FutureArg::Await); item_fn.apply_arguments(&mut arguments, &mut ()); let code = item_fn.block.display_code(); assert_in!(code, mut_await_argument_code_string("a")); } } } rstest_macros-0.26.1/src/render/crate_resolver.rs000064400000000000000000000012571046102023000202420ustar 00000000000000use syn::parse_quote; pub fn crate_name() -> syn::Path { cfg_if::cfg_if! { if #[cfg(feature = "crate-name")] { use proc_macro_crate::FoundCrate; use quote::format_ident; match proc_macro_crate::crate_name("rstest").expect("rstest is present in `Cargo.toml` qed") { FoundCrate::Itself => parse_quote! { rstest }, FoundCrate::Name(name) => { let myself = format_ident!("{name}"); parse_quote! { #myself } } } } else { parse_quote! { rstest } } } } pub fn std_path() -> syn::Path { let rstest = crate_name(); parse_quote! { #rstest::__std } } rstest_macros-0.26.1/src/render/fixture.rs000064400000000000000000000451631046102023000167150ustar 00000000000000use proc_macro2::{Span, TokenStream}; use syn::token::Async; use syn::{parse_quote, FnArg, Generics, Ident, ItemFn, ReturnType}; use quote::quote; use super::{inject, render_exec_call}; use crate::refident::MaybeIdent; use crate::render::{apply_arguments::ApplyArguments, crate_resolver::std_path}; use crate::resolver::{self, Resolver}; use crate::{parse::fixture::FixtureInfo, utils::generics_clean_up}; fn wrap_return_type_as_static_ref(rt: ReturnType) -> ReturnType { match rt { syn::ReturnType::Type(_, t) => parse_quote! { -> &'static #t }, o => o, } } fn wrap_call_impl_with_call_once_impl(call_impl: TokenStream, rt: &ReturnType) -> TokenStream { let std = std_path(); match rt { syn::ReturnType::Type(_, t) => parse_quote! { static CELL: #std::sync::OnceLock<#t> = #std::sync::OnceLock::new(); CELL.get_or_init(|| #call_impl ) }, _ => parse_quote! { static CELL: #std::sync::Once = #std::sync::Once::new(); CELL.call_once(|| #call_impl ); }, } } pub(crate) fn render(mut fixture: ItemFn, info: FixtureInfo) -> TokenStream { let mut arguments = info.arguments.clone(); fixture.apply_arguments(&mut arguments, &mut ()); let name = &fixture.sig.ident; let asyncness = &fixture.sig.asyncness.clone(); let inner_args = info .arguments .replace_fn_args_with_related_inner_pat(fixture.sig.inputs.iter().cloned()) .collect::>(); let args_ident = inner_args .iter() .filter_map(MaybeIdent::maybe_ident) .cloned() .collect::>(); let orig_attrs = &fixture.attrs; let generics = &fixture.sig.generics; let mut default_output = info .attributes .extract_default_type() .unwrap_or_else(|| fixture.sig.output.clone()); let default_generics = generics_clean_up(&fixture.sig.generics, std::iter::empty(), &default_output); let default_where_clause = &default_generics.where_clause; let where_clause = &fixture.sig.generics.where_clause; let mut output = fixture.sig.output.clone(); let visibility = &fixture.vis; let resolver = ( resolver::fixtures::get(&info.arguments, info.data.fixtures()), resolver::values::get(info.data.values()), ); let generics_idents = generics .type_params() .map(|tp| &tp.ident) .cloned() .collect::>(); let inject = inject::resolve_arguments(inner_args.iter(), &resolver, &generics_idents); let partials = (1..=inner_args.len()).map(|n| { render_partial_impl( &inner_args, &fixture.sig.output, &fixture.sig.generics, fixture.sig.asyncness.as_ref(), n, &resolver, &info, ) }); let args = args_ident .iter() .map(|arg| parse_quote! { #arg }) .collect::>(); let call_get = render_exec_call(parse_quote! { Self::get }, &args, asyncness.is_some()); let mut call_impl = render_exec_call(parse_quote! { #name }, &args, asyncness.is_some()); if info.arguments.is_once() { call_impl = wrap_call_impl_with_call_once_impl(call_impl, &output); output = wrap_return_type_as_static_ref(output); default_output = wrap_return_type_as_static_ref(default_output); } quote! { #[doc(hidden)] #[allow(non_camel_case_types)] #[allow(clippy::empty_structs_with_brackets)] #visibility struct #name {} impl #name { #(#orig_attrs)* #[doc(hidden)] #[allow(unused_mut)] pub #asyncness fn get #generics (#(#inner_args),*) #output #where_clause { #call_impl } #[doc(hidden)] pub #asyncness fn default #default_generics () #default_output #default_where_clause { #inject #call_get } #(#partials)* } #[allow(dead_code)] #fixture } } fn render_partial_impl( args: &[FnArg], output: &ReturnType, generics: &Generics, asyncness: Option<&Async>, n: usize, resolver: &impl Resolver, info: &FixtureInfo, ) -> TokenStream { let mut output = info .attributes .extract_partial_type(n) .unwrap_or_else(|| output.clone()); if info.arguments.is_once() { output = wrap_return_type_as_static_ref(output); } let generics = generics_clean_up(generics, args.iter().take(n), &output); let where_clause = &generics.where_clause; let genercs_idents = generics .type_params() .map(|tp| &tp.ident) .cloned() .collect::>(); let inject = inject::resolve_arguments(args.iter().skip(n), resolver, &genercs_idents); let sign_args = args.iter().take(n); let fixture_args = args .iter() .filter_map(MaybeIdent::maybe_ident) .map(|arg| parse_quote! {#arg}) .collect::>(); let name = Ident::new(&format!("partial_{n}"), Span::call_site()); let call_get = render_exec_call( parse_quote! { Self::get }, &fixture_args, asyncness.is_some(), ); quote! { #[allow(unused_mut)] pub #asyncness fn #name #generics (#(#sign_args),*) #output #where_clause { #inject #call_get } } } #[cfg(test)] mod should { use rstest_test::{assert_in, assert_not_in}; use syn::{ parse::{Parse, ParseStream}, parse2, parse_str, ItemImpl, ItemStruct, Result, }; use crate::parse::{ arguments::{ArgumentsInfo, FutureArg}, Attribute, Attributes, }; use super::*; use crate::test::{assert_eq, *}; use rstest_reuse::*; #[derive(Clone)] struct FixtureOutput { orig: ItemFn, fixture: ItemStruct, core_impl: ItemImpl, } impl Parse for FixtureOutput { fn parse(input: ParseStream) -> Result { Ok(FixtureOutput { fixture: input.parse()?, core_impl: input.parse()?, orig: input.parse()?, }) } } fn parse_fixture>(code: S) -> (ItemFn, FixtureOutput) { let item_fn = parse_str::(code.as_ref()).unwrap(); let tokens = render(item_fn.clone(), Default::default()); (item_fn, parse2(tokens).unwrap()) } fn test_maintains_function_visibility(code: &str) { let (item_fn, out) = parse_fixture(code); assert_eq!(item_fn.vis, out.fixture.vis); assert_eq!(item_fn.vis, out.orig.vis); } fn select_method>(impl_code: ItemImpl, name: S) -> Option { impl_code .items .into_iter() .filter_map(|ii| match ii { syn::ImplItem::Fn(f) => Some(f), _ => None, }) .find(|f| f.sig.ident == name.as_ref()) } #[test] fn maintains_pub_visibility() { test_maintains_function_visibility(r#"pub fn test() { }"#); } #[test] fn maintains_no_pub_visibility() { test_maintains_function_visibility(r#"fn test() { }"#); } #[test] fn implement_a_get_method_with_input_fixture_signature() { let (item_fn, out) = parse_fixture( r#" pub fn test, B>(mut s: String, v: &u32, a: &mut [i32], r: R) -> (u32, B, String, &str) where B: Borrow { } "#, ); let mut signature = select_method(out.core_impl, "get").unwrap().sig; signature.ident = item_fn.sig.ident.clone(); assert_eq!(item_fn.sig, signature); } #[test] fn return_a_static_reference_if_once_attribute() { let item_fn = parse_str::(r#" pub fn test, B>(mut s: String, v: &u32, a: &mut [i32], r: R) -> (u32, B, String, &str) where B: Borrow { } "#).unwrap(); let info = FixtureInfo::default().with_once(); let out: FixtureOutput = parse2(render(item_fn.clone(), info)).unwrap(); let signature = select_method(out.core_impl, "get").unwrap().sig; assert_eq!(signature.output, "-> &'static (u32, B, String, &str)".ast()) } #[template] #[rstest( method => ["default", "get", "partial_1", "partial_2", "partial_3"]) ] #[case::async_fn(true)] #[case::not_async_fn(false)] fn async_fixture_cases(#[case] is_async: bool, method: &str) {} #[apply(async_fixture_cases)] fn fixture_method_should_be_async_if_fixture_function_is_async( #[case] is_async: bool, method: &str, ) { let prefix = if is_async { "async" } else { "" }; let (_, out) = parse_fixture(&format!( r#" pub {} fn test(mut s: String, v: &u32, a: &mut [i32]) -> u32 where B: Borrow {{ }} "#, prefix )); let signature = select_method(out.core_impl, method).unwrap().sig; assert_eq!(is_async, signature.asyncness.is_some()); } #[apply(async_fixture_cases)] fn fixture_method_should_use_await_if_fixture_function_is_async( #[case] is_async: bool, method: &str, ) { let prefix = if is_async { "async" } else { "" }; let (_, out) = parse_fixture(&format!( r#" pub {} fn test(mut s: String, v: &u32, a: &mut [i32]) -> u32 {{ }} "#, prefix )); let body = select_method(out.core_impl, method).unwrap().block; let last_statement = body.stmts.last().unwrap(); let is_await = match last_statement { syn::Stmt::Expr(syn::Expr::Await(_), _) => true, _ => false, }; assert_eq!(is_async, is_await); } #[test] fn implement_a_default_method_with_input_cleaned_fixture_signature_and_no_args() { let (item_fn, out) = parse_fixture( r#" pub fn test, B, F, H: Iterator>(mut s: String, v: &u32, a: &mut [i32], r: R) -> (H, B, String, &str) where F: ToString, B: Borrow { } "#, ); let default_decl = select_method(out.core_impl, "default").unwrap().sig; let expected = parse_str::( r#" pub fn default>() -> (H, B, String, &str) where B: Borrow { } "#, ) .unwrap(); assert_eq!(expected.sig.generics, default_decl.generics); assert_eq!(item_fn.sig.output, default_decl.output); assert!(default_decl.inputs.is_empty()); } #[test] fn use_default_return_type_if_any() { let item_fn = parse_str::( r#" pub fn test, B, F, H: Iterator>() -> (H, B) where F: ToString, B: Borrow { } "#, ) .unwrap(); let tokens = render( item_fn.clone(), FixtureInfo { attributes: Attributes { attributes: vec![Attribute::Type( parse_str("default").unwrap(), parse_str("(impl Iterator, B)").unwrap(), )], } .into(), ..Default::default() }, ); let out: FixtureOutput = parse2(tokens).unwrap(); let expected = parse_str::( r#" pub fn default() -> (impl Iterator, B) where B: Borrow { } "#, ) .unwrap(); let default_decl = select_method(out.core_impl, "default").unwrap().sig; assert_eq!(expected.sig, default_decl); } #[test] fn implement_partial_methods() { let (item_fn, out) = parse_fixture( r#" pub fn test(mut s: String, v: &u32, a: &mut [i32]) -> usize { } "#, ); let partials = (1..=3) .map(|n| { select_method(out.core_impl.clone(), format!("partial_{}", n)) .unwrap() .sig }) .collect::>(); // All 3 methods found assert!(select_method(out.core_impl, "partial_4").is_none()); let expected_1 = parse_str::( r#" pub fn partial_1(mut s: String) -> usize { } "#, ) .unwrap(); assert_eq!(expected_1.sig, partials[0]); for p in partials { assert_eq!(item_fn.sig.output, p.output); } } #[rstest] #[case::base("fn test, U: AsRef, F: ToString>(mut s: S, v: U) -> F {}", vec![ "fn default() -> F {}", "fn partial_1, F: ToString>(mut s: S) -> F {}", "fn partial_2, U: AsRef, F: ToString>(mut s: S, v: U) -> F {}", ] )] #[case::associated_type("fn test(mut i: T) where T::Item: Copy {}", vec![ "fn default() {}", "fn partial_1(mut i: T) where T::Item: Copy {}", ] )] #[case::not_remove_const_generics("fn test(v: [u32; N]) -> [i32; N] {}", vec![ "fn default() -> [i32; N] {}", "fn partial_1(v: [u32; N]) -> [i32; N] {}", ] )] #[case::remove_const_generics("fn test(a: i32, v: [u32; N]) {}", vec![ "fn default() {}", "fn partial_1(a:i32) {}", "fn partial_2(a:i32, v: [u32; N]) {}", ] )] fn clean_generics(#[case] code: &str, #[case] expected: Vec<&str>) { let (item_fn, out) = parse_fixture(code); let n_args = item_fn.sig.inputs.iter().count(); let mut signatures = vec![select_method(out.core_impl.clone(), "default").unwrap().sig]; signatures.extend((1..=n_args).map(|n| { select_method(out.core_impl.clone(), format!("partial_{}", n)) .unwrap() .sig })); let expected = expected .into_iter() .map(parse_str::) .map(|f| f.unwrap().sig) .collect::>(); assert_eq!(expected, signatures); } #[test] fn use_partial_return_type_if_any() { let item_fn = parse_str::( r#" pub fn test, B, F, H: Iterator>(h: H, b: B) -> (H, B) where F: ToString, B: Borrow { } "#, ) .unwrap(); let tokens = render( item_fn.clone(), FixtureInfo { attributes: Attributes { attributes: vec![Attribute::Type( parse_str("partial_1").unwrap(), parse_str("(H, impl Iterator)").unwrap(), )], } .into(), ..Default::default() }, ); let out: FixtureOutput = parse2(tokens).unwrap(); let expected = parse_str::( r#" pub fn partial_1>(h: H) -> (H, impl Iterator) { } "#, ) .unwrap(); let partial = select_method(out.core_impl, "partial_1").unwrap(); assert_eq!(expected.sig, partial.sig); } #[test] fn add_future_boilerplate_if_requested() { let item_fn: ItemFn = r#"async fn test(async_ref_u32: &u32, async_u32: u32,simple: u32) { }"#.ast(); let mut arguments = ArgumentsInfo::default(); arguments.add_future(pat("async_ref_u32")); arguments.add_future(pat("async_u32")); let tokens = render( item_fn.clone(), FixtureInfo { arguments, ..Default::default() }, ); let out: FixtureOutput = parse2(tokens).unwrap(); let expected = parse_str::( r#" async fn get<'_async_ref_u32>( async_ref_u32: impl core::future::Future, async_u32: impl core::future::Future, simple: u32 ) { } "#, ) .unwrap(); let rendered = select_method(out.core_impl, "get").unwrap(); assert_eq!(expected.sig, rendered.sig); } #[test] fn use_global_await() { let item_fn: ItemFn = r#"fn test(a: i32, b:i32, c:i32) {} "#.ast(); let mut arguments: ArgumentsInfo = Default::default(); arguments.set_global_await(true); arguments.add_future(pat("a")); arguments.add_future(pat("b")); let tokens = render( item_fn.clone(), FixtureInfo { arguments, ..Default::default() }, ); let out: FixtureOutput = parse2(tokens).unwrap(); let code = out.orig.display_code(); assert_in!(code, await_argument_code_string("a")); assert_in!(code, await_argument_code_string("b")); assert_not_in!(code, await_argument_code_string("c")); } #[test] fn use_selective_await() { let item_fn: ItemFn = r#"fn test(a: i32, b:i32, c:i32) {} "#.ast(); let mut arguments: ArgumentsInfo = Default::default(); arguments.set_future(pat("a"), FutureArg::Define); arguments.set_future(pat("b"), FutureArg::Await); let tokens = render( item_fn.clone(), FixtureInfo { arguments, ..Default::default() }, ); let out: FixtureOutput = parse2(tokens).unwrap(); let code = out.orig.display_code(); assert_not_in!(code, await_argument_code_string("a")); assert_in!(code, await_argument_code_string("b")); assert_not_in!(code, await_argument_code_string("c")); } } rstest_macros-0.26.1/src/render/inject.rs000064400000000000000000000222271046102023000164770ustar 00000000000000use std::borrow::Cow; use proc_macro2::TokenStream; use quote::quote; use syn::{parse_quote, Expr, FnArg, Ident, Pat, Stmt, Type}; use crate::{ refident::{IntoPat, MaybeIdent, MaybePat, MaybeType}, render::crate_resolver::crate_name, resolver::Resolver, utils::{fn_arg_mutability, IsLiteralExpression}, }; pub(crate) fn resolve_arguments<'a>( args: impl Iterator, resolver: &impl Resolver, generic_types: &[Ident], ) -> TokenStream { let define_vars = args.map(|arg| ArgumentResolver::new(resolver, generic_types).resolve(arg)); quote! { #(#define_vars)* } } struct ArgumentResolver<'resolver, 'idents, 'f, R> where R: Resolver + 'resolver, { resolver: &'resolver R, generic_types_names: &'idents [Ident], magic_conversion: &'f dyn Fn(Cow, &Type) -> Expr, } impl<'resolver, 'idents, R> ArgumentResolver<'resolver, 'idents, '_, R> where R: Resolver + 'resolver, { fn new(resolver: &'resolver R, generic_types_names: &'idents [Ident]) -> Self { Self { resolver, generic_types_names, magic_conversion: &handling_magic_conversion_code, } } fn resolve(&self, arg: &FnArg) -> Option { let pat = arg.maybe_pat()?; let mutability = fn_arg_mutability(arg); let unused_mut: Option = mutability .as_ref() .map(|_| parse_quote! {#[allow(unused_mut)]}); let arg_type = arg.maybe_type()?; let fixture_name = self.fixture_name(pat); let mut fixture = self .resolver .resolve(pat) .or_else(|| self.resolver.resolve(&fixture_name.clone().into_pat())) .unwrap_or_else(|| default_fixture_resolve(&fixture_name)); if fixture.is_literal() && self.type_can_be_get_from_literal_str(arg_type) { fixture = Cow::Owned((self.magic_conversion)(fixture, arg_type)); } Some(parse_quote! { #unused_mut let #pat = #fixture; }) } fn fixture_name(&self, ident: &Pat) -> Ident { let ident = ident .maybe_ident() .cloned() .expect("BUG: Here all arguments should be PatIdent types"); let id_str = ident.to_string(); if id_str.starts_with('_') && !id_str.starts_with("__") { Ident::new(&id_str[1..], ident.span()) } else { ident } } fn type_can_be_get_from_literal_str(&self, t: &Type) -> bool { // Check valid type to apply magic conversion match t { Type::ImplTrait(_) | Type::TraitObject(_) | Type::Infer(_) | Type::Group(_) | Type::Macro(_) | Type::Never(_) | Type::Paren(_) | Type::Verbatim(_) | Type::Slice(_) => return false, _ => {} } match t.maybe_ident() { Some(id) => !self.generic_types_names.contains(id), None => true, } } } fn default_fixture_resolve(ident: &Ident) -> Cow<'_, Expr> { Cow::Owned(parse_quote! { #ident::default() }) } fn handling_magic_conversion_code(fixture: Cow, arg_type: &Type) -> Expr { let rstest_path = crate_name(); parse_quote! { { use #rstest_path::magic_conversion::*; (&&&Magic::<#arg_type>(core::marker::PhantomData)).magic_conversion(#fixture) } } } #[cfg(test)] mod should { use super::*; use crate::{ test::{assert_eq, *}, utils::fn_args, }; #[rstest] #[case::as_is("fix: String", "let fix = fix::default();")] #[case::without_underscore("_fix: String", "let _fix = fix::default();")] #[case::do_not_remove_inner_underscores("f_i_x: String", "let f_i_x = f_i_x::default();")] #[case::do_not_remove_double_underscore("__fix: String", "let __fix = __fix::default();")] #[case::preserve_mut_but_annotate_as_allow_unused_mut( "mut fix: String", "#[allow(unused_mut)] let mut fix = fix::default();" )] fn call_fixture(#[case] arg_str: &str, #[case] expected: &str) { let arg = arg_str.ast(); let injected = ArgumentResolver::new(&EmptyResolver {}, &[]) .resolve(&arg) .unwrap(); assert_eq!(injected, expected.ast()); } #[rstest] #[case::as_is("fix: String", ("fix", expr("bar()")), "let fix = bar();")] #[case::with_allow_unused_mut("mut fix: String", ("fix", expr("bar()")), "#[allow(unused_mut)] let mut fix = bar();")] #[case::without_underscore("_fix: String", ("fix", expr("bar()")), "let _fix = bar();")] #[case::without_remove_underscore_if_value("_orig: S", ("_orig", expr("S{}")), r#"let _orig = S{};"#)] fn call_given_fixture( #[case] arg_str: &str, #[case] rule: (&str, Expr), #[case] expected: &str, ) { let arg = arg_str.ast(); let mut resolver = std::collections::HashMap::new(); resolver.insert(pat(rule.0), &rule.1); let injected = ArgumentResolver::new(&resolver, &[]).resolve(&arg).unwrap(); assert_eq!(injected, expected.ast()); } fn _mock_conversion_code(fixture: Cow, arg_type: &Type) -> Expr { parse_quote! { #fixture as #arg_type } } #[rstest] #[case::implement_it( "fn test(arg: MyType){}", 0, r#"let arg = "value to convert" as MyType;"# )] #[case::discard_impl( "fn test(arg: impl AsRef){}", 0, r#"let arg = "value to convert";"# )] #[case::discard_generic_type( "fn test>(arg: S){}", 0, r#"let arg = "value to convert";"# )] fn handle_magic_conversion(#[case] fn_str: &str, #[case] n_arg: usize, #[case] expected: &str) { let function = fn_str.ast(); let arg = fn_args(&function).nth(n_arg).unwrap(); let generics = function .sig .generics .type_params() .map(|tp| &tp.ident) .cloned() .collect::>(); let mut resolver = std::collections::HashMap::new(); let expr = expr(r#""value to convert""#); resolver.insert(arg.maybe_pat().unwrap().clone(), &expr); let ag = ArgumentResolver { resolver: &resolver, generic_types_names: &generics, magic_conversion: &_mock_conversion_code, }; let injected = ag.resolve(&arg).unwrap(); assert_eq!(injected, expected.ast()); } #[rstest] #[case::simple_type( "fn test(arg: MyType) {}", 0, "let arg = { use rstest::magic_conversion::*; (&&&Magic::(core::marker::PhantomData)).magic_conversion(\"value to convert\") };" )] #[case::discard_impl( "fn test(arg: impl AsRef) {}", 0, r#"let arg = "value to convert";"# )] #[case::discard_generic_type( "fn test>(arg: S) {}", 0, r#"let arg = "value to convert";"# )] #[case::reference_type( "fn test(arg: &MyType) {}", 0, "let arg = { use rstest::magic_conversion::*; (&&&Magic::<&MyType>(core::marker::PhantomData)).magic_conversion(\"value to convert\") };" )] #[case::mutable_reference_type( "fn test(arg: &mut MyType) {}", 0, "let arg = { use rstest::magic_conversion::*; (&&&Magic::<&mut MyType>(core::marker::PhantomData)).magic_conversion(\"value to convert\") };" )] #[case::generic_type_with_lifetime( "fn test<'a, T>(arg: &'a T) where T: Default {}", 0, "let arg = { use rstest::magic_conversion::*; (&&&Magic::<&'a T>(core::marker::PhantomData)).magic_conversion(\"value to convert\") };" )] #[case::type_with_generic_parameters( "fn test(arg: Option) {}", 0, "let arg = { use rstest::magic_conversion::*; (&&&Magic::>(core::marker::PhantomData)).magic_conversion(\"value to convert\") };" )] #[case::complex_type( "fn test(arg: Result, MyError>) {}", 0, "let arg = { use rstest::magic_conversion::*; (&&&Magic::, MyError>>(core::marker::PhantomData)).magic_conversion(\"value to convert\") };" )] fn generated_code_uses_phantom_data( #[case] fn_str: &str, #[case] n_arg: usize, #[case] expected: &str, ) { let function = fn_str.ast(); let arg = fn_args(&function).nth(n_arg).unwrap(); let generics = function .sig .generics .type_params() .map(|tp| &tp.ident) .cloned() .collect::>(); let mut resolver = std::collections::HashMap::new(); let expr = expr(r#""value to convert""#); resolver.insert(arg.maybe_pat().unwrap().clone(), &expr); let ag = ArgumentResolver::new(&resolver, &generics); let injected = ag.resolve(&arg).unwrap(); assert_eq!(injected, expected.ast()); } } rstest_macros-0.26.1/src/render/mod.rs000064400000000000000000000351121046102023000157770ustar 00000000000000pub mod crate_resolver; pub(crate) mod fixture; mod test; mod wrapper; use std::collections::HashMap; use syn::token::Async; use proc_macro2::{Span, TokenStream}; use syn::{parse_quote, Attribute, Expr, FnArg, Ident, ItemFn, Pat, Path, ReturnType, Stmt}; use quote::{format_ident, quote}; use crate::refident::MaybePat; use crate::utils::sanitize_ident; use crate::{ parse::{ rstest::{RsTestAttributes, RsTestInfo}, testcase::TestCase, vlist::ValueList, }, utils::attr_is, }; use crate::{ refident::MaybeIdent, resolver::{self, Resolver}, }; use wrapper::WrapByModule; pub(crate) use fixture::render as fixture; use crate::parse::arguments::TestAttr; use self::apply_arguments::ApplyArguments; use self::crate_resolver::crate_name; pub(crate) mod apply_arguments; pub(crate) mod inject; pub(crate) fn single(mut test: ItemFn, mut info: RsTestInfo) -> TokenStream { test.apply_arguments(&mut info.arguments, &mut ()); let resolver = resolver::fixtures::get(&info.arguments, info.data.fixtures()); let args = test.sig.inputs.iter().cloned().collect::>(); let attrs = std::mem::take(&mut test.attrs); let asyncness = test.sig.asyncness; single_test_case( &test.sig.ident, &test.sig.ident, &args, &attrs, &test.sig.output, asyncness, Some(&test), resolver, &info, &test.sig.generics, &None, ) } pub(crate) fn parametrize(mut test: ItemFn, info: RsTestInfo) -> TokenStream { let mut arguments_info = info.arguments.clone(); test.apply_arguments(&mut arguments_info, &mut ()); let resolver_fixtures = resolver::fixtures::get(&info.arguments, info.data.fixtures()); let rendered_cases = cases_data(&info, test.sig.ident.span()) .map(|c| { CaseDataValues::new( c.ident, c.attributes, Box::new((c.resolver, &resolver_fixtures)), c.info, ) }) .map(|case| case.render(&test, &info)) .collect(); test_group(test, rendered_cases) } type ArgumentDataResolver<'a> = Box<(&'a dyn Resolver, (Pat, Expr))>; impl ValueList { fn render( &self, test: &ItemFn, resolver: &dyn Resolver, attrs: &[syn::Attribute], info: &RsTestInfo, case_info: &Option, ) -> TokenStream { let span = test.sig.ident.span(); let test_cases = self .argument_data(resolver, info) .map(|(name, r)| { CaseDataValues::new(Ident::new(&name, span), attrs, r, case_info.clone()) }) .map(|test_case| test_case.render(test, info)); quote! { #(#test_cases)* } } fn argument_data<'a>( &'a self, resolver: &'a dyn Resolver, info: &'a RsTestInfo, ) -> impl Iterator)> + 'a { let max_len = self.values.len(); self.values.iter().enumerate().map(move |(index, value)| { let description = sanitize_ident(&value.description()); let arg = info.arguments.inner_pat(&self.arg); let arg_name = arg .maybe_ident() .expect("BUG: Here all arguments should be PatIdent types") .to_string(); let name = format!( "{}_{:0len$}_{description:.64}", arg_name, index + 1, len = max_len.display_len() ); let resolver_this = (arg.clone(), value.expr.clone()); (name, Box::new((resolver, resolver_this))) }) } } #[derive(Clone, Debug)] struct CaseInfo { description: Option, pos: usize, } impl CaseInfo { fn new(description: Option, pos: usize) -> Self { Self { description, pos } } } fn _matrix_recursive<'a>( test: &ItemFn, list_values: &'a [&'a ValueList], resolver: &dyn Resolver, attrs: &'a [syn::Attribute], info: &RsTestInfo, case_info: &Option, ) -> TokenStream { if list_values.is_empty() { return Default::default(); } let vlist = list_values[0]; let list_values = &list_values[1..]; if list_values.is_empty() { let mut attrs = attrs.to_vec(); attrs.push(parse_quote!( #[allow(non_snake_case)] )); vlist.render(test, resolver, &attrs, info, case_info) } else { let span = test.sig.ident.span(); let modules = vlist .argument_data(resolver, info) .map(move |(name, resolver)| { _matrix_recursive(test, list_values, &resolver, attrs, info, case_info) .wrap_by_mod(&Ident::new(&name, span)) }); quote! { #( #[allow(non_snake_case)] #modules )* } } } pub(crate) fn matrix(mut test: ItemFn, mut info: RsTestInfo) -> TokenStream { test.apply_arguments(&mut info.arguments, &mut ()); let span = test.sig.ident.span(); let cases = cases_data(&info, span).collect::>(); let resolver = resolver::fixtures::get(&info.arguments, info.data.fixtures()); let rendered_cases = if cases.is_empty() { let list_values = info.data.list_values().collect::>(); _matrix_recursive(&test, &list_values, &resolver, &[], &info, &None) } else { cases .into_iter() .map(|c| { let list_values = info.data.list_values().collect::>(); _matrix_recursive( &test, &list_values, &(&c.resolver, &resolver), c.attributes, &info, &c.info, ) .wrap_by_mod(&c.ident) }) .collect() }; test_group(test, rendered_cases) } fn resolve_test_attr( test_attr: Option<&TestAttr>, ) -> Option { match test_attr { Some(TestAttr::Explicit(attr)) => { Some(quote! { #attr }) } Some(TestAttr::InAttrs) => { // test attr is already in the attributes; we don't need to re-inject it None } None => { Some(quote! { #[test] }) } } } fn render_exec_call(fn_path: Path, args: &[Expr], is_async: bool) -> TokenStream { if is_async { quote! {#fn_path(#(#args),*).await} } else { quote! {#fn_path(#(#args),*)} } } fn render_test_call( fn_path: Path, args: &[Expr], timeout: Option, is_async: bool, ) -> TokenStream { let timeout = timeout.map(|x| quote! {#x}).or_else(|| { std::env::var("RSTEST_TIMEOUT") .ok() .map(|to| quote! { core::time::Duration::from_secs( (#to).parse().unwrap()) }) }); let rstest_path = crate_name(); match (timeout, is_async) { (Some(to_expr), true) => quote! { use #rstest_path::timeout::*; execute_with_timeout_async(move || #fn_path(#(#args),*), #to_expr).await }, (Some(to_expr), false) => quote! { use #rstest_path::timeout::*; execute_with_timeout_sync(move || #fn_path(#(#args),*), #to_expr) }, _ => render_exec_call(fn_path, args, is_async), } } fn generics_types_ident(generics: &syn::Generics) -> impl Iterator { generics.type_params().map(|tp| &tp.ident) } /// Render a single test case: /// /// * `name` - Test case name /// * `testfn_name` - The name of test function to call /// * `args` - The arguments of the test function /// * `attrs` - The expected test attributes /// * `output` - The expected test return type /// * `asyncness` - The `async` fn token /// * `test_impl` - If you want embed test function (should be the one called by `testfn_name`) /// * `resolver` - The resolver used to resolve injected values /// * `info` - `RsTestInfo` that's expose the requested test behavior /// * `generic_types` - The generic types used in signature /// // Ok I need some refactoring here but now that not a real issue #[allow(clippy::too_many_arguments)] fn single_test_case( name: &Ident, testfn_name: &Ident, args: &[FnArg], attrs: &[Attribute], output: &ReturnType, asyncness: Option, test_impl: Option<&ItemFn>, resolver: impl Resolver, info: &RsTestInfo, generics: &syn::Generics, case_info: &Option, ) -> TokenStream { let (attrs, trace_me): (Vec<_>, Vec<_>) = attrs.iter().cloned().partition(|a| !attr_is(a, "trace")); let mut attributes = info.attributes.clone(); if !trace_me.is_empty() { attributes.add_trace(format_ident!("trace")); } let generics_types = generics_types_ident(generics).cloned().collect::>(); let args = info .arguments .replace_fn_args_with_related_inner_pat(args.iter().cloned()) .collect::>(); let (injectable_args, ignored_args): (Vec<_>, Vec<_>) = args.iter().partition(|arg| match arg.maybe_pat() { Some(pat) => !info.arguments.is_ignore(pat), None => true, }); let test_fn_name_str = testfn_name.to_string(); let description = match case_info .as_ref() .and_then(|c| c.description.as_ref()) .map(|d| d.to_string()) { Some(s) => quote! { Some(#s) }, None => quote! { None }, }; let pos = match case_info.as_ref().map(|c| c.pos) { Some(p) => quote! { Some(#p) }, None => quote! { None }, }; let context_resolver = info .arguments .contexts() .map(|p| { (p.clone(), { let e: Expr = parse_quote! { Context::new(module_path!(), #test_fn_name_str, #description, #pos) }; e }) }) .collect::>(); let inject = inject::resolve_arguments( injectable_args.into_iter(), &(context_resolver, &resolver), &generics_types, ); let args = args .iter() .filter_map(MaybePat::maybe_pat) .cloned() .collect::>(); let trace_args = trace_arguments(args.iter(), &attributes); let is_async = asyncness.is_some(); let (attrs, timeouts): (Vec<_>, Vec<_>) = attrs.iter().cloned().partition(|a| !attr_is(a, "timeout")); let timeout = timeouts .into_iter() .last() .map(|attribute| attribute.parse_args::().unwrap()); let test_attr = resolve_test_attr(info.arguments.test_attr()); let args = args .iter() .map(|arg| (arg, info.arguments.is_by_refs(arg))) .filter_map(|(a, by_refs)| a.maybe_ident().map(|id| (id, by_refs))) .map(|(arg, by_ref)| { if by_ref { parse_quote! { &#arg } } else { parse_quote! { #arg } } }) .collect::>(); let execute = render_test_call(testfn_name.clone().into(), &args, timeout, is_async); let mut lifetimes = generics.lifetimes().peekable(); let lifetimes = if lifetimes.peek().is_some() { Some(quote! {<#(#lifetimes,)*>}) } else { None }; quote! { #(#attrs)* #test_attr #asyncness fn #name #lifetimes (#(#ignored_args,)*) #output { #test_impl #inject #trace_args #execute } } } fn trace_arguments<'a>( args: impl Iterator, attributes: &RsTestAttributes, ) -> Option { let mut statements = args .filter(|&arg| attributes.trace_me(arg)) .map(|arg| { let s: Stmt = parse_quote! { println!("{} = {:?}", stringify!(#arg), #arg); }; s }) .peekable(); if statements.peek().is_some() { Some(quote! { println!("{:-^40}", " TEST ARGUMENTS "); #(#statements)* println!("{:-^40}", " TEST START "); }) } else { None } } impl CaseDataValues<'_> { fn render(self, testfn: &ItemFn, info: &RsTestInfo) -> TokenStream { let args = testfn.sig.inputs.iter().cloned().collect::>(); let mut attrs = testfn.attrs.clone(); attrs.extend(self.attributes.iter().cloned()); let asyncness = testfn.sig.asyncness; single_test_case( &self.ident, &testfn.sig.ident, &args, &attrs, &testfn.sig.output, asyncness, None, self.resolver, info, &testfn.sig.generics, &self.info, ) } } fn test_group(mut test: ItemFn, rendered_cases: TokenStream) -> TokenStream { let fname = &test.sig.ident; test.attrs = vec![]; quote! { #[cfg(test)] #test #[cfg(test)] mod #fname { use super::*; #rendered_cases } } } trait DisplayLen { fn display_len(&self) -> usize; } impl DisplayLen for D { fn display_len(&self) -> usize { format!("{self}").len() } } fn format_case_name(case: &TestCase, index: usize, display_len: usize) -> String { let description = case .description .as_ref() .map(|d| format!("_{d}")) .unwrap_or_default(); format!("case_{index:0display_len$}{description}") } struct CaseDataValues<'a> { ident: Ident, attributes: &'a [syn::Attribute], resolver: Box, info: Option, } impl<'a> CaseDataValues<'a> { fn new( ident: Ident, attributes: &'a [syn::Attribute], resolver: Box, info: Option, ) -> Self { Self { ident, attributes, resolver, info, } } } fn cases_data(info: &RsTestInfo, name_span: Span) -> impl Iterator> { let display_len = info.data.cases().count().display_len(); info.data.cases().enumerate().map({ move |(n, case)| { let resolver_case = info .data .case_args() .cloned() .map(|arg| info.arguments.inner_pat(&arg).clone()) .zip(case.args.iter()) .collect::>(); CaseDataValues::new( Ident::new(&format_case_name(case, n + 1, display_len), name_span), case.attrs.as_slice(), Box::new(resolver_case), Some(CaseInfo::new(case.description.clone(), n)), ) } }) } rstest_macros-0.26.1/src/render/test.rs000064400000000000000000001755521046102023000162140ustar 00000000000000#![cfg(test)] use syn::{ parse::{Parse, ParseStream, Result}, parse2, parse_str, visit::Visit, ItemFn, ItemMod, LocalInit, }; use super::*; use crate::test::{assert_eq, fixture, *}; use crate::utils::*; trait SetAsync { fn set_async(&mut self, is_async: bool); } impl SetAsync for ItemFn { fn set_async(&mut self, is_async: bool) { self.sig.asyncness = if is_async { Some(parse_quote! { async }) } else { None }; } } fn trace_argument_code_string(arg_name: &str) -> String { let arg_name = ident(arg_name); let statement: Stmt = parse_quote! { println!("{} = {:?}", stringify!(#arg_name) ,#arg_name); }; statement.display_code() } mod single_test_should { use rstest_test::{assert_in, assert_not_in}; use crate::{ parse::arguments::{ArgumentsInfo, FutureArg}, test::{assert_eq, *}, }; use super::*; #[test] fn add_return_type_if_any() { let input_fn: ItemFn = "fn function(fix: String) -> Result { Ok(42) }".ast(); let result: ItemFn = single(input_fn.clone(), Default::default()).ast(); assert_eq!(result.sig.output, input_fn.sig.output); } fn extract_inner_test_function(outer: &ItemFn) -> ItemFn { let first_stmt = outer.block.stmts.get(0).unwrap(); parse_quote! { #first_stmt } } #[test] fn include_given_function() { let input_fn: ItemFn = r#" pub fn test, B>(mut s: String, v: &u32, a: &mut [i32], r: R) -> (u32, B, String, &str) where B: Borrow { let some = 42; assert_eq!(42, some); } "#.ast(); let result: ItemFn = single(input_fn.clone(), Default::default()).ast(); let inner_fn = extract_inner_test_function(&result); let inner_fn_impl: Stmt = inner_fn.block.stmts.last().cloned().unwrap(); assert_eq!(inner_fn.sig, input_fn.sig); assert_eq!(inner_fn_impl.display_code(), input_fn.block.display_code()); } #[test] fn not_remove_lifetimes() { let input_fn: ItemFn = r#" pub fn test<'a, 'b, 'c: 'a + 'b>(a: A<'a>, b: A<'b>, c: A<'c>) -> A<'c> { } "# .ast(); let result: ItemFn = single(input_fn.clone(), Default::default()).ast(); assert_eq!(3, result.sig.generics.lifetimes().count()); } #[rstest] fn not_copy_any_attributes( #[values( "#[test]", "#[very::complicated::path]", "#[test]#[should_panic]", "#[should_panic]#[test]", "#[a]#[b]#[c]" )] attributes: &str, ) { let attributes = attrs(attributes); let mut input_fn: ItemFn = r#"pub fn test(_s: String){}"#.ast(); input_fn.attrs = attributes; let result: ItemFn = single(input_fn.clone(), Default::default()).ast(); let first_stmt = result.block.stmts.get(0).unwrap(); let inner_fn: ItemFn = parse_quote! { #first_stmt }; assert!(inner_fn.attrs.is_empty()); } #[rstest] #[case::sync(false)] #[case::async_fn(true)] fn use_injected_test_attribute_to_mark_test_functions_if_any( #[case] is_async: bool, #[values( "#[test]", "#[other::test]", "#[very::complicated::path::test]", "#[prev]#[test]", "#[test]#[after]", "#[prev]#[other::test]" )] attributes: &str, ) { let attributes = attrs(attributes); let mut input_fn: ItemFn = r#"fn test(_s: String) {} "#.ast(); let mut info = RsTestInfo::default(); input_fn.set_async(is_async); input_fn.attrs = attributes.clone(); info.arguments.set_test_attr(Some(TestAttr::InAttrs)); let result: ItemFn = single(input_fn.clone(), info).ast(); assert_eq!(result.attrs, attributes); } #[test] fn use_global_await() { let input_fn: ItemFn = r#"fn test(a: i32, b:i32, c:i32) {} "#.ast(); let mut info: RsTestInfo = Default::default(); info.arguments.set_global_await(true); info.arguments.add_future(pat("a")); info.arguments.add_future(pat("b")); let item_fn: ItemFn = single(input_fn.clone(), info).ast(); assert_in!( item_fn.block.display_code(), await_argument_code_string("a") ); assert_in!( item_fn.block.display_code(), await_argument_code_string("b") ); assert_not_in!( item_fn.block.display_code(), await_argument_code_string("c") ); } #[test] fn use_selective_await() { let input_fn: ItemFn = r#"fn test(a: i32, b:i32, c:i32) {} "#.ast(); let mut info: RsTestInfo = Default::default(); info.arguments.set_future(pat("a"), FutureArg::Define); info.arguments.set_future(pat("b"), FutureArg::Await); let item_fn: ItemFn = single(input_fn.clone(), info).ast(); assert_not_in!( item_fn.block.display_code(), await_argument_code_string("a",) ); assert_in!( item_fn.block.display_code(), await_argument_code_string("b") ); assert_not_in!( item_fn.block.display_code(), await_argument_code_string("c") ); } #[test] fn use_ref_if_any() { let input_fn: ItemFn = r#"fn test(a: i32, b:i32, c:i32) {} "#.ast(); let mut info: RsTestInfo = Default::default(); info.arguments.set_by_ref(pat("a")); info.arguments.set_by_ref(pat("c")); let item_fn: ItemFn = single(input_fn.clone(), info).ast(); assert_in!( item_fn.block.stmts.last().display_code(), ref_argument_code_string("a") ); assert_not_in!( item_fn.block.stmts.last().display_code(), ref_argument_code_string("b") ); assert_in!( item_fn.block.stmts.last().display_code(), ref_argument_code_string("c") ); } #[test] fn trace_arguments_values() { let input_fn: ItemFn = r#"#[trace]fn test(s: String, a:i32) {} "#.ast(); let item_fn: ItemFn = single(input_fn.clone(), Default::default()).ast(); assert_in!( item_fn.block.display_code(), trace_argument_code_string("s") ); assert_in!( item_fn.block.display_code(), trace_argument_code_string("a") ); } #[test] fn trace_not_all_arguments_values() { let input_fn: ItemFn = r#"#[trace] fn test(a_trace: i32, b_no_trace:i32, c_no_trace:i32, d_trace:i32) {} "# .ast(); let mut attributes = RsTestAttributes::default(); attributes.add_notraces(vec![pat("b_no_trace"), pat("c_no_trace")]); let item_fn: ItemFn = single( input_fn.clone(), RsTestInfo { attributes, ..Default::default() }, ) .ast(); assert_in!( item_fn.block.display_code(), trace_argument_code_string("a_trace") ); assert_not_in!( item_fn.block.display_code(), trace_argument_code_string("b_no_trace") ); assert_not_in!( item_fn.block.display_code(), trace_argument_code_string("c_no_trace") ); assert_in!( item_fn.block.display_code(), trace_argument_code_string("d_trace") ); } #[rstest] #[case::sync("", parse_quote! { #[test] })] fn add_default_test_attribute( #[case] prefix: &str, #[case] test_attribute: Attribute, #[values( "", "#[no_one]", "#[should_panic]", "#[should_panic]#[other]", "#[a::b::c]#[should_panic]" )] attributes: &str, ) { let attributes = attrs(attributes); let mut input_fn: ItemFn = format!(r#"{} fn test(_s: String) {{}} "#, prefix).ast(); input_fn.attrs = attributes.clone(); let result: ItemFn = single(input_fn.clone(), Default::default()).ast(); let (generated_attribute, old_attributes) = result.attrs.split_last().unwrap(); assert_eq!(old_attributes, attributes.as_slice()); assert_eq!(generated_attribute, &test_attribute); } #[rstest] #[case::sync(false, false)] #[case::async_fn(true, true)] fn use_await_for_no_async_test_function(#[case] is_async: bool, #[case] use_await: bool) { let mut input_fn: ItemFn = r#"fn test(_s: String) {} "#.ast(); input_fn.set_async(is_async); if is_async { input_fn.attrs.push(parse_quote!(#[async_std::test])); } let result: ItemFn = single(input_fn.clone(), Default::default()).ast(); let last_stmt = result.block.stmts.last().unwrap(); assert_eq!(use_await, last_stmt.is_await()); } #[test] fn add_future_boilerplate_if_requested() { let item_fn: ItemFn = r#" #[async_std::test] async fn test(async_ref_u32: &u32, async_u32: u32,simple: u32) { } "# .ast(); let mut arguments = ArgumentsInfo::default(); arguments.add_future(pat("async_ref_u32")); arguments.add_future(pat("async_u32")); let info = RsTestInfo { arguments, ..Default::default() }; let result: ItemFn = single(item_fn.clone(), info).ast(); let inner_fn = extract_inner_test_function(&result); let expected = parse_str::( r#"async fn test<'_async_ref_u32>( async_ref_u32: impl core::future::Future, async_u32: impl core::future::Future, simple: u32 ) { } "#, ) .unwrap(); assert_eq!(inner_fn.sig, expected.sig); } } #[derive(Debug)] struct TestsGroup { requested_test: ItemFn, module: ItemMod, } impl Parse for TestsGroup { fn parse(input: ParseStream) -> Result { Ok(Self { requested_test: input.parse()?, module: input.parse()?, }) } } trait QueryAttrs { #[allow(dead_code)] fn has_attr(&self, attr: &syn::Path) -> bool; fn has_attr_that_ends_with(&self, attr: &syn::PathSegment) -> bool; } impl QueryAttrs for ItemFn { fn has_attr(&self, attr: &syn::Path) -> bool { self.attrs.iter().find(|a| a.path() == attr).is_some() } fn has_attr_that_ends_with(&self, name: &syn::PathSegment) -> bool { self.attrs .iter() .find(|a| attr_ends_with(a, name)) .is_some() } } /// To extract all test functions struct TestFunctions(Vec); fn is_test_fn(item_fn: &ItemFn) -> bool { item_fn.has_attr_that_ends_with(&parse_quote! { test }) } impl TestFunctions { fn is_test_fn(item_fn: &ItemFn) -> bool { is_test_fn(item_fn) } } impl<'ast> Visit<'ast> for TestFunctions { //noinspection RsTypeCheck fn visit_item_fn(&mut self, item_fn: &'ast ItemFn) { if Self::is_test_fn(item_fn) { self.0.push(item_fn.clone()) } } } trait Named { fn name(&self) -> String; } impl Named for Ident { fn name(&self) -> String { self.to_string() } } impl Named for ItemFn { fn name(&self) -> String { self.sig.ident.name() } } impl Named for ItemMod { fn name(&self) -> String { self.ident.name() } } trait Names { fn names(&self) -> Vec; } impl Names for Vec { fn names(&self) -> Vec { self.iter().map(Named::name).collect() } } trait ModuleInspector { fn get_all_tests(&self) -> Vec; fn get_tests(&self) -> Vec; fn get_modules(&self) -> Vec; } impl ModuleInspector for ItemMod { fn get_tests(&self) -> Vec { self.content .as_ref() .map(|(_, items)| { items .iter() .filter_map(|it| match it { syn::Item::Fn(item_fn) if is_test_fn(item_fn) => Some(item_fn.clone()), _ => None, }) .collect() }) .unwrap_or_default() } fn get_all_tests(&self) -> Vec { let mut f = TestFunctions(vec![]); f.visit_item_mod(&self); f.0 } fn get_modules(&self) -> Vec { self.content .as_ref() .map(|(_, items)| { items .iter() .filter_map(|it| match it { syn::Item::Mod(item_mod) => Some(item_mod.clone()), _ => None, }) .collect() }) .unwrap_or_default() } } impl ModuleInspector for TestsGroup { fn get_all_tests(&self) -> Vec { self.module.get_all_tests() } fn get_tests(&self) -> Vec { self.module.get_tests() } fn get_modules(&self) -> Vec { self.module.get_modules() } } #[derive(Default, Debug)] struct Assignments(HashMap); impl<'ast> Visit<'ast> for Assignments { //noinspection RsTypeCheck fn visit_local(&mut self, assign: &syn::Local) { match &assign { syn::Local { pat: syn::Pat::Ident(pat), init: Some(LocalInit { expr, .. }), .. } => { self.0.insert(pat.ident.to_string(), expr.as_ref().clone()); } _ => {} } } } impl Assignments { pub fn collect_assignments(item_fn: &ItemFn) -> Self { let mut collect = Self::default(); collect.visit_item_fn(item_fn); collect } } impl From for TestsGroup { fn from(tokens: TokenStream) -> Self { syn::parse2::(tokens).unwrap() } } mod cases_should { use rstest_test::{assert_in, assert_not_in}; use crate::parse::{ arguments::{ArgumentsInfo, FutureArg}, rstest::{RsTestData, RsTestItem}, }; use super::{assert_eq, *}; fn into_rstest_data(item_fn: &ItemFn) -> RsTestData { RsTestData { items: fn_args_pats(item_fn) .cloned() .map(RsTestItem::CaseArgName) .collect(), } } struct TestCaseBuilder { item_fn: ItemFn, info: RsTestInfo, } impl TestCaseBuilder { fn new(item_fn: ItemFn) -> Self { let info: RsTestInfo = into_rstest_data(&item_fn).into(); Self { item_fn, info } } fn from>(s: S) -> Self { Self::new(s.as_ref().ast()) } fn set_async(mut self, is_async: bool) -> Self { self.item_fn.set_async(is_async); // when building an async test case, let's set an implicit test attribute // so we can use the proper async runtime. if is_async { self.item_fn.attrs.push(parse_quote!(#[async_std::test])); } self } fn push_case>(mut self, case: T) -> Self { self.info.push_case(case.into()); self } fn extend>(mut self, cases: impl Iterator) -> Self { self.info.extend(cases.map(Into::into)); self } fn take(self) -> (ItemFn, RsTestInfo) { (self.item_fn, self.info) } fn add_notrace(mut self, pats: Vec) -> Self { self.info.attributes.add_notraces(pats); self } } fn one_simple_case() -> (ItemFn, RsTestInfo) { TestCaseBuilder::from(r#"fn test(mut fix: String) { println!("user code") }"#) .push_case(r#"String::from("3")"#) .take() } fn some_simple_cases(cases: i32) -> (ItemFn, RsTestInfo) { TestCaseBuilder::from(r#"fn test(mut fix: String) { println!("user code") }"#) .extend((0..cases).map(|_| r#"String::from("3")"#)) .take() } #[test] fn create_a_module_named_as_test_function() { let (item_fn, info) = TestCaseBuilder::from("fn should_be_the_module_name(mut fix: String) {}").take(); let tokens = parametrize(item_fn, info); let output = TestsGroup::from(tokens); assert_eq!(output.module.ident, "should_be_the_module_name"); } #[test] fn copy_user_function() { let (item_fn, info) = TestCaseBuilder::from( r#"fn should_be_the_module_name(mut fix: String) { println!("user code") }"#, ) .take(); let tokens = parametrize(item_fn.clone(), info); let mut output = TestsGroup::from(tokens); let test_impl: Stmt = output.requested_test.block.stmts.last().cloned().unwrap(); output.requested_test.attrs = vec![]; assert_eq!(output.requested_test.sig, item_fn.sig); assert_eq!(test_impl.display_code(), item_fn.block.display_code()); } #[test] fn should_not_copy_should_panic_attribute() { let (item_fn, info) = TestCaseBuilder::from( r#"#[should_panic] fn with_should_panic(mut fix: String) { println!("user code") }"#, ) .take(); let tokens = parametrize(item_fn.clone(), info); let output = TestsGroup::from(tokens); assert!(!format!("{:?}", output.requested_test.attrs).contains("should_panic")); } #[test] fn should_mark_test_with_given_attributes() { let (item_fn, info) = TestCaseBuilder::from(r#"#[should_panic] #[other(value)] fn test(s: String){}"#) .push_case(r#"String::from("3")"#) .take(); let tokens = parametrize(item_fn.clone(), info); let tests = TestsGroup::from(tokens).get_all_tests(); // Sanity check assert!(tests.len() > 0); for t in tests { assert_eq!(item_fn.attrs, &t.attrs[..t.attrs.len() - 1]); } } #[rstest] #[case::empty("")] #[case::some_attrs("#[a]#[b::c]#[should_panic]")] fn should_add_attributes_given_in_the_test_case( #[case] fnattrs: &str, #[values("", "#[should_panic]", "#[first]#[second(arg)]")] case_attrs: &str, ) { let given_attrs = attrs(fnattrs); let case_attrs = attrs(case_attrs); let (mut item_fn, info) = TestCaseBuilder::from(r#"fn test(v: i32){}"#) .push_case(TestCase::from("42").with_attrs(case_attrs.clone())) .take(); item_fn.attrs = given_attrs.clone(); let tokens = parametrize(item_fn, info); let tests = TestsGroup::from(tokens).get_all_tests(); let test_attrs = tests[0].attrs.split_last().unwrap().1; let l = given_attrs.len(); assert_eq!(case_attrs.as_slice(), &test_attrs[l..]); assert_eq!(given_attrs.as_slice(), &test_attrs[..l]); } #[test] fn mark_user_function_as_test() { let (item_fn, info) = TestCaseBuilder::from( r#"fn should_be_the_module_name(mut fix: String) { println!("user code") }"#, ) .take(); let tokens = parametrize(item_fn, info); let output = TestsGroup::from(tokens); assert_eq!( output.requested_test.attrs, vec![parse_quote! {#[cfg(test)]}] ); } #[test] fn mark_module_as_test() { let (item_fn, info) = TestCaseBuilder::from( r#"fn should_be_the_module_name(mut fix: String) { println!("user code") }"#, ) .take(); let tokens = parametrize(item_fn, info); let output = TestsGroup::from(tokens); assert_eq!(output.module.attrs, vec![parse_quote! {#[cfg(test)]}]); } #[test] fn add_a_test_case() { let (item_fn, info) = one_simple_case(); let tokens = parametrize(item_fn, info); let tests = TestsGroup::from(tokens).get_all_tests(); assert_eq!(1, tests.len()); assert!(&tests[0].sig.ident.to_string().starts_with("case_")) } #[test] fn add_return_type_if_any() { let (item_fn, info) = TestCaseBuilder::from("fn function(fix: String) -> Result { Ok(42) }") .push_case(r#"String::from("3")"#) .take(); let tokens = parametrize(item_fn.clone(), info); let tests = TestsGroup::from(tokens).get_all_tests(); assert_eq!(tests[0].sig.output, item_fn.sig.output); } #[test] fn not_copy_user_function() { let t_name = "test_name"; let (item_fn, info) = TestCaseBuilder::from(format!( "fn {}(fix: String) -> Result {{ Ok(42) }}", t_name )) .push_case(r#"String::from("3")"#) .take(); let tokens = parametrize(item_fn, info); let test = &TestsGroup::from(tokens).get_all_tests()[0]; let inner_functions = extract_inner_functions(&test.block); assert_eq!(0, inner_functions.filter(|f| f.sig.ident == t_name).count()); } #[test] fn starts_case_number_from_1() { let (item_fn, info) = one_simple_case(); let tokens = parametrize(item_fn.clone(), info); let tests = TestsGroup::from(tokens).get_all_tests(); assert!( &tests[0].sig.ident.to_string().starts_with("case_1"), "Should starts with case_1 but is {}", tests[0].sig.ident.to_string() ) } #[test] fn add_all_test_cases() { let (item_fn, info) = some_simple_cases(5); let tokens = parametrize(item_fn.clone(), info); let tests = TestsGroup::from(tokens).get_all_tests(); let valid_names = tests .iter() .filter(|it| it.sig.ident.to_string().starts_with("case_")); assert_eq!(5, valid_names.count()) } #[test] fn left_pad_case_number_by_zeros() { let (item_fn, info) = some_simple_cases(1000); let tokens = parametrize(item_fn.clone(), info); let tests = TestsGroup::from(tokens).get_all_tests(); let first_name = tests[0].sig.ident.to_string(); let last_name = tests[999].sig.ident.to_string(); assert!( first_name.ends_with("_0001"), "Should ends by _0001 but is {}", first_name ); assert!( last_name.ends_with("_1000"), "Should ends by _1000 but is {}", last_name ); let valid_names = tests .iter() .filter(|it| it.sig.ident.to_string().len() == first_name.len()); assert_eq!(1000, valid_names.count()) } #[test] fn use_description_if_any() { let (item_fn, mut info) = one_simple_case(); let description = "show_this_description"; if let &mut RsTestItem::TestCase(ref mut case) = &mut info.data.items[1] { case.description = Some(parse_str::(description).unwrap()); } else { panic!("Test case should be the second one"); } let tokens = parametrize(item_fn.clone(), info); let tests = TestsGroup::from(tokens).get_all_tests(); assert!(tests[0] .sig .ident .to_string() .ends_with(&format!("_{}", description))); } #[rstest] #[case::sync(false)] #[case::async_fn(true)] fn use_injected_test_attribute_to_mark_test_functions_if_any( #[case] is_async: bool, #[values( "#[test]", "#[other::test]", "#[very::complicated::path::test]", "#[prev]#[test]", "#[test]#[after]", "#[prev]#[other::test]" )] attributes: &str, ) { let attributes = attrs(attributes); let (mut item_fn, mut info) = TestCaseBuilder::from(r#"fn test(s: String){}"#) .push_case(r#"String::from("3")"#) .set_async(is_async) .take(); info.arguments.set_test_attr(Some(TestAttr::InAttrs)); item_fn.attrs = attributes.clone(); item_fn.set_async(is_async); let tokens = parametrize(item_fn.clone(), info); let test = &TestsGroup::from(tokens).get_all_tests()[0]; assert_eq!(attributes, test.attrs); } #[rstest] #[case::sync(false, parse_quote! { #[test] })] fn add_default_test_attribute( #[case] is_async: bool, #[case] test_attribute: Attribute, #[values( "", "#[no_one]", "#[should_panic]", "#[should_panic]#[other]", "#[a::b::c]#[should_panic]" )] attributes: &str, ) { let attributes = attrs(attributes); let (mut item_fn, info) = TestCaseBuilder::from( r#"fn should_be_the_module_name(mut fix: String) { println!("user code") }"#, ) .push_case("42") .set_async(is_async) .take(); item_fn.attrs = attributes.clone(); let tokens = parametrize(item_fn, info); let tests = TestsGroup::from(tokens).get_all_tests(); let (generated_attribute, old_attributes) = tests[0].attrs.split_last().unwrap(); assert_eq!(old_attributes, attributes.as_slice()); assert_eq!(generated_attribute, &test_attribute); } #[test] fn add_future_boilerplate_if_requested() { let (item_fn, mut info) = TestCaseBuilder::from( r#"async fn test(async_ref_u32: &u32, async_u32: u32,simple: u32) { }"#, ) .take(); let mut arguments = ArgumentsInfo::default(); arguments.add_future(pat("async_ref_u32")); arguments.add_future(pat("async_u32")); info.arguments = arguments; let tokens = parametrize(item_fn.clone(), info); let test_function = TestsGroup::from(tokens).requested_test; let expected = parse_str::( r#"async fn test<'_async_ref_u32>( async_ref_u32: impl core::future::Future, async_u32: impl core::future::Future, simple: u32 ) { } "#, ) .unwrap(); assert_eq!(test_function.sig, expected.sig); } #[rstest] #[case::sync(false, false)] #[case::async_fn(true, true)] fn use_await_for_async_test_function(#[case] is_async: bool, #[case] use_await: bool) { let (item_fn, info) = TestCaseBuilder::from(r#"fn test(mut fix: String) { println!("user code") }"#) .set_async(is_async) .push_case(r#"String::from("3")"#) .take(); let tokens = parametrize(item_fn, info); let tests = TestsGroup::from(tokens).get_all_tests(); let last_stmt = tests[0].block.stmts.last().unwrap(); assert_eq!(use_await, last_stmt.is_await()); } #[test] fn trace_arguments_value() { let (item_fn, info) = TestCaseBuilder::from(r#"#[trace] fn test(a_trace_me: i32, b_trace_me: i32) {}"#) .push_case(TestCase::from_iter(vec!["1", "2"])) .push_case(TestCase::from_iter(vec!["3", "4"])) .take(); let tokens = parametrize(item_fn, info); let tests = TestsGroup::from(tokens).get_all_tests(); assert!(tests.len() > 0); for test in tests { for name in &["a_trace_me", "b_trace_me"] { assert_in!(test.block.display_code(), trace_argument_code_string(name)); } } } #[test] fn trace_just_some_arguments_value() { let (item_fn, info) = TestCaseBuilder::from(r#"#[trace] fn test(a_trace_me: i32, b_no_trace_me: i32, c_no_trace_me: i32, d_trace_me: i32) {}"#) .push_case(TestCase::from_iter(vec!["1", "2", "1", "2"])) .push_case(TestCase::from_iter(vec!["3", "4", "3", "4"])) .add_notrace(to_pats!(["b_no_trace_me", "c_no_trace_me"])) .take(); let tokens = parametrize(item_fn, info); let tests = TestsGroup::from(tokens).get_all_tests(); assert!(tests.len() > 0); for test in tests { for should_be_present in &["a_trace_me", "d_trace_me"] { assert_in!( test.block.display_code(), trace_argument_code_string(should_be_present) ); } for should_not_be_present in &["b_trace_me", "c_trace_me"] { assert_not_in!( test.block.display_code(), trace_argument_code_string(should_not_be_present) ); } } } #[test] fn trace_just_one_case() { let (item_fn, info) = TestCaseBuilder::from(r#"fn test(a_no_trace_me: i32, b_trace_me: i32) {}"#) .push_case(TestCase::from_iter(vec!["1", "2"])) .push_case(TestCase::from_iter(vec!["3", "4"]).with_attrs(attrs("#[trace]"))) .add_notrace(to_pats!(["a_no_trace_me"])) .take(); let tokens = parametrize(item_fn, info); let tests = TestsGroup::from(tokens).get_all_tests(); assert_not_in!( tests[0].block.display_code(), trace_argument_code_string("b_trace_me") ); assert_in!( tests[1].block.display_code(), trace_argument_code_string("b_trace_me") ); assert_not_in!( tests[1].block.display_code(), trace_argument_code_string("a_no_trace_me") ); } #[test] fn use_global_await() { let (item_fn, mut info) = TestCaseBuilder::from(r#"fn test(a: i32, b:i32, c:i32) {}"#) .push_case(TestCase::from_iter(vec!["1", "2", "3"])) .push_case(TestCase::from_iter(vec!["1", "2", "3"])) .take(); info.arguments.set_global_await(true); info.arguments.add_future(pat("a")); info.arguments.add_future(pat("b")); let tokens = parametrize(item_fn, info); let tests = TestsGroup::from(tokens); let code = tests.requested_test.block.display_code(); assert_in!(code, await_argument_code_string("a")); assert_in!(code, await_argument_code_string("b")); assert_not_in!(code, await_argument_code_string("c")); } #[test] fn use_selective_await() { let (item_fn, mut info) = TestCaseBuilder::from(r#"fn test(a: i32, b:i32, c:i32) {}"#) .push_case(TestCase::from_iter(vec!["1", "2", "3"])) .push_case(TestCase::from_iter(vec!["1", "2", "3"])) .take(); info.arguments.set_future(pat("a"), FutureArg::Define); info.arguments.set_future(pat("b"), FutureArg::Await); let tokens = parametrize(item_fn, info); let tests = TestsGroup::from(tokens); let code = tests.requested_test.block.display_code(); assert_not_in!(code, await_argument_code_string("a")); assert_in!(code, await_argument_code_string("b")); assert_not_in!(code, await_argument_code_string("c")); } #[test] fn render_context() { let (item_fn, mut info) = TestCaseBuilder::from(r#"fn test_with_context(ctx: Context, a: u32) {}"#) .push_case(TestCase { args: vec![expr("1")], attrs: Default::default(), description: Some(ident("my_description")), }) .push_case(TestCase { args: vec![expr("2")], attrs: Default::default(), description: Some(ident("other_description")), }) .take(); info.arguments.add_context(pat("ctx")); let tokens = parametrize(item_fn, info); let tests = TestsGroup::from(tokens); fn code(tests: &TestsGroup, id: usize) -> String { tests.module.get_all_tests()[id].block.display_code() } assert_in!( code(&tests, 0), r#"let ctx = Context::new(module_path!(), "test_with_context", Some("my_description"), Some(0usize));"# .ast::() .display_code() ); assert_in!( code(&tests, 1), r#"let ctx = Context::new(module_path!(), "test_with_context", Some("other_description"), Some(1usize));"# .ast::() .display_code() ); } } mod matrix_cases_should { use rstest_test::{assert_in, assert_not_in}; use crate::parse::{ arguments::{ArgumentsInfo, FutureArg}, rstest::RsTestData, }; /// Should test matrix tests render without take in account MatrixInfo to RsTestInfo /// transformation use super::{assert_eq, *}; fn into_rstest_data(item_fn: &ItemFn) -> RsTestData { RsTestData { items: fn_args_pats(item_fn) .cloned() .map(|it| { ValueList { arg: it, values: vec![], } .into() }) .collect(), } } #[test] fn create_a_module_named_as_test_function() { let item_fn = "fn should_be_the_module_name(mut fix: String) {}".ast(); let data = into_rstest_data(&item_fn); let tokens = matrix(item_fn.clone(), data.into()); let output = TestsGroup::from(tokens); assert_eq!(output.module.ident, "should_be_the_module_name"); } #[test] fn copy_user_function() { let item_fn = r#"fn should_be_the_module_name(mut fix: String) { println!("user code") }"#.ast(); let data = into_rstest_data(&item_fn); let tokens = matrix(item_fn.clone(), data.into()); let mut output = TestsGroup::from(tokens); let test_impl: Stmt = output.requested_test.block.stmts.last().cloned().unwrap(); output.requested_test.attrs = vec![]; assert_eq!(output.requested_test.sig, item_fn.sig); assert_eq!(test_impl.display_code(), item_fn.block.display_code()); } #[test] fn not_copy_user_function() { let t_name = "test_name"; let item_fn: ItemFn = format!( "fn {}(fix: String) -> Result {{ Ok(42) }}", t_name ) .ast(); let info = RsTestInfo { data: RsTestData { items: vec![values_list("fix", &["1"]).into()].into(), }, ..Default::default() }; let tokens = matrix(item_fn, info); let test = &TestsGroup::from(tokens).get_all_tests()[0]; let inner_functions = extract_inner_functions(&test.block); assert_eq!(0, inner_functions.filter(|f| f.sig.ident == t_name).count()); } #[test] fn not_copy_should_panic_attribute() { let item_fn = r#"#[should_panic] fn with_should_panic(mut fix: String) { println!("user code") }"# .ast(); let info = RsTestInfo { data: RsTestData { items: vec![values_list("fix", &["1"]).into()].into(), }, ..Default::default() }; let tokens = matrix(item_fn, info); let output = TestsGroup::from(tokens); assert!(!format!("{:?}", output.requested_test.attrs).contains("should_panic")); } #[test] fn should_mark_test_with_given_attributes() { let item_fn: ItemFn = r#"#[should_panic] #[other(value)] fn test(_s: String){}"#.ast(); let info = RsTestInfo { data: RsTestData { items: vec![values_list("fix", &["1"]).into()].into(), }, ..Default::default() }; let tokens = matrix(item_fn.clone(), info); let tests = TestsGroup::from(tokens).get_all_tests(); // Sanity check assert!(tests.len() > 0); for t in tests { let end = t.attrs.len() - 2; assert_eq!(item_fn.attrs, &t.attrs[0..end]); } } #[test] fn add_return_type_if_any() { let item_fn: ItemFn = "fn function(fix: String) -> Result { Ok(42) }".ast(); let info = RsTestInfo { data: RsTestData { items: vec![values_list("fix", &["1", "2", "3"]).into()].into(), }, ..Default::default() }; let tokens = matrix(item_fn.clone(), info); let tests = TestsGroup::from(tokens).get_tests(); assert_eq!(tests[0].sig.output, item_fn.sig.output); assert_eq!(tests[1].sig.output, item_fn.sig.output); assert_eq!(tests[2].sig.output, item_fn.sig.output); } #[test] fn mark_user_function_as_test() { let item_fn = r#"fn should_be_the_module_name(mut fix: String) { println!("user code") }"#.ast(); let data = into_rstest_data(&item_fn); let tokens = matrix(item_fn.clone(), data.into()); let output = TestsGroup::from(tokens); let expected = parse2::(quote! { #[cfg(test)] fn some() {} }) .unwrap() .attrs; assert_eq!(expected, output.requested_test.attrs); } #[test] fn mark_module_as_test() { let item_fn = r#"fn should_be_the_module_name(mut fix: String) { println!("user code") }"#.ast(); let data = into_rstest_data(&item_fn); let tokens = matrix(item_fn.clone(), data.into()); let output = TestsGroup::from(tokens); let expected = parse2::(quote! { #[cfg(test)] mod some {} }) .unwrap() .attrs; assert_eq!(expected, output.module.attrs); } #[test] fn with_just_one_arg() { let arg_name = "fix"; let info = RsTestInfo { data: RsTestData { items: vec![values_list(arg_name, &["1", "2", "3"]).into()].into(), }, ..Default::default() }; let item_fn = format!(r#"fn test({}: u32) {{ println!("user code") }}"#, arg_name).ast(); let tokens = matrix(item_fn, info); let tests = TestsGroup::from(tokens).get_tests(); assert_eq!(3, tests.len()); assert!(&tests[0].sig.ident.to_string().starts_with("fix_")) } #[rstest] #[case::sync(false)] #[case::async_fn(true)] fn use_injected_test_attribute_to_mark_test_functions_if_any( #[case] is_async: bool, #[values( "#[test]", "#[other::test]", "#[very::complicated::path::test]", "#[prev]#[test]", "#[test]#[after]", "#[prev]#[other::test]" )] attributes: &str, ) { let attributes = attrs(attributes); let filter = attrs("#[allow(non_snake_case)]"); let data = RsTestData { items: vec![values_list("v", &["1", "2", "3"]).into()].into(), }; let mut item_fn: ItemFn = r#"fn test(v: u32) {{ println!("user code") }}"#.ast(); item_fn.set_async(is_async); item_fn.attrs = attributes.clone(); let mut info : RsTestInfo = data.into(); info.arguments.set_test_attr(Some(TestAttr::InAttrs)); let tokens = matrix(item_fn, info); let tests = TestsGroup::from(tokens).get_all_tests(); // Sanity check assert!(tests.len() > 0); for test in tests { let filtered: Vec<_> = test .attrs .into_iter() .filter(|a| !filter.contains(a)) .collect(); assert_eq!(attributes, filtered); } } #[rstest] #[case::sync(false, parse_quote! { #[test] })] fn add_default_test_attribute( #[case] is_async: bool, #[case] test_attribute: Attribute, #[values( "", "#[no_one]", "#[should_panic]", "#[should_panic]#[other]", "#[a::b::c]#[should_panic]" )] attributes: &str, ) { let attributes = attrs(attributes); let data = RsTestData { items: vec![values_list("v", &["1", "2", "3"]).into()].into(), }; let mut item_fn: ItemFn = r#"fn test(v: u32) {{ println!("user code") }}"#.ast(); item_fn.set_async(is_async); item_fn.attrs = attributes.clone(); let tokens = matrix(item_fn, data.into()); let tests = TestsGroup::from(tokens).get_all_tests(); // Sanity check assert!(tests.len() > 0); for test in tests { assert_eq!(&test.attrs[..test.attrs.len() - 2], attributes.as_slice()); assert_eq!(test.attrs[test.attrs.len() - 1], test_attribute); } } #[test] fn add_future_boilerplate_if_requested() { let item_fn = r#"async fn test(async_ref_u32: &u32, async_u32: u32,simple: u32) { }"#.ast(); let mut arguments = ArgumentsInfo::default(); arguments.add_future(pat("async_ref_u32")); arguments.add_future(pat("async_u32")); let info = RsTestInfo { arguments, ..Default::default() }; let tokens = matrix(item_fn, info); let test_function = TestsGroup::from(tokens).requested_test; let expected = parse_str::( r#"async fn test<'_async_ref_u32>( async_ref_u32: impl core::future::Future, async_u32: impl core::future::Future, simple: u32 ) { } "#, ) .unwrap(); assert_eq!(test_function.sig, expected.sig); } #[rstest] fn add_allow_non_snake_case( #[values( "", "#[no_one]", "#[should_panic]", "#[should_panic]#[other]", "#[a::b::c]#[should_panic]" )] attributes: &str, ) { let attributes = attrs(attributes); let non_snake_case = &attrs("#[allow(non_snake_case)]")[0]; let data = RsTestData { items: vec![values_list("v", &["1", "2", "3"]).into()].into(), }; let mut item_fn: ItemFn = r#"fn test(v: u32) {{ println!("user code") }}"#.ast(); item_fn.attrs = attributes.clone(); let tokens = matrix(item_fn, data.into()); let tests = TestsGroup::from(tokens).get_all_tests(); // Sanity check assert!(tests.len() > 0); for test in tests { assert_eq!(&test.attrs[test.attrs.len() - 2], non_snake_case); assert_eq!(&test.attrs[..test.attrs.len() - 2], attributes.as_slice()); } } #[rstest] #[case::sync(false, false)] #[case::async_fn(true, true)] fn use_await_for_async_test_function(#[case] is_async: bool, #[case] use_await: bool) { let data = RsTestData { items: vec![values_list("v", &["1", "2", "3"]).into()].into(), }; let mut item_fn: ItemFn = r#"fn test(v: u32) {{ println!("user code") }}"#.ast(); item_fn.set_async(is_async); if is_async { item_fn.attrs.push(parse_quote!(#[async_std::test])); } let tokens = matrix(item_fn, data.into()); let tests = TestsGroup::from(tokens).get_all_tests(); // Sanity check assert!(tests.len() > 0); for test in tests { let last_stmt = test.block.stmts.last().unwrap(); assert_eq!(use_await, last_stmt.is_await()); } } #[test] fn trace_arguments_value() { let data = RsTestData { items: vec![ values_list("a_trace_me", &["1", "2"]).into(), values_list("b_trace_me", &["3", "4"]).into(), ] .into(), }; let item_fn: ItemFn = r#"#[trace] fn test(a_trace_me: u32, b_trace_me: u32) {}"#.ast(); let tokens = matrix(item_fn, data.into()); let tests = TestsGroup::from(tokens).get_all_tests(); assert!(tests.len() > 0); for test in tests { for name in &["a_trace_me", "b_trace_me"] { assert_in!(test.block.display_code(), trace_argument_code_string(name)); } } } #[test] fn trace_just_some_arguments_value() { let data = RsTestData { items: vec![ values_list("a_trace_me", &["1", "2"]).into(), values_list("b_no_trace_me", &["3", "4"]).into(), values_list("c_no_trace_me", &["5", "6"]).into(), values_list("d_trace_me", &["7", "8"]).into(), ] .into(), }; let mut attributes: RsTestAttributes = Default::default(); attributes.add_notraces(vec![pat("b_no_trace_me"), pat("c_no_trace_me")]); let item_fn: ItemFn = r#"#[trace] fn test(a_trace_me: u32, b_no_trace_me: u32, c_no_trace_me: u32, d_trace_me: u32) {}"#.ast(); let tokens = matrix( item_fn, RsTestInfo { data, attributes, ..Default::default() }, ); let tests = TestsGroup::from(tokens).get_all_tests(); assert!(tests.len() > 0); for test in tests { for should_be_present in &["a_trace_me", "d_trace_me"] { assert_in!( test.block.display_code(), trace_argument_code_string(should_be_present) ); } for should_not_be_present in &["b_no_trace_me", "c_no_trace_me"] { assert_not_in!( test.block.display_code(), trace_argument_code_string(should_not_be_present) ); } } } #[test] fn use_global_await() { let item_fn: ItemFn = r#"fn test(a: i32, b:i32, c:i32) {}"#.ast(); let data = RsTestData { items: vec![ values_list("a", &["1"]).into(), values_list("b", &["2"]).into(), values_list("c", &["3"]).into(), ] .into(), }; let mut info = RsTestInfo { data, attributes: Default::default(), arguments: Default::default(), }; info.arguments.set_global_await(true); info.arguments.add_future(pat("a")); info.arguments.add_future(pat("b")); let tokens = matrix(item_fn, info); let tests = TestsGroup::from(tokens); let code = tests.requested_test.block.display_code(); assert_in!(code, await_argument_code_string("a")); assert_in!(code, await_argument_code_string("b")); assert_not_in!(code, await_argument_code_string("c")); } #[test] fn use_selective_await() { let item_fn: ItemFn = r#"fn test(a: i32, b:i32, c:i32) {}"#.ast(); let data = RsTestData { items: vec![ values_list("a", &["1"]).into(), values_list("b", &["2"]).into(), values_list("c", &["3"]).into(), ] .into(), }; let mut info = RsTestInfo { data, attributes: Default::default(), arguments: Default::default(), }; info.arguments.set_future(pat("a"), FutureArg::Define); info.arguments.set_future(pat("b"), FutureArg::Await); let tokens = matrix(item_fn, info); let tests = TestsGroup::from(tokens); let code = tests.requested_test.block.display_code(); assert_not_in!(code, await_argument_code_string("a")); assert_in!(code, await_argument_code_string("b")); assert_not_in!(code, await_argument_code_string("c")); } mod two_args_should { /// Should test matrix tests render without take in account MatrixInfo to RsTestInfo /// transformation use super::{assert_eq, *}; fn fixture<'a>() -> (Vec<&'a str>, ItemFn, RsTestInfo) { let names = vec!["first", "second"]; ( names.clone(), format!( r#"fn test({}: u32, {}: u32) {{ println!("user code") }}"#, names[0], names[1] ) .ast(), RsTestInfo { data: RsTestData { items: vec![ values_list(names[0], &["1", "2", "3"]).into(), values_list(names[1], &["1", "2"]).into(), ], }, ..Default::default() }, ) } #[test] fn contain_a_module_for_each_first_arg() { let (names, item_fn, info) = fixture(); let tokens = matrix(item_fn, info); let modules = TestsGroup::from(tokens).module.get_modules().names(); let expected = (1..=3) .map(|i| format!("{}_{}", names[0], i)) .collect::>(); assert_eq!(expected.len(), modules.len()); for (e, m) in expected.into_iter().zip(modules.into_iter()) { assert_in!(m, e); } } #[test] fn annotate_modules_with_allow_non_snake_name() { let (_, item_fn, info) = fixture(); let non_snake_case = &attrs("#[allow(non_snake_case)]")[0]; let tokens = matrix(item_fn, info); let modules = TestsGroup::from(tokens).module.get_modules(); for module in modules { assert!(module.attrs.contains(&non_snake_case)); } } #[test] fn create_all_tests() { let (_, item_fn, info) = fixture(); let tokens = matrix(item_fn, info); let tests = TestsGroup::from(tokens).module.get_all_tests().names(); assert_eq!(6, tests.len()); } #[test] fn create_all_modules_with_the_same_functions() { let (_, item_fn, info) = fixture(); let tokens = matrix(item_fn, info); let tests = TestsGroup::from(tokens) .module .get_modules() .into_iter() .map(|m| m.get_tests().names()) .collect::>(); assert_eq!(tests[0], tests[1]); assert_eq!(tests[1], tests[2]); } #[test] fn test_name_should_contain_argument_name() { let (names, item_fn, info) = fixture(); let tokens = matrix(item_fn, info); let tests = TestsGroup::from(tokens).module.get_modules()[0] .get_tests() .names(); let expected = (1..=2) .map(|i| format!("{}_{}", names[1], i)) .collect::>(); assert_eq!(expected.len(), tests.len()); for (e, m) in expected.into_iter().zip(tests.into_iter()) { assert_in!(m, e); } } } #[test] fn three_args_should_create_all_function_4_mods_at_the_first_level_and_3_at_the_second() { let (first, second, third) = ("first", "second", "third"); let info = RsTestInfo { data: RsTestData { items: vec![ values_list(first, &["1", "2", "3", "4"]).into(), values_list(second, &["1", "2", "3"]).into(), values_list(third, &["1", "2"]).into(), ], }, ..Default::default() }; let item_fn = format!( r#"fn test({}: u32, {}: u32, {}: u32) {{ println!("user code") }}"#, first, second, third ) .ast(); let tokens = matrix(item_fn, info); let tg = TestsGroup::from(tokens); assert_eq!(24, tg.module.get_all_tests().len()); assert_eq!(4, tg.module.get_modules().len()); assert_eq!(3, tg.module.get_modules()[0].get_modules().len()); assert_eq!(3, tg.module.get_modules()[3].get_modules().len()); assert_eq!( 2, tg.module.get_modules()[0].get_modules()[0] .get_tests() .len() ); assert_eq!( 2, tg.module.get_modules()[3].get_modules()[1] .get_tests() .len() ); } #[test] fn pad_case_index() { let item_fn: ItemFn = r#"fn test(first: u32, second: u32, third: u32) { println!("user code") }"#.ast(); let values = (1..=100).map(|i| i.to_string()).collect::>(); let info = RsTestInfo { data: RsTestData { items: vec![ values_list("first", values.as_ref()).into(), values_list("second", values[..10].as_ref()).into(), values_list("third", values[..2].as_ref()).into(), ], }, ..Default::default() }; let tokens = matrix(item_fn.clone(), info); let tg = TestsGroup::from(tokens); let mods = tg.get_modules().names(); assert_in!(mods[0], "first_001"); assert_in!(mods[99], "first_100"); let mods = tg.get_modules()[0].get_modules().names(); assert_in!(mods[0], "second_01"); assert_in!(mods[9], "second_10"); let functions = tg.get_modules()[0].get_modules()[1].get_tests().names(); assert_in!(functions[0], "third_1"); assert_in!(functions[1], "third_2"); } #[test] fn render_context() { let item_fn: ItemFn = r#"fn matrix_with_context(ctx: Context, first: u32, second: u32) { println!("user code") }"#.ast(); let values = vec!["1".to_string(), "2".to_string()]; let mut info = RsTestInfo { data: RsTestData { items: vec![ values_list("first", values.as_ref()).into(), values_list("second", values.as_ref()).into(), ], }, ..Default::default() }; info.arguments.add_context(pat("ctx")); let tokens = matrix(item_fn.clone(), info); let tests = TestsGroup::from(tokens); fn code(tests: &TestsGroup, id: usize) -> String { tests.module.get_all_tests()[id].block.display_code() } for t in 0..tests.module.get_all_tests().len() { assert_in!( code(&tests, t), r#"let ctx = Context::new(module_path!(), "matrix_with_context", None, None);"# .ast::() .display_code() ); } } } mod complete_should { use crate::parse::rstest::RsTestData; use super::{assert_eq, *}; fn rendered_case(fn_name: &str) -> TestsGroup { let item_fn: ItemFn = format!( r#" #[first] #[second(arg)] fn {}( fix: u32, a: f64, b: f32, x: i32, y: i32) {{}}"#, fn_name ) .ast(); let data = RsTestData { items: vec![ fixture("fix", &["2"]).into(), ident("a").into(), ident("b").into(), vec!["1f64", "2f32"] .into_iter() .collect::() .into(), TestCase { description: Some(ident("description")), ..vec!["3f64", "4f32"].into_iter().collect::() } .with_attrs(attrs("#[third]#[forth(other)]")) .into(), values_list("x", &["12", "-2"]).into(), values_list("y", &["-3", "42"]).into(), ], }; matrix(item_fn.clone(), data.into()).into() } fn test_case() -> TestsGroup { rendered_case("test_function") } #[test] fn use_function_name_as_outer_module() { let rendered = rendered_case("should_be_the_outer_module_name"); assert_eq!(rendered.module.ident, "should_be_the_outer_module_name") } #[test] fn have_one_module_for_each_parametrized_case() { let rendered = test_case(); assert_eq!( vec!["case_1", "case_2_description"], rendered .get_modules() .iter() .map(|m| m.ident.to_string()) .collect::>() ); } #[test] fn implement_exactly_8_tests() { let rendered = test_case(); assert_eq!(8, rendered.get_all_tests().len()); } #[test] fn implement_exactly_4_tests_in_each_module() { let modules = test_case().module.get_modules(); assert_eq!(4, modules[0].get_all_tests().len()); assert_eq!(4, modules[1].get_all_tests().len()); } #[test] fn assign_same_case_value_for_each_test() { let modules = test_case().module.get_modules(); for f in modules[0].get_all_tests() { let assignments = Assignments::collect_assignments(&f); assert_eq!(assignments.0["a"], expr("1f64")); assert_eq!(assignments.0["b"], expr("2f32")); } for f in modules[1].get_all_tests() { let assignments = Assignments::collect_assignments(&f); assert_eq!(assignments.0["a"], expr("3f64")); assert_eq!(assignments.0["b"], expr("4f32")); } } #[test] fn assign_all_case_combination_in_tests() { let modules = test_case().module.get_modules(); let cases = vec![("12", "-3"), ("12", "42"), ("-2", "-3"), ("-2", "42")]; for module in modules { for ((x, y), f) in cases.iter().zip(module.get_all_tests().iter()) { let assignments = Assignments::collect_assignments(f); assert_eq!(assignments.0["x"], expr(x)); assert_eq!(assignments.0["y"], expr(y)); } } } #[test] fn mark_test_with_given_attributes() { let modules = test_case().module.get_modules(); let attrs = attrs("#[first]#[second(arg)]"); for f in modules[0].get_all_tests() { let end = f.attrs.len() - 2; assert_eq!(attrs, &f.attrs[..end]); } for f in modules[1].get_all_tests() { assert_eq!(attrs, &f.attrs[..2]); } } #[test] fn should_add_attributes_given_in_the_test_case() { let modules = test_case().module.get_modules(); let attrs = attrs("#[third]#[forth(other)]"); for f in modules[1].get_all_tests() { assert_eq!(attrs, &f.attrs[2..4]); } } } mod test_attribute_should { use super::*; use crate::test::assert_eq; enum TestAttrStyle { Explicit, Implicit, Omitted, } fn make_test_fn(is_async: bool, attr_style: TestAttrStyle) -> (ItemFn, RsTestInfo) { use std::fmt::Write as _; let mut info = RsTestInfo::default(); let mut out = String::from("#[rstest]\n"); match attr_style { TestAttrStyle::Explicit => { info.arguments.set_test_attr(Some(TestAttr::Explicit(attr("#[my_explicit_test_attr]").into()))); } TestAttrStyle::Implicit => { info.arguments.set_test_attr(Some(TestAttr::InAttrs)); writeln!(&mut out, "#[implicit::test::runner::test]").unwrap(); } TestAttrStyle::Omitted => { info.arguments.set_test_attr(None); } }; if is_async { out.push_str("async "); } out.push_str("fn test_attr_test() {}"); (out.ast(), info) } /// Check if itemfns are equivalent: /// /// - attributes don't have to match /// - generic tokens are not checked, but generic params must match /// - blocks are not checked (they have been munged) fn itemfn_near_match(expect: &ItemFn, result: &ItemFn) { assert_eq!(expect.vis, result.vis); assert_eq!(expect.sig.constness, result.sig.constness); assert_eq!(expect.sig.asyncness, result.sig.asyncness); assert_eq!(expect.sig.unsafety, result.sig.unsafety); assert_eq!(expect.sig.abi, result.sig.abi); assert_eq!(expect.sig.fn_token, result.sig.fn_token); assert_eq!(expect.sig.ident, result.sig.ident); assert_eq!(expect.sig.generics.params, result.sig.generics.params); assert_eq!( expect.sig.generics.where_clause, result.sig.generics.where_clause ); assert_eq!(expect.sig.paren_token, result.sig.paren_token); assert_eq!(expect.sig.inputs, result.sig.inputs); assert_eq!(expect.sig.variadic, result.sig.variadic); assert_eq!(expect.sig.output, result.sig.output); } #[test] fn use_explicit_test_attr_when_sync() { let (input, info) = make_test_fn(false, TestAttrStyle::Explicit); let result = single(input.clone(), info).ast::(); assert!(result .attrs .iter() .any(|attr| attr_is(attr, "my_explicit_test_attr"))); itemfn_near_match(&input, &result); } #[test] fn use_implicit_test_attr_when_sync() { let (input, info) = make_test_fn(false, TestAttrStyle::Implicit); let result = single(input.clone(), info).ast::(); assert_eq!( result .attrs .iter() .find(|attr| attr_starts_with(attr, &parse_quote!(implicit))), Some(&parse_quote!(#[implicit::test::runner::test])) ); itemfn_near_match(&input, &result); } #[test] fn use_explicit_test_attr_when_async() { let (input, info) = make_test_fn(true, TestAttrStyle::Explicit); let result = single(input.clone(), info).ast::(); assert!(result .attrs .iter() .any(|attr| attr_is(attr, "my_explicit_test_attr"))); itemfn_near_match(&input, &result); } #[test] fn use_implicit_test_attr_when_async() { let (input, info) = make_test_fn(true, TestAttrStyle::Implicit); let result = single(input.clone(), info).ast::(); assert_eq!( result .attrs .iter() .find(|attr| attr_starts_with(attr, &parse_quote!(implicit))), Some(&parse_quote!(#[implicit::test::runner::test])) ); itemfn_near_match(&input, &result); } #[test] fn fallback_for_synchronous_test_when_missing() { let (input, info) = make_test_fn(false, TestAttrStyle::Omitted); let result = single(input.clone(), info).ast::(); assert!(result.attrs.iter().any(|attr| attr_is(attr, "test"))); itemfn_near_match(&input, &result); } } rstest_macros-0.26.1/src/render/wrapper.rs000064400000000000000000000006221046102023000166760ustar 00000000000000use proc_macro2::TokenStream; use quote::{quote, ToTokens}; use syn::Ident; pub(crate) trait WrapByModule { fn wrap_by_mod(&self, mod_name: &Ident) -> TokenStream; } impl WrapByModule for T { fn wrap_by_mod(&self, mod_name: &Ident) -> TokenStream { quote! { mod #mod_name { use super::*; #self } } } } rstest_macros-0.26.1/src/resolver.rs000064400000000000000000000155501046102023000156060ustar 00000000000000/// Define `Resolver` trait and implement it on some hashmaps and also define the `Resolver` tuple /// composition. Provide also some utility functions related to how to create a `Resolver` and /// resolving render. /// use std::borrow::Cow; use std::collections::HashMap; use syn::{parse_quote, Expr, Pat}; use crate::parse::Fixture; pub(crate) mod fixtures { use quote::format_ident; use crate::parse::arguments::ArgumentsInfo; use super::*; pub(crate) fn get<'a>( arguments: &ArgumentsInfo, fixtures: impl Iterator, ) -> impl Resolver + 'a { fixtures .map(|f| { ( arguments.inner_pat(&f.arg).clone(), extract_resolve_expression(f), ) }) .collect::>() } fn extract_resolve_expression(fixture: &Fixture) -> syn::Expr { let resolve = fixture.resolve.clone(); let positional = &fixture.positional.0; let f_name = match positional.len() { 0 => format_ident!("default"), l => format_ident!("partial_{}", l), }; parse_quote! { #resolve::#f_name(#(#positional), *) } } #[cfg(test)] mod should { use super::*; use crate::test::{assert_eq, *}; #[rstest] #[case(&[], "default()")] #[case(&["my_expression"], "partial_1(my_expression)")] #[case(&["first", "other"], "partial_2(first, other)")] fn resolve_by_use_the_given_name( #[case] args: &[&str], #[case] expected: &str, #[values(None, Some("minnie"), Some("__destruct_1"))] inner_pat: Option<&str>, ) { let data = vec![fixture("pippo", args)]; let mut arguments: ArgumentsInfo = Default::default(); let mut request = pat("pippo"); if let Some(inner) = inner_pat { arguments.set_inner_pat(pat("pippo"), pat(inner)); request = pat(inner); } let resolver = get(&arguments, data.iter()); let resolved = resolver.resolve(&request).unwrap().into_owned(); assert_eq!(resolved, format!("pippo::{}", expected).ast()); } #[rstest] #[case(&[], "default()")] #[case(&["my_expression"], "partial_1(my_expression)")] #[case(&["first", "other"], "partial_2(first, other)")] fn resolve_by_use_the_resolve_field( #[case] args: &[&str], #[case] expected: &str, #[values("pluto", "minnie::pluto")] resolver_path: &str, #[values(None, Some("minnie"), Some("__destruct_1"))] inner_pat: Option<&str>, ) { let data = vec![fixture("pippo", args).with_resolve(resolver_path)]; let mut arguments: ArgumentsInfo = Default::default(); let mut request = pat("pippo"); if let Some(inner) = inner_pat { arguments.set_inner_pat(pat("pippo"), pat(inner)); request = pat(inner); } let resolver = get(&arguments, data.iter()); let resolved = resolver.resolve(&request).unwrap().into_owned(); assert_eq!(resolved, format!("{}::{}", resolver_path, expected).ast()); } } } pub(crate) mod values { use super::*; use crate::parse::fixture::ArgumentValue; pub(crate) fn get<'a>(values: impl Iterator) -> impl Resolver + 'a { values .map(|av| (av.arg.clone(), &av.expr)) .collect::>() } #[cfg(test)] mod should { use super::*; use crate::test::{assert_eq, *}; #[test] fn resolve_by_use_the_given_name() { let data = vec![ arg_value("pippo", "42"), arg_value("donaldduck", "vec![1,2]"), ]; let resolver = get(data.iter()); assert_eq!( resolver.resolve(&pat("pippo")).unwrap().into_owned(), "42".ast() ); assert_eq!( resolver.resolve(&pat("donaldduck")).unwrap().into_owned(), "vec![1,2]".ast() ); } } } /// A trait that `resolve` the given ident to expression code to assign the value. pub(crate) trait Resolver { fn resolve(&self, arg: &Pat) -> Option>; } impl Resolver for HashMap { fn resolve(&self, arg: &Pat) -> Option> { self.get(arg) .or_else(|| self.get(&pat_invert_mutability(arg))) .map(|&c| Cow::Borrowed(c)) } } impl Resolver for HashMap { fn resolve(&self, arg: &Pat) -> Option> { self.get(arg).map(Cow::Borrowed) } } impl Resolver for (R1, R2) { fn resolve(&self, arg: &Pat) -> Option> { self.0.resolve(arg).or_else(|| self.1.resolve(arg)) } } impl Resolver for &R { fn resolve(&self, arg: &Pat) -> Option> { (*self).resolve(arg) } } impl Resolver for Box { fn resolve(&self, arg: &Pat) -> Option> { (**self).resolve(arg) } } impl Resolver for (Pat, Expr) { fn resolve(&self, arg: &Pat) -> Option> { if arg == &self.0 { Some(Cow::Borrowed(&self.1)) } else { None } } } pub(crate) fn pat_invert_mutability(p: &Pat) -> Pat { match p.clone() { Pat::Ident(mut ident) => { ident.mutability = match ident.mutability { Some(_) => None, None => Some(syn::parse_quote! { mut }), }; syn::Pat::Ident(ident) } p => p, } } #[cfg(test)] mod should { use super::*; use crate::test::{assert_eq, *}; use syn::parse_str; #[test] fn return_the_given_expression() { let ast = parse_str("fn function(mut foo: String) {}").unwrap(); let arg = first_arg_pat(&ast); let expected = expr("bar()"); let mut resolver = HashMap::new(); resolver.insert(pat("foo").with_mut(), &expected); assert_eq!(expected, (&resolver).resolve(&arg).unwrap().into_owned()) } #[test] fn return_the_given_expression_also_if_not_mut_searched() { let ast = parse_str("fn function(foo: String) {}").unwrap(); let arg = first_arg_pat(&ast); let expected = expr("bar()"); let mut resolver = HashMap::new(); resolver.insert(pat("foo").with_mut(), &expected); assert_eq!(expected, (&resolver).resolve(&arg).unwrap().into_owned()) } #[test] fn return_none_for_unknown_argument() { let ast = "fn function(mut fix: String) {}".ast(); let arg = first_arg_pat(&ast); assert!(EmptyResolver.resolve(&arg).is_none()) } } rstest_macros-0.26.1/src/test.rs000064400000000000000000000212521046102023000147200ustar 00000000000000#![macro_use] /// Unit testing utility module. Collect a bunch of functions¯o and impls to simplify unit /// testing bolilerplate. /// use std::borrow::Cow; pub(crate) use pretty_assertions::assert_eq; use proc_macro2::TokenTree; use quote::quote; pub(crate) use rstest::{fixture, rstest}; use syn::{parse::Parse, parse2, parse_quote, parse_str, Error, Expr, Ident, Pat, Stmt}; use utils::fn_args_pats; use super::*; use crate::parse::{ fixture::{FixtureData, FixtureItem}, rstest::{RsTestData, RsTestItem}, testcase::TestCase, vlist::ValueList, Attribute, Fixture, Positional, }; use crate::resolver::Resolver; use parse::fixture::ArgumentValue; macro_rules! to_args { ($e:expr) => {{ $e.iter().map(expr).collect::>() }}; } macro_rules! to_fnargs { ($e:expr) => {{ $e.iter().map(fn_arg).collect::>() }}; } macro_rules! to_exprs { ($e:expr) => { $e.iter().map(|s| expr(s)).collect::>() }; } macro_rules! to_strs { ($e:expr) => { $e.iter().map(ToString::to_string).collect::>() }; } macro_rules! to_idents { ($e:expr) => { $e.iter().map(|s| ident(s)).collect::>() }; } macro_rules! to_pats { ($e:expr) => { $e.iter().map(|s| pat(s)).collect::>() }; } struct Outer(T); impl Parse for Outer { fn parse(input: syn::parse::ParseStream) -> syn::Result { let outer: Ident = input.parse()?; if outer == "outer" { let content; let _ = syn::parenthesized!(content in input); content.parse().map(Outer) } else { Err(Error::new(outer.span(), "Expected 'outer'")) } } } pub(crate) fn parse_meta>(test_case: S) -> T { let to_parse = format!( r#" #[outer({})] fn to_parse() {{}} "#, test_case.as_ref() ); let item_fn = parse_str::(&to_parse).expect(&format!("Cannot parse '{}'", to_parse)); let tokens = quote!( #item_fn ); let tt = tokens.into_iter().skip(1).next().unwrap(); if let TokenTree::Group(g) = tt { let ts = g.stream(); parse2::>(ts).unwrap().0 } else { panic!("Cannot find group in {:#?}", tt) } } pub(crate) trait ToAst { fn ast(self) -> T; } impl ToAst for &str { #[track_caller] fn ast(self) -> T { parse_str(self).unwrap() } } impl ToAst for String { #[track_caller] fn ast(self) -> T { parse_str(&self).unwrap() } } impl ToAst for proc_macro2::TokenStream { #[track_caller] fn ast(self) -> T { parse2(self).unwrap() } } pub(crate) fn ident(s: impl AsRef) -> Ident { s.as_ref().ast() } pub(crate) fn path(s: impl AsRef) -> syn::Path { s.as_ref().ast() } pub(crate) fn pat(s: impl AsRef) -> syn::Pat { syn::parse::Parser::parse_str(Pat::parse_single, s.as_ref()).unwrap() } pub trait PatBuilder { fn with_mut(self) -> Self; } impl PatBuilder for syn::Pat { fn with_mut(self) -> Self { match self { Pat::Ident(mut ident) => { ident.mutability = Some("mut".ast()); syn::Pat::Ident(ident) } _ => unimplemented!("Unsupported pattern: {:?}", self,), } } } pub(crate) fn expr(s: impl AsRef) -> syn::Expr { s.as_ref().ast() } pub(crate) fn fn_arg(s: impl AsRef) -> syn::FnArg { s.as_ref().ast() } pub(crate) fn attr(s: impl AsRef) -> syn::Attribute { let a = attrs(s); assert_eq!(1, a.len()); a.into_iter().next().unwrap() } pub(crate) fn attrs(s: impl AsRef) -> Vec { parse_str::(&format!( r#"{} fn _no_name_() {{}} "#, s.as_ref() )) .unwrap() .attrs } pub(crate) fn fixture(name: impl AsRef, args: &[&str]) -> Fixture { let name = name.as_ref().to_owned(); Fixture::new(pat(&name), path(&name), Positional(to_exprs!(args))) } pub(crate) fn arg_value(name: impl AsRef, value: impl AsRef) -> ArgumentValue { ArgumentValue::new(pat(name), expr(value)) } pub(crate) fn values_list>(arg: &str, values: &[S]) -> ValueList { ValueList { arg: pat(arg), values: values.into_iter().map(|s| expr(s).into()).collect(), } } pub(crate) fn first_arg_pat(ast: &ItemFn) -> &Pat { fn_args_pats(&ast).next().unwrap() } pub(crate) fn extract_inner_functions(block: &syn::Block) -> impl Iterator { block.stmts.iter().filter_map(|s| match s { syn::Stmt::Item(syn::Item::Fn(f)) => Some(f), _ => None, }) } pub(crate) fn literal_expressions_str() -> Vec<&'static str> { vec![ "42", "42isize", "1.0", "-1", "-1.0", "true", "1_000_000u64", "0b10100101u8", r#""42""#, "b'H'", ] } pub(crate) trait ExtractArgs { fn args(&self) -> Vec; } impl ExtractArgs for TestCase { fn args(&self) -> Vec { self.args.iter().cloned().collect() } } impl ExtractArgs for ValueList { fn args(&self) -> Vec { self.values.iter().map(|v| v.expr.clone()).collect() } } impl Attribute { pub fn attr>(s: S) -> Self { Attribute::Attr(ident(s)) } pub fn tagged, SA: AsRef>(tag: SI, attrs: Vec) -> Self { Attribute::Tagged(ident(tag), attrs.into_iter().map(pat).collect()) } pub fn typed, T: AsRef>(tag: S, inner: T) -> Self { Attribute::Type(ident(tag), parse_str(inner.as_ref()).unwrap()) } } impl RsTestInfo { pub fn push_case(&mut self, case: TestCase) { self.data.items.push(RsTestItem::TestCase(case)); } pub fn extend(&mut self, cases: impl Iterator) { self.data.items.extend(cases.map(RsTestItem::TestCase)); } } impl Fixture { pub fn with_resolve(mut self, resolve_path: &str) -> Self { self.resolve = path(resolve_path); self } } impl TestCase { pub fn with_description(mut self, description: &str) -> Self { self.description = Some(ident(description)); self } pub fn with_attrs(mut self, attrs: Vec) -> Self { self.attrs = attrs; self } } impl> FromIterator for TestCase { fn from_iter>(iter: T) -> Self { TestCase { args: iter.into_iter().map(expr).collect(), attrs: Default::default(), description: None, } } } impl<'a> From<&'a str> for TestCase { fn from(argument: &'a str) -> Self { std::iter::once(argument).collect() } } impl From> for RsTestData { fn from(items: Vec) -> Self { Self { items } } } impl From for RsTestInfo { fn from(data: RsTestData) -> Self { Self { data, ..Default::default() } } } impl From> for Positional { fn from(data: Vec) -> Self { Positional(data) } } impl From> for FixtureData { fn from(fixtures: Vec) -> Self { Self { items: fixtures } } } pub(crate) struct EmptyResolver; impl<'a> Resolver for EmptyResolver { fn resolve(&self, _pat: &Pat) -> Option> { None } } pub(crate) trait IsAwait { fn is_await(&self) -> bool; } impl IsAwait for Stmt { fn is_await(&self) -> bool { match self { Stmt::Expr(Expr::Await(_), _) => true, _ => false, } } } pub(crate) trait DisplayCode { fn display_code(&self) -> String; } impl DisplayCode for T { fn display_code(&self) -> String { self.to_token_stream().to_string() } } impl crate::parse::fixture::FixtureInfo { pub(crate) fn with_once(mut self) -> Self { self.arguments.set_once(Some(attr("#[once]"))); self } } pub(crate) fn await_argument_code_string(arg_name: &str) -> String { let arg_name = ident(arg_name); let statement: Stmt = parse_quote! { let #arg_name = #arg_name.await; }; statement.display_code() } pub(crate) fn ref_argument_code_string(arg_name: &str) -> String { let arg_name = ident(arg_name); let statement: Expr = parse_quote! { &#arg_name }; statement.display_code() } pub(crate) fn mut_await_argument_code_string(arg_name: &str) -> String { let arg_name = ident(arg_name); let statement: Stmt = parse_quote! { let mut #arg_name = #arg_name.await; }; statement.display_code() } rstest_macros-0.26.1/src/utils.rs000064400000000000000000000341161046102023000151040ustar 00000000000000/// Contains some unsorted functions used across others modules /// use quote::format_ident; use std::collections::{HashMap, HashSet}; use unicode_ident::is_xid_continue; use crate::refident::{MaybeIdent, MaybePat}; use syn::{Attribute, Expr, FnArg, Generics, Ident, ItemFn, Pat, ReturnType, Type, WherePredicate}; /// Return an iterator over fn arguments items. /// pub(crate) fn fn_args_pats(test: &ItemFn) -> impl Iterator { fn_args(test).filter_map(MaybePat::maybe_pat) } pub(crate) fn compare_pat(a: &Pat, b: &Pat) -> bool { match (a, b) { (Pat::Ident(a), Pat::Ident(b)) => a.ident == b.ident, (Pat::Tuple(a), Pat::Tuple(b)) => a.elems == b.elems, (Pat::TupleStruct(a), Pat::TupleStruct(b)) => a.path == b.path && a.elems == b.elems, (Pat::Struct(a), Pat::Struct(b)) => a.path == b.path && a.fields == b.fields, _ => false, } } /// Return if function declaration has an ident /// pub(crate) fn fn_args_has_pat(fn_decl: &ItemFn, pat: &Pat) -> bool { fn_args_pats(fn_decl).any(|id| compare_pat(id, pat)) } /// Return an iterator over fn arguments. /// pub(crate) fn fn_args(item_fn: &ItemFn) -> impl Iterator { item_fn.sig.inputs.iter() } pub(crate) fn attr_ends_with(attr: &Attribute, segment: &syn::PathSegment) -> bool { attr.path().segments.iter().last() == Some(segment) } pub(crate) fn attr_starts_with(attr: &Attribute, segment: &syn::PathSegment) -> bool { attr.path().segments.iter().next() == Some(segment) } pub(crate) fn attr_is(attr: &Attribute, name: &str) -> bool { attr.path().is_ident(&format_ident!("{}", name)) } pub(crate) fn attr_in(attr: &Attribute, names: &[&str]) -> bool { names .iter() .any(|name| attr.path().is_ident(&format_ident!("{}", name))) } pub(crate) trait IsLiteralExpression { fn is_literal(&self) -> bool; } impl> IsLiteralExpression for E { fn is_literal(&self) -> bool { matches!( self.as_ref(), Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(_), .. }) ) } } // Recoursive search id by reference till find one in ends fn _is_used( visited: &mut HashSet, id: &Ident, references: &HashMap>, ends: &HashSet, ) -> bool { if visited.contains(id) { return false; } visited.insert(id.clone()); if ends.contains(id) { return true; } if references.contains_key(id) { for referred in references.get(id).unwrap() { if _is_used(visited, referred, references, ends) { return true; } } } false } // Recoursive search id by reference till find one in ends fn is_used(id: &Ident, references: &HashMap>, ends: &HashSet) -> bool { let mut visited = Default::default(); _is_used(&mut visited, id, references, ends) } impl MaybeIdent for syn::WherePredicate { fn maybe_ident(&self) -> Option<&Ident> { match self { WherePredicate::Type(syn::PredicateType { bounded_ty: t, .. }) => { first_type_path_segment_ident(t) } WherePredicate::Lifetime(syn::PredicateLifetime { lifetime, .. }) => { Some(&lifetime.ident) } _ => None, } } } #[derive(Default)] struct SearchSimpleTypeName(HashSet); impl SearchSimpleTypeName { fn take(self) -> HashSet { self.0 } fn visit_inputs<'a>(&mut self, inputs: impl Iterator) { use syn::visit::Visit; inputs.for_each(|fn_arg| self.visit_fn_arg(fn_arg)); } fn visit_output(&mut self, output: &ReturnType) { use syn::visit::Visit; self.visit_return_type(output); } fn collect_from_type_param(tp: &syn::TypeParam) -> Self { let mut s: Self = Default::default(); use syn::visit::Visit; s.visit_type_param(tp); s } fn collect_from_where_predicate(wp: &syn::WherePredicate) -> Self { let mut s: Self = Default::default(); use syn::visit::Visit; s.visit_where_predicate(wp); s } } impl<'ast> syn::visit::Visit<'ast> for SearchSimpleTypeName { fn visit_path(&mut self, p: &'ast syn::Path) { if let Some(id) = p.get_ident() { self.0.insert(id.clone()); } syn::visit::visit_path(self, p) } fn visit_lifetime(&mut self, i: &'ast syn::Lifetime) { self.0.insert(i.ident.clone()); syn::visit::visit_lifetime(self, i) } } // Take generics definitions and where clauses and return the // a map from simple types (lifetime names or type with just names) // to a set of all simple types that use it as some costrain. fn extract_references_map(generics: &Generics) -> HashMap> { let mut references = HashMap::>::default(); // Extracts references from types param generics.type_params().for_each(|tp| { SearchSimpleTypeName::collect_from_type_param(tp) .take() .into_iter() .for_each(|id| { references.entry(id).or_default().insert(tp.ident.clone()); }); }); // Extracts references from where clauses generics .where_clause .iter() .flat_map(|wc| wc.predicates.iter()) .filter_map(|wp| wp.maybe_ident().map(|id| (id, wp))) .for_each(|(ref_ident, wp)| { SearchSimpleTypeName::collect_from_where_predicate(wp) .take() .into_iter() .for_each(|id| { references.entry(id).or_default().insert(ref_ident.clone()); }); }); references } // Return a hash set that contains all types and lifetimes referenced // in input/output expressed by a single ident. fn references_ident_types<'a>( generics: &Generics, inputs: impl Iterator, output: &ReturnType, ) -> HashSet { let mut used: SearchSimpleTypeName = Default::default(); used.visit_output(output); used.visit_inputs(inputs); let references = extract_references_map(generics); let mut used = used.take(); let input_output = used.clone(); // Extend the input output collected ref with the transitive ones: used.extend( generics .params .iter() .filter_map(MaybeIdent::maybe_ident) .filter(|&id| is_used(id, &references, &input_output)) .cloned(), ); used } fn filtered_predicates(mut wc: syn::WhereClause, valids: &HashSet) -> syn::WhereClause { wc.predicates = wc .predicates .clone() .into_iter() .filter(|wp| { wp.maybe_ident() .map(|t| valids.contains(t)) .unwrap_or_default() }) .collect(); wc } fn filtered_generics<'a>( params: impl Iterator + 'a, valids: &'a HashSet, ) -> impl Iterator + 'a { params.filter(move |p| match p.maybe_ident() { Some(id) => valids.contains(id), None => false, }) } //noinspection RsTypeCheck pub(crate) fn generics_clean_up<'a>( original: &Generics, inputs: impl Iterator, output: &ReturnType, ) -> syn::Generics { let used = references_ident_types(original, inputs, output); let mut result: Generics = original.clone(); result.params = filtered_generics(result.params.into_iter(), &used).collect(); result.where_clause = result.where_clause.map(|wc| filtered_predicates(wc, &used)); result } // If type is not self and doesn't starts with :: return the first ident // of its path segment: only if is a simple path. // If type is a simple ident just return the this ident. That is useful to // find the base type for associate type indication fn first_type_path_segment_ident(t: &Type) -> Option<&Ident> { match t { Type::Path(tp) if tp.qself.is_none() && tp.path.leading_colon.is_none() => tp .path .segments .iter() .next() .and_then(|ps| match ps.arguments { syn::PathArguments::None => Some(&ps.ident), _ => None, }), _ => None, } } pub(crate) fn fn_arg_mutability(arg: &FnArg) -> Option { match arg { FnArg::Typed(syn::PatType { pat, .. }) => match pat.as_ref() { syn::Pat::Ident(syn::PatIdent { mutability, .. }) => *mutability, _ => None, }, _ => None, } } pub(crate) fn sanitize_ident(name: &str) -> String { name.chars() .filter(|c| !c.is_whitespace()) .map(|c| match c { '"' | '\'' => "__".to_owned(), ':' | '(' | ')' | '{' | '}' | '[' | ']' | ',' | '.' | '*' | '+' | '/' | '-' | '%' | '^' | '!' | '&' | '|' => "_".to_owned(), _ => c.to_string(), }) .collect::() .chars() .filter(|&c| is_xid_continue(c)) .collect() } #[cfg(test)] mod test { use syn::parse_quote; use super::*; use crate::test::{assert_eq, *}; #[test] fn fn_args_has_pat_should() { let item_fn = parse_quote! { fn the_function(first: u32, second: u32) {} }; assert!(fn_args_has_pat(&item_fn, &pat("first"))); assert!(!fn_args_has_pat(&item_fn, &pat("third"))); } #[rstest] #[case::base("fn foo(a: A) -> B {}", &["A", "B"])] #[case::use_const_in_array("fn foo(a: A) -> [u32; B] {}", &["A", "B", "u32"])] #[case::in_type_args("fn foo(a: A) -> SomeType {}", &["A", "B"])] #[case::in_type_args("fn foo(a: SomeType, b: SomeType) {}", &["A", "B"])] #[case::pointers("fn foo(a: *const A, b: &B) {}", &["A", "B"])] #[case::lifetime("fn foo<'a, A, B, C>(a: A, b: &'a B) {}", &["a", "A", "B"])] #[case::transitive_lifetime("fn foo<'a, A, B, C>(a: A, b: B) where B: Iterator + 'a {}", &["a", "A", "B"])] #[case::associated("fn foo<'a, A:Copy, C>(b: impl Iterator + 'a) {}", &["a", "A"])] #[case::transitive_in_defs("fn foo>(b: B) {}", &["A", "B"])] #[case::transitive_in_where("fn foo(b: B) where B: Iterator {}", &["A", "B"])] #[case::transitive_const("fn foo(b: B) where B: Some {}", &["A", "B"])] #[case::transitive_lifetime("fn foo<'a, A, B, C>(a: A, b: B) where B: Iterator + 'a {}", &["a", "A", "B"])] #[case::transitive_lifetime(r#"fn foo<'a, 'b, 'c, 'd, A, B, C> (a: A, b: B) where B: Iterator + 'c, 'c: 'a + 'b {}"#, &["a", "b", "c", "A", "B"])] fn references_ident_types_should(#[case] f: &str, #[case] expected: &[&str]) { let f: ItemFn = f.ast(); let used = references_ident_types(&f.sig.generics, f.sig.inputs.iter(), &f.sig.output); let expected = to_idents!(expected) .into_iter() .collect::>(); assert_eq!(expected, used); } #[rstest] #[case::remove_not_in_output( r#"fn test, B, F, H: Iterator>() -> (H, B, String, &str) where F: ToString, B: Borrow {}"#, r#"fn test>() -> (H, B, String, &str) where B: Borrow {}"# )] #[case::not_remove_used_in_arguments( r#"fn test, B, F, H: Iterator> (h: H, it: impl Iterator, j: &[B]) where F: ToString, B: Borrow {}"#, r#"fn test, B, H: Iterator> (h: H, it: impl Iterator, j: &[B]) where B: Borrow {}"# )] #[case::dont_remove_transitive( r#"fn test(a: A) where B: AsRef, A: Iterator, D: ArsRef {}"#, r#"fn test(a: A) where B: AsRef, A: Iterator {}"# )] #[case::remove_unused_lifetime( "fn test<'a, 'b, 'c, 'd, 'e, 'f, 'g, A>(a: &'a uint32, b: impl AsRef + 'b) where 'b: 'c + 'd, A: Copy + 'e, 'f: 'g {}", "fn test<'a, 'b, 'c, 'd, 'e, A>(a: &'a uint32, b: impl AsRef + 'b) where 'b: 'c + 'd, A: Copy + 'e {}" )] #[case::remove_unused_const( r#"fn test (a: [u32; A], b: SomeType, c: T) where T: Iterator, O: AsRef {}"#, r#"fn test (a: [u32; A], b: SomeType, c: T) where T: Iterator {}"# )] fn generics_cleaner(#[case] code: &str, #[case] expected: &str) { // Should remove all generics parameters that are not present in output let item_fn: ItemFn = code.ast(); let expected: ItemFn = expected.ast(); let cleaned = generics_clean_up( &item_fn.sig.generics, item_fn.sig.inputs.iter(), &item_fn.sig.output, ); assert_eq!(expected.sig.generics, cleaned); } #[rstest] #[case("1", "1")] #[case(r#""1""#, "__1__")] #[case(r#"Some::SomeElse"#, "Some__SomeElse")] #[case(r#""minnie".to_owned()"#, "__minnie___to_owned__")] #[case( r#"vec![1 , 2, 3]"#, "vec__1_2_3_" )] #[case( r#"some_macro!("first", {second}, [third])"#, "some_macro____first____second___third__" )] #[case(r#"'x'"#, "__x__")] #[case::ops(r#"a*b+c/d-e%f^g"#, "a_b_c_d_e_f_g")] fn sanitaze_ident_name(#[case] expression: impl AsRef, #[case] expected: impl AsRef) { assert_eq!(expected.as_ref(), sanitize_ident(expression.as_ref())); } }