├── .github
├── dependabot.yml
└── workflows
│ └── ci.yml
├── .gitignore
├── CHANGELOG.md
├── Cargo.toml
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
├── RELEASING.md
├── tracing-test-macro-tests
├── Cargo.toml
├── LICENSE-APACHE
├── LICENSE-MIT
└── src
│ └── lib.rs
├── tracing-test-macro
├── .gitignore
├── Cargo.toml
├── LICENSE-APACHE
├── LICENSE-MIT
└── src
│ └── lib.rs
└── tracing-test
├── .gitignore
├── Cargo.toml
├── LICENSE-APACHE
├── LICENSE-MIT
├── README.md
└── src
├── internal.rs
├── lib.rs
└── subscriber.rs
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 |
4 | # Create PRs for GitHub Actions updates
5 | # src: https://github.com/marketplace/actions/build-and-push-docker-images#keep-up-to-date-with-github-dependabot
6 | - package-ecosystem: "github-actions"
7 | directory: "/"
8 | schedule:
9 | interval: "daily"
10 |
11 | # Note: Rust dependencies are not handled here. For those
12 | # dependencies, we want Dependabot only for security updates, which is
13 | # already enabled through GitHub repository settings.
14 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | on:
2 | push:
3 | branches:
4 | - main
5 | pull_request:
6 | schedule:
7 | - cron: '30 3 * * 2'
8 |
9 | name: CI
10 |
11 | jobs:
12 |
13 | test:
14 | name: run tests
15 | strategy:
16 | matrix:
17 | rust: [1.71.1, stable]
18 | runs-on: ubuntu-latest
19 | steps:
20 | - uses: actions/checkout@v4
21 | - uses: dtolnay/rust-toolchain@master
22 | with:
23 | toolchain: ${{ matrix.rust }}
24 | - name: Build with default features
25 | run: cargo build
26 | - name: Build with all features
27 | run: cargo build --all-features
28 | - name: Run tests
29 | run: cargo test --all-features
30 |
31 | clippy:
32 | name: run clippy lints
33 | runs-on: ubuntu-latest
34 | steps:
35 | - uses: actions/checkout@v4
36 | - uses: dtolnay/rust-toolchain@1.71.1
37 | with:
38 | components: clippy
39 | - run: cargo clippy --all-features
40 |
41 | fmt:
42 | name: run rustfmt
43 | runs-on: ubuntu-latest
44 | steps:
45 | - uses: actions/checkout@v4
46 | - uses: dtolnay/rust-toolchain@1.71.1
47 | with:
48 | components: rustfmt
49 | - run: rustup component add rustfmt
50 | - run: cargo fmt --all -- --check
51 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Rust
2 | target/
3 | Cargo.lock
4 |
5 | # Vim swapfiles
6 | *.swp
7 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
6 |
7 |
8 | ## 0.2.5 - 2024-06-02
9 |
10 | - [change] Replace `lazy_static` dependency with `std::sync::OnceCell` (#36)
11 | - [change] Update syn (#40)
12 | - [change] Include license text in the packaged crates (#41)
13 |
14 |
15 | ## 0.2.4 - 2023-02-01
16 |
17 | - [bug] `logs_assert` should not get logs from other tests (#19)
18 | - [bug] Fully qualify usage of `Result` in `logs_assert` (#17)
19 | - [docs] Add note that integration tests need no-env-filter (#20)
20 |
21 |
22 | ## 0.2.3 - 2022-07-20
23 |
24 | - [feature] Add no-env-filter feature to disable log filtering (#16)
25 |
26 |
27 | ## 0.2.2 - 2022-06-03
28 |
29 | - [bug] Ensure correct `Result` type is used (#15)
30 |
31 |
32 | ## 0.2.1 - 2021-11-23
33 |
34 | - [bug] Fix wrong internal dependencies
35 |
36 |
37 | ## 0.2.0 - 2021-11-23
38 |
39 | - [feature] Add new `logs_assert` function that allows for more flexible log
40 | assertions (#7)
41 | - [change] Bump `tokio` dev-dependency to version 1 (#9)
42 | - [change] Bump `tracing-subscriber` dependency to 0.3 (#11)
43 |
44 |
45 | ## 0.1.0 - 2020-11-19
46 |
47 | Initial release to crates.io.
48 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 |
3 | members = [
4 | "tracing-test",
5 | "tracing-test-macro",
6 | "tracing-test-macro-tests",
7 | ]
8 |
--------------------------------------------------------------------------------
/LICENSE-APACHE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
--------------------------------------------------------------------------------
/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | Copyright (C) 2020-2023 Threema GmbH, Danilo Bargen
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy of
4 | this software and associated documentation files (the "Software"), to deal in
5 | the Software without restriction, including without limitation the rights to
6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7 | of the Software, and to permit persons to whom the Software is furnished to do
8 | so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in all
11 | copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19 | SOFTWARE.
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # tracing-test
2 |
3 | [![Build status][workflow-badge]][workflow]
4 | [![Crates.io Version][crates-io-badge]][crates-io]
5 | [![Crates.io Downloads][crates-io-download-badge]][crates-io-download]
6 |
7 | This crate provides an easy way to enable logging in tests that use
8 | [tracing](https://tracing.rs/), even if they're async. Additionally, it adds a
9 | way to assert that certain things were logged.
10 |
11 | The focus is on testing the logging, not on debugging the tests. That's why the
12 | library ensures that the logs do not depend on external state. For example, the
13 | `RUST_LOG` env variable is not used for log filtering.
14 |
15 | Similar crates:
16 |
17 | - [test-log](https://crates.io/crates/test-log): Initialize loggers before
18 | running tests
19 | - [tracing-fluent-assertions](https://crates.io/crates/tracing-fluent-assertions):
20 | More powerful assertions that also allow analyzing spans
21 |
22 | ## Docs / Usage / Example
23 |
24 | See .
25 |
26 | ## License
27 |
28 | Copyright © 2020-2023 Threema GmbH, Danilo Bargen and Contributors.
29 |
30 | Licensed under either of
31 |
32 | * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or
33 | http://www.apache.org/licenses/LICENSE-2.0)
34 | * MIT license ([LICENSE-MIT](LICENSE-MIT) or
35 | http://opensource.org/licenses/MIT) at your option.
36 |
37 | ### Contribution
38 |
39 | Unless you explicitly state otherwise, any contribution intentionally submitted
40 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall
41 | be dual licensed as above, without any additional terms or conditions.
42 |
43 |
44 |
45 | [workflow]: https://github.com/dbrgn/tracing-test/actions?query=workflow%3ACI
46 | [workflow-badge]: https://img.shields.io/github/actions/workflow/status/dbrgn/tracing-test/ci.yml?branch=main
47 | [crates-io]: https://crates.io/crates/tracing-test
48 | [crates-io-badge]: https://img.shields.io/crates/v/tracing-test.svg?maxAge=3600
49 | [crates-io-download]: https://crates.io/crates/tracing-test
50 | [crates-io-download-badge]: https://img.shields.io/crates/d/tracing-test.svg?maxAge=3600
51 |
--------------------------------------------------------------------------------
/RELEASING.md:
--------------------------------------------------------------------------------
1 | # Releasing
2 |
3 | Set variables:
4 |
5 | $ export VERSION=X.Y.Z
6 | $ export GPG_KEY=20EE002D778AE197EF7D0D2CB993FF98A90C9AB1
7 |
8 | Update version numbers (Including the dependency of `tracing-test` on
9 | `tracing-test-macro`!):
10 |
11 | $ vim -p tracing-test/Cargo.toml tracing-test-macro/Cargo.toml
12 |
13 | Ensure that everything still works:
14 |
15 | $ cargo check
16 |
17 | Update changelog:
18 |
19 | $ vim CHANGELOG.md
20 |
21 | Commit & tag:
22 |
23 | $ git commit -S${GPG_KEY} -m "Release v${VERSION}"
24 | $ git tag -s -u ${GPG_KEY} v${VERSION} -m "Version ${VERSION}"
25 |
26 | Publish:
27 |
28 | $ cd tracing-test-macro && cargo publish
29 | $ cd ../tracing-test && cargo publish
30 | $ git push && git push --tags
31 |
--------------------------------------------------------------------------------
/tracing-test-macro-tests/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "tracing-test-macro-tests"
3 | version = "0.1.0"
4 | authors = ["Danilo Bargen "]
5 | license = "MIT"
6 | edition = "2018"
7 | repository = "https://github.com/dbrgn/tracing-test"
8 | description = "Only used for internal testing, don't publish."
9 | publish = false
10 |
11 | [dependencies]
12 |
13 | [dev-dependencies]
14 | tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
15 | tracing = { version = "0.1", default-features = false }
16 | tracing-test = { path = "../tracing-test" }
17 |
--------------------------------------------------------------------------------
/tracing-test-macro-tests/LICENSE-APACHE:
--------------------------------------------------------------------------------
1 | ../LICENSE-APACHE
--------------------------------------------------------------------------------
/tracing-test-macro-tests/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | ../LICENSE-MIT
--------------------------------------------------------------------------------
/tracing-test-macro-tests/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! This crate is only here to test the `tracing-test-macro` crate (because proc macros cannot be
2 | //! tested from within the crate itself).
3 |
4 | #[cfg(test)]
5 | mod tests {
6 | use tracing::{info, warn};
7 | use tracing_test::traced_test;
8 |
9 | #[tokio::test]
10 | #[traced_test]
11 | async fn test_logs_are_captured() {
12 | // Local log
13 | info!("This is being logged on the info level");
14 |
15 | info!("CountMe");
16 | // Log from a spawned task (which runs in a separate thread)
17 | tokio::spawn(async {
18 | warn!("This is being logged on the warn level from a spawned task");
19 | info!("CountMe");
20 | })
21 | .await
22 | .unwrap();
23 |
24 | // Ensure that `logs_contain` works as intended
25 | assert!(logs_contain("logged on the info level"));
26 | assert!(logs_contain("logged on the warn level"));
27 | assert!(!logs_contain("logged on the error level"));
28 |
29 | // Ensure that `logs_assert` works as intended (with a closure)
30 | logs_assert(|lines: &[&str]| {
31 | match lines.iter().filter(|line| line.contains("CountMe")).count() {
32 | 2 => Ok(()),
33 | n => Err(format!("Count should be 2, but was {}", n)),
34 | }
35 | });
36 |
37 | // Ensure that `logs_assert` works as intended (with a function)
38 | fn assert_fn(lines: &[&str]) -> Result<(), String> {
39 | match lines.iter().filter(|line| line.contains("CountMe")).count() {
40 | 2 => Ok(()),
41 | n => Err(format!("Count should be 2, but was {}", n)),
42 | }
43 | }
44 | logs_assert(assert_fn);
45 | }
46 |
47 | #[traced_test]
48 | #[test]
49 | fn annotate_sync_test() {
50 | assert!(!logs_contain("Logging from a non-async test"));
51 | info!("Logging from a non-async test");
52 | assert!(logs_contain("Logging from a non-async test"));
53 | assert!(!logs_contain("This was never logged"));
54 | }
55 |
56 | #[traced_test]
57 | #[test]
58 | fn no_log_from_other_test1() {
59 | info!("log count");
60 | logs_assert(|lines: &[&str]| {
61 | match lines
62 | .iter()
63 | .filter(|line| line.contains("log count"))
64 | .count()
65 | {
66 | 1 => Ok(()),
67 | n => Err(format!("Count should be 1, but was {}", n)),
68 | }
69 | });
70 | }
71 |
72 | #[traced_test]
73 | #[test]
74 | fn no_log_from_other_test2() {
75 | info!("log count");
76 | logs_assert(|lines: &[&str]| {
77 | match lines
78 | .iter()
79 | .filter(|line| line.contains("log count"))
80 | .count()
81 | {
82 | 1 => Ok(()),
83 | n => Err(format!("Count should be 1, but was {}", n)),
84 | }
85 | });
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/tracing-test-macro/.gitignore:
--------------------------------------------------------------------------------
1 | Cargo.lock
2 |
--------------------------------------------------------------------------------
/tracing-test-macro/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "tracing-test-macro"
3 | version = "0.2.5"
4 | authors = ["Danilo Bargen "]
5 | license = "MIT"
6 | edition = "2018"
7 | repository = "https://github.com/dbrgn/tracing-test"
8 | description = """
9 | A procedural macro that allow for easier testing of crates that use `tracing`.
10 |
11 | Internal crate, should only be used through the `tracing-test` crate.
12 | """
13 | readme = "../README.md"
14 | categories = ["development-tools::testing"]
15 |
16 | [lib]
17 | proc-macro = true
18 |
19 | [dependencies]
20 | quote = "1"
21 | syn = { version = "2", features = ["full"] }
22 |
23 | [badges]
24 | maintenance = { status = "experimental" }
25 |
26 | [features]
27 | # Disable hardcoded env filter
28 | no-env-filter = []
29 |
--------------------------------------------------------------------------------
/tracing-test-macro/LICENSE-APACHE:
--------------------------------------------------------------------------------
1 | ../LICENSE-APACHE
--------------------------------------------------------------------------------
/tracing-test-macro/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | ../LICENSE-MIT
--------------------------------------------------------------------------------
/tracing-test-macro/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! # tracing_test_macro
2 | //!
3 | //! This crate provides a procedural macro that can be added to test functions in order to ensure
4 | //! that all tracing logs are written to a global buffer.
5 | //!
6 | //! You should not use this crate directly. Instead, use the macro through [tracing-test].
7 | //!
8 | //! [tracing-test]: https://docs.rs/tracing-test
9 | extern crate proc_macro;
10 |
11 | use std::sync::{Mutex, OnceLock};
12 |
13 | use proc_macro::TokenStream;
14 | use quote::{quote, ToTokens};
15 | use syn::{parse, ItemFn, Stmt};
16 |
17 | /// Registered scopes.
18 | ///
19 | /// By default, every traced test registers a span with the function name.
20 | /// However, since multiple tests can share the same function name, in case
21 | /// of conflict, a counter is appended.
22 | ///
23 | /// This vector is used to store all already registered scopes.
24 | fn registered_scopes() -> &'static Mutex> {
25 | static REGISTERED_SCOPES: OnceLock>> = OnceLock::new();
26 | REGISTERED_SCOPES.get_or_init(|| Mutex::new(vec![]))
27 | }
28 |
29 | /// Check whether this test function name is already taken as scope. If yes, a
30 | /// counter is appended to make it unique. In the end, a unique scope is returned.
31 | fn get_free_scope(mut test_fn_name: String) -> String {
32 | let mut vec = registered_scopes().lock().unwrap();
33 | let mut counter = 1;
34 | let len = test_fn_name.len();
35 | while vec.contains(&test_fn_name) {
36 | counter += 1;
37 | test_fn_name.replace_range(len.., &counter.to_string());
38 | }
39 | vec.push(test_fn_name.clone());
40 | test_fn_name
41 | }
42 |
43 | /// A procedural macro that ensures that a global logger is registered for the
44 | /// annotated test.
45 | ///
46 | /// Additionally, the macro injects a local function called `logs_contain`,
47 | /// which can be used to assert that a certain string was logged within this
48 | /// test.
49 | ///
50 | /// Check out the docs of the `tracing-test` crate for more usage information.
51 | #[proc_macro_attribute]
52 | pub fn traced_test(_attr: TokenStream, item: TokenStream) -> TokenStream {
53 | // Parse annotated function
54 | let mut function: ItemFn = parse(item).expect("Could not parse ItemFn");
55 |
56 | // Determine scope
57 | let scope = get_free_scope(function.sig.ident.to_string());
58 |
59 | // Determine features
60 | //
61 | // Note: This cannot be called in the block below, otherwise it would be
62 | // evaluated in the context of the calling crate, not of the macro
63 | // crate!
64 | let no_env_filter = cfg!(feature = "no-env-filter");
65 |
66 | // Prepare code that should be injected at the start of the function
67 | let init = parse::(
68 | quote! {
69 | tracing_test::internal::INITIALIZED.call_once(|| {
70 | let env_filter = if #no_env_filter {
71 | "trace".to_string()
72 | } else {
73 | let crate_name = module_path!()
74 | .split(":")
75 | .next()
76 | .expect("Could not find crate name in module path")
77 | .to_string();
78 | format!("{}=trace", crate_name)
79 | };
80 | let mock_writer = tracing_test::internal::MockWriter::new(&tracing_test::internal::global_buf());
81 | let subscriber = tracing_test::internal::get_subscriber(mock_writer, &env_filter);
82 | tracing::dispatcher::set_global_default(subscriber)
83 | .expect("Could not set global tracing subscriber");
84 | });
85 | }
86 | .into(),
87 | )
88 | .expect("Could not parse quoted statement init");
89 | let span = parse::(
90 | quote! {
91 | let span = tracing::info_span!(#scope);
92 | }
93 | .into(),
94 | )
95 | .expect("Could not parse quoted statement span");
96 | let enter = parse::(
97 | quote! {
98 | let _enter = span.enter();
99 | }
100 | .into(),
101 | )
102 | .expect("Could not parse quoted statement enter");
103 | let logs_contain_fn = parse::(
104 | quote! {
105 | fn logs_contain(val: &str) -> bool {
106 | tracing_test::internal::logs_with_scope_contain(#scope, val)
107 | }
108 |
109 | }
110 | .into(),
111 | )
112 | .expect("Could not parse quoted statement logs_contain_fn");
113 | let logs_assert_fn = parse::(
114 | quote! {
115 | /// Run a function against the log lines. If the function returns
116 | /// an `Err`, panic. This can be used to run arbitrary assertion
117 | /// logic against the logs.
118 | fn logs_assert(f: impl Fn(&[&str]) -> std::result::Result<(), String>) {
119 | match tracing_test::internal::logs_assert(#scope, f) {
120 | Ok(()) => {},
121 | Err(msg) => panic!("The logs_assert function returned an error: {}", msg),
122 | };
123 | }
124 | }
125 | .into(),
126 | )
127 | .expect("Could not parse quoted statement logs_assert_fn");
128 |
129 | // Inject code into function
130 | function.block.stmts.insert(0, init);
131 | function.block.stmts.insert(1, span);
132 | function.block.stmts.insert(2, enter);
133 | function.block.stmts.insert(3, logs_contain_fn);
134 | function.block.stmts.insert(4, logs_assert_fn);
135 |
136 | // Generate token stream
137 | TokenStream::from(function.to_token_stream())
138 | }
139 |
140 | #[cfg(test)]
141 | mod tests {
142 | use super::*;
143 |
144 | #[test]
145 | fn test_get_free_scope() {
146 | let initial = get_free_scope("test_fn_name".to_string());
147 | assert_eq!(initial, "test_fn_name");
148 |
149 | let second = get_free_scope("test_fn_name".to_string());
150 | assert_eq!(second, "test_fn_name2");
151 | let third = get_free_scope("test_fn_name".to_string());
152 | assert_eq!(third, "test_fn_name3");
153 |
154 | // Insert a conflicting entry
155 | let fourth = get_free_scope("test_fn_name4".to_string());
156 | assert_eq!(fourth, "test_fn_name4");
157 |
158 | let fifth = get_free_scope("test_fn_name5".to_string());
159 | assert_eq!(fifth, "test_fn_name5");
160 | }
161 | }
162 |
--------------------------------------------------------------------------------
/tracing-test/.gitignore:
--------------------------------------------------------------------------------
1 | Cargo.lock
2 |
--------------------------------------------------------------------------------
/tracing-test/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "tracing-test"
3 | version = "0.2.5"
4 | authors = ["Danilo Bargen "]
5 | license = "MIT"
6 | edition = "2018"
7 | repository = "https://github.com/dbrgn/tracing-test"
8 | description = """
9 | Helper functions and macros that allow for easier testing of crates that use `tracing`.
10 | """
11 | readme = "../README.md"
12 | categories = ["development-tools::testing"]
13 |
14 | [dependencies]
15 | tracing-core = "0.1"
16 | tracing-subscriber = { version = "0.3", features = ["env-filter"] }
17 | tracing-test-macro = { path = "../tracing-test-macro", version = "0.2.5" }
18 |
19 | [dev-dependencies]
20 | tokio = { version = "1", features = ["rt-multi-thread", "macros"] } # Used for doctests
21 | tracing = { version = "0.1", default-features = false, features = ["std"] }
22 |
23 | [badges]
24 | maintenance = { status = "experimental" }
25 |
26 | [features]
27 | # Disable hardcoded env filter
28 | no-env-filter = ["tracing-test-macro/no-env-filter"]
29 |
--------------------------------------------------------------------------------
/tracing-test/LICENSE-APACHE:
--------------------------------------------------------------------------------
1 | ../LICENSE-APACHE
--------------------------------------------------------------------------------
/tracing-test/LICENSE-MIT:
--------------------------------------------------------------------------------
1 | ../LICENSE-MIT
--------------------------------------------------------------------------------
/tracing-test/README.md:
--------------------------------------------------------------------------------
1 | ../README.md
--------------------------------------------------------------------------------
/tracing-test/src/internal.rs:
--------------------------------------------------------------------------------
1 | //! Internal functionality used by the [`#[traced_test]`](attr.traced_test.html) macro.
2 | //!
3 | //! These functions should usually not be accessed from user code. The stability of these functions
4 | //! is not guaranteed, the API may change even in patch releases.
5 | use std::sync::{Mutex, Once, OnceLock};
6 |
7 | pub use crate::subscriber::{get_subscriber, MockWriter};
8 |
9 | /// Static variable to ensure that logging is only initialized once.
10 | pub static INITIALIZED: Once = Once::new();
11 |
12 | /// The global log output buffer used in tests.
13 | #[doc(hidden)]
14 | pub fn global_buf() -> &'static Mutex> {
15 | static GLOBAL_BUF: OnceLock>> = OnceLock::new();
16 | GLOBAL_BUF.get_or_init(|| Mutex::new(vec![]))
17 | }
18 |
19 | /// Return whether the logs with the specified scope contain the specified value.
20 | ///
21 | /// This function should usually not be used directly, instead use the `logs_contain(val: &str)`
22 | /// function injected by the [`#[traced_test]`](attr.traced_test.html) macro.
23 | pub fn logs_with_scope_contain(scope: &str, val: &str) -> bool {
24 | let logs = String::from_utf8(global_buf().lock().unwrap().to_vec()).unwrap();
25 | for line in logs.split('\n') {
26 | if line.contains(&format!(" {}:", scope)) && line.contains(val) {
27 | return true;
28 | }
29 | }
30 | false
31 | }
32 |
33 | /// Run a function against a slice of logs for the specified scope and return
34 | /// its result.
35 | ///
36 | /// This function should usually not be used directly, instead use the
37 | /// `logs_assert(F) where F: Fn(&[&str]) -> Result<(), String>` function
38 | /// injected by the [`#[traced_test]`](attr.traced_test.html) macro.
39 | pub fn logs_assert(scope: &str, f: F) -> std::result::Result<(), String>
40 | where
41 | F: Fn(&[&str]) -> std::result::Result<(), String>,
42 | {
43 | let buf = global_buf().lock().unwrap();
44 | let logs: Vec<&str> = std::str::from_utf8(&buf)
45 | .expect("Logs contain invalid UTF8")
46 | .lines()
47 | .filter(|line| line.contains(&format!(" {}:", scope)))
48 | .collect();
49 | f(&logs)
50 | }
51 |
--------------------------------------------------------------------------------
/tracing-test/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! Helper functions and macros that allow for easier testing of crates that use `tracing`.
2 | //!
3 | //! The focus is on testing the logging, not on debugging the tests. That's why the
4 | //! library ensures that the logs do not depend on external state. For example, the
5 | //! `RUST_LOG` env variable is not used for log filtering.
6 | //!
7 | //! Similar crates:
8 | //!
9 | //! - [test-log](https://crates.io/crates/test-log): Initialize loggers before
10 | //! running tests
11 | //! - [tracing-fluent-assertions](https://crates.io/crates/tracing-fluent-assertions):
12 | //! More powerful assertions that also allow analyzing spans
13 | //!
14 | //! ## Usage
15 | //!
16 | //! This crate should mainly be used through the
17 | //! [`#[traced_test]`](attr.traced_test.html) macro.
18 | //!
19 | //! First, add a dependency on `tracing-test` in `Cargo.toml`:
20 | //!
21 | //! ```toml
22 | //! tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
23 | //! tracing = "0.1"
24 | //! tracing-test = "0.1"
25 | //! ```
26 | //!
27 | //! Then, annotate your test function with the `#[traced_test]` macro.
28 | //!
29 | //! ```rust
30 | //! use tracing::{info, warn};
31 | //! use tracing_test::traced_test;
32 | //!
33 | //! #[tokio::test]
34 | //! #[traced_test]
35 | //! async fn test_logs_are_captured() {
36 | //! // Local log
37 | //! info!("This is being logged on the info level");
38 | //!
39 | //! // Log from a spawned task (which runs in a separate thread)
40 | //! tokio::spawn(async {
41 | //! warn!("This is being logged on the warn level from a spawned task");
42 | //! })
43 | //! .await
44 | //! .unwrap();
45 | //!
46 | //! // Ensure that certain strings are or aren't logged
47 | //! assert!(logs_contain("logged on the info level"));
48 | //! assert!(logs_contain("logged on the warn level"));
49 | //! assert!(!logs_contain("logged on the error level"));
50 | //!
51 | //! // Ensure that the string `logged` is logged exactly twice
52 | //! logs_assert(|lines: &[&str]| {
53 | //! match lines.iter().filter(|line| line.contains("logged")).count() {
54 | //! 2 => Ok(()),
55 | //! n => Err(format!("Expected two matching logs, but found {}", n)),
56 | //! }
57 | //! });
58 | //! }
59 | //! ```
60 | //!
61 | //! Done! You can write assertions using one of two injected functions:
62 | //!
63 | //! - `logs_contain(&str) -> bool`: Use this within an `assert!` call to ensure
64 | //! that a certain string is (or isn't) logged anywhere in the logs.
65 | //! - `logs_assert(f: impl Fn(&[&str]) -> Result<(), String>)`: Run a function
66 | //! against the log lines. If the function returns an `Err`, panic. This can
67 | //! be used to run arbitrary assertion logic against the logs.
68 | //!
69 | //! Logs are written to stdout, so they are captured by the cargo test runner
70 | //! by default, but printed if the test fails.
71 | //!
72 | //! Of course, you can also annotate regular non-async tests:
73 | //!
74 | //! ```rust
75 | //! use tracing::info;
76 | //! use tracing_test::traced_test;
77 | //!
78 | //! #[traced_test]
79 | //! #[test]
80 | //! fn plain_old_test() {
81 | //! assert!(!logs_contain("Logging from a non-async test"));
82 | //! info!("Logging from a non-async test");
83 | //! assert!(logs_contain("Logging from a non-async test"));
84 | //! assert!(!logs_contain("This was never logged"));
85 | //! }
86 | //! ```
87 | //!
88 | //! ## Rationale / Why You Need This
89 | //!
90 | //! Tracing allows you to set a default subscriber within a scope:
91 | //!
92 | //! ```rust
93 | //! # let subscriber = tracing::Dispatch::new(tracing_subscriber::FmtSubscriber::new());
94 | //! # let req = 123;
95 | //! # fn get_response(fake_req: u8) {}
96 | //! let response = tracing::dispatcher::with_default(&subscriber, || get_response(req));
97 | //! ```
98 | //!
99 | //! This works fine, as long as no threads are involved. As soon as you use a
100 | //! multi-threaded test runtime (e.g. the `#[tokio::test]` with the
101 | //! `rt-multi-thread` feature) and spawn tasks, the tracing logs in those tasks
102 | //! will not be captured by the subscriber.
103 | //!
104 | //! The macro provided in this crate registers a global default subscriber instead.
105 | //! This subscriber contains a writer which logs into a global static in-memory buffer.
106 | //!
107 | //! At the beginning of every test, the macro injects span opening code. The span
108 | //! uses the name of the test function (unless it's already taken, then a counter
109 | //! is appended). This means that the logs from a test are prefixed with the test
110 | //! name, which helps when debugging.
111 | //!
112 | //! Finally, a function called `logs_contain(value: &str)` is injected into every
113 | //! annotated test. It filters the logs in the buffer to include only lines
114 | //! containing ` {span_name}: ` and then searches the value in the matching log
115 | //! lines. This can be used to assert that a message was logged during a test.
116 | //!
117 | //! ## Per-crate Filtering
118 | //!
119 | //! By default, `tracing-test` sets an env filter that filters out all logs
120 | //! except the ones from your crate (equivalent to
121 | //! `RUST_LOG==trace`). If you need to capture logs from other crates
122 | //! as well, you can turn off this log filtering globally by enabling the
123 | //! `no-env-filter` Cargo feature:
124 | //!
125 | //! ```toml
126 | //! tracing-test = { version = "0.1", features = ["no-env-filter"] }
127 | //! ```
128 | //!
129 | //! Note that this will result in _all_ logs from _all_ your dependencies being
130 | //! captured! This means that the `logs_contain` function may become less
131 | //! useful, and you might need to use `logs_assert` instead, with your own
132 | //! custom filtering logic.
133 | //!
134 | //! **Note:** Rust "integration tests" (in the `tests/` directory) are each
135 | //! built into a separate crate from the crate they test. As a result,
136 | //! integration tests must use `no-env-filter` to capture and observe logs.
137 |
138 | pub mod internal;
139 | mod subscriber;
140 |
141 | pub use tracing_test_macro::traced_test;
142 |
--------------------------------------------------------------------------------
/tracing-test/src/subscriber.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | io,
3 | sync::{Mutex, MutexGuard},
4 | };
5 |
6 | use tracing_core::Dispatch;
7 | use tracing_subscriber::{fmt::MakeWriter, FmtSubscriber};
8 |
9 | /// A fake writer that writes into a buffer (behind a mutex).
10 | #[derive(Debug)]
11 | pub struct MockWriter<'a> {
12 | buf: &'a Mutex>,
13 | }
14 |
15 | impl<'a> MockWriter<'a> {
16 | /// Create a new `MockWriter` that writes into the specified buffer (behind a mutex).
17 | pub fn new(buf: &'a Mutex>) -> Self {
18 | Self { buf }
19 | }
20 |
21 | /// Give access to the internal buffer (behind a `MutexGuard`).
22 | fn buf(&self) -> io::Result>> {
23 | // Note: The `lock` will block. This would be a problem in production code,
24 | // but is fine in tests.
25 | self.buf
26 | .lock()
27 | .map_err(|_| io::Error::from(io::ErrorKind::Other))
28 | }
29 | }
30 |
31 | impl<'a> io::Write for MockWriter<'a> {
32 | fn write(&mut self, buf: &[u8]) -> io::Result {
33 | // Lock target buffer
34 | let mut target = self.buf()?;
35 |
36 | // Write to stdout in order to show up in tests
37 | print!("{}", String::from_utf8(buf.to_vec()).unwrap());
38 |
39 | // Write to buffer
40 | target.write(buf)
41 | }
42 |
43 | fn flush(&mut self) -> io::Result<()> {
44 | self.buf()?.flush()
45 | }
46 | }
47 |
48 | impl<'a> MakeWriter<'_> for MockWriter<'a> {
49 | type Writer = Self;
50 |
51 | fn make_writer(&self) -> Self::Writer {
52 | MockWriter::new(self.buf)
53 | }
54 | }
55 |
56 | /// Return a new subscriber that writes to the specified [`MockWriter`].
57 | ///
58 | /// [`MockWriter`]: struct.MockWriter.html
59 | pub fn get_subscriber(mock_writer: MockWriter<'static>, env_filter: &str) -> Dispatch {
60 | FmtSubscriber::builder()
61 | .with_env_filter(env_filter)
62 | .with_writer(mock_writer)
63 | .with_level(true)
64 | .with_ansi(false)
65 | .into()
66 | }
67 |
--------------------------------------------------------------------------------