├── .github └── workflows │ └── rust.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE.txt ├── Makefile.toml ├── README.md ├── examples ├── simple.rs └── stream.rs └── src ├── lib.rs ├── tap_suite.rs ├── tap_suite_builder.rs ├── tap_test.rs ├── tap_test_builder.rs └── tap_writer.rs /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ubuntu-latest 9 | 10 | steps: 11 | - uses: actions/checkout@v1 12 | - name: Build 13 | run: cargo build --verbose 14 | - name: Run tests 15 | run: cargo test --verbose 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | Dockerfile 3 | *.crt 4 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "testanything" 7 | version = "0.4.1" 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "testanything" 3 | version = "0.4.1" 4 | authors = ["Jonathan E. Magen "] 5 | edition = "2021" 6 | description = "Generate results in the Test Anything Protocol (TAP)" 7 | license = "Apache-2.0" 8 | homepage = "https://github.com/cigna/tap-rust" 9 | repository = "https://github.com/cigna/tap-rust" 10 | readme = "README.md" 11 | 12 | [dependencies] 13 | 14 | [features] 15 | default = ["std"] 16 | 17 | # Provide all features. Requires a dependency on the Rust standard library. 18 | std = [] 19 | 20 | # Provide everything except printing to stdout. Uses `alloc`, which is a subset 21 | # of std but may be enabled without depending on all of std. 22 | alloc = [] 23 | -------------------------------------------------------------------------------- /LICENSE-APACHE.txt: -------------------------------------------------------------------------------- 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 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile.toml: -------------------------------------------------------------------------------- 1 | [tasks.format] 2 | install_crate = "rustfmt" 3 | command = "cargo" 4 | args = ["fmt", "--", "--check"] 5 | 6 | [tasks.build] 7 | command = "cargo" 8 | args = ["build"] 9 | 10 | [tasks.test] 11 | command = "cargo" 12 | args = ["test"] 13 | 14 | [tasks.check] 15 | command = "cargo" 16 | args = ["check"] 17 | 18 | [tasks.clippy] 19 | command = "cargo" 20 | args = ["clippy"] 21 | 22 | [tasks.quality] 23 | dependencies = ["build", "test", "check", "clippy"] 24 | 25 | [tasks.publish] 26 | command = "cargo" 27 | args = ["publish"] 28 | dependencies = ["quality"] 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TAP: Test Anything Protocol 2 | 3 | `->` Lives on crates.io as the [`testanything`](https://crates.io/crates/testanything) crate. 4 | 5 | This Rust library provides facilities for the generating and emitting results in the [Test Anything Protocol](https://en.wikipedia.org/wiki/Test_Anything_Protocol). Please feel free to see [testanything.org](http://testanything.org/) for more information. 6 | 7 | ## Usage 8 | 9 | Please see the examples in the `examples` folder. 10 | 11 | Simple: 12 | 13 | ``` 14 | 1..2 15 | ok 1 Panda Bamboo 16 | not ok 2 Curry Noodle 17 | # Tree 18 | # Flower 19 | ``` 20 | 21 | ### Use with `alloc` only (`#[no_std]`) 22 | 23 | To use this crate with alloc in `#[no_std]`, use: 24 | 25 | `testanything = { version = "*", default-features = false, features = ["alloc"] }` 26 | 27 | ## Testing 28 | 29 | ```shell 30 | cargo test 31 | ``` 32 | 33 | ## License 34 | 35 | [Apache License Version 2.0](https://spdx.org/licenses/Apache-2.0.html) 36 | -------------------------------------------------------------------------------- /examples/simple.rs: -------------------------------------------------------------------------------- 1 | extern crate testanything; 2 | 3 | use testanything::tap_suite_builder::TapSuiteBuilder; 4 | use testanything::tap_test_builder::TapTestBuilder; 5 | 6 | use std::io; 7 | 8 | fn main() { 9 | // Make some tests 10 | let passing_test = TapTestBuilder::new() 11 | .name("Panda Bamboo") 12 | .passed(true) 13 | .finalize(); 14 | 15 | let failing_test = TapTestBuilder::new() 16 | .name("Curry Noodle") 17 | .passed(false) 18 | .diagnostics(&vec!["Tree", "Flower"]) 19 | .finalize(); 20 | 21 | // Build the suite 22 | let tap_suite = TapSuiteBuilder::new() 23 | .name("Example TAP Suite") 24 | .tests(vec![passing_test, failing_test]) 25 | .finalize(); 26 | 27 | match tap_suite.print(io::stdout().lock()) { 28 | Ok(_) => {} 29 | Err(reason) => eprintln!("{}", reason), 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /examples/stream.rs: -------------------------------------------------------------------------------- 1 | extern crate testanything; 2 | 3 | use testanything::tap_writer::TapWriter; 4 | 5 | fn main() { 6 | let writer = TapWriter::new("Example TAP stream"); 7 | 8 | // Write the plan out. This can come before or after the test results themselves. 9 | writer.plan(1, 6); 10 | 11 | // Give me the name as a diagnostic line 12 | writer.name(); 13 | 14 | // Print out some test results 15 | writer.ok(1, "Panda"); 16 | writer.ok(2, "Bamboo"); 17 | writer.ok(3, "Curry"); 18 | // This one failed, so explain why with a diagnostic line 19 | writer.not_ok(4, "Noodle"); 20 | writer.diagnostic("The above test failed because of XYZ reason"); 21 | writer.ok(5, "Tree"); 22 | 23 | // uh oh! something went horribly wrong and we need to stop before 24 | // we print out the results from test 6! 25 | writer.bail_out_with_message("Destabilized warp core! Can't continue!"); 26 | } 27 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! The Test Anything Protocol (TAP) is a plaintext format for expressing test results. It has been around since 1987 when it was invented to help test Perl. With this crate, this wonderfully-useful tool has been brought to Rust! 2 | //! 3 | //! This crate provides the machinery needed for producing and emitting TAP streams. 4 | //! 5 | //! For working, executable examples, please see the `examples` directory. 6 | //! 7 | //! # Examples 8 | //! 9 | //! The first method for producing a TAP stream is the `TapSuite` mechanism. This will come in handy when you're iterating over a collection and want to `map` it to a collection of `TapTest` results. We supply a `TapSuiteBuilder` and a `TapTestBuilder` to make this as nice as possible. 10 | //! 11 | //! Behold! The `TapSuite` 12 | //! 13 | //! ``` 14 | //! use testanything::tap_test_builder::TapTestBuilder; 15 | //! use testanything::tap_suite_builder::TapSuiteBuilder; 16 | //! 17 | //! use std::io; 18 | //! 19 | //! // Build a failing test 20 | //! let failing_tap_test = TapTestBuilder::new() 21 | //! .name("Example TAP test") 22 | //! .passed(false) 23 | //! .diagnostics(&vec!["This test failed because of X"]) 24 | //! .finalize(); 25 | //! 26 | //! // Construct a test result suite 27 | //! let tap_suite = TapSuiteBuilder::new() 28 | //! .name("Example TAP suite") 29 | //! .tests(vec![failing_tap_test]) 30 | //! .finalize(); 31 | //! 32 | //! // Print TAP to standard output in one chunk 33 | //! match tap_suite.print(io::stdout().lock()) { 34 | //! Ok(_) => {} 35 | //! Err(reason) => eprintln!("{}", reason), 36 | //! } 37 | //! ``` 38 | //! 39 | //! The second method uses the `TapWriter` facility and may be thought of as the direct approach. This mechanism allows you to write a semi-customizable TAP stream to STDOUT from anywhere in your program. Since the TAP specification requires that TAP be emitted to STDOUT, the `TapWriter` doesn't get fancy with any of the stream interfaces. 40 | //! 41 | //! Behold, the `TapWriter`! 42 | //! 43 | //! ``` 44 | //! use testanything::tap_writer::TapWriter; 45 | //! 46 | //! let writer = TapWriter::new("Example TAP stream"); 47 | //! 48 | //! // Write the plan out. This can come before or after the test results themselves. 49 | //! writer.plan(1, 6); 50 | //! 51 | //! // Give me the name as a diagnostic line 52 | //! writer.name(); 53 | //! 54 | //! // Print out some test results 55 | //! writer.ok(1, "Panda"); 56 | //! writer.ok(2, "Bamboo"); 57 | //! writer.ok(3, "Curry"); 58 | //! // This one failed, so explain why with a diagnostic line 59 | //! writer.not_ok(4, "Noodle"); 60 | //! writer.diagnostic("The above test failed because of XYZ reason"); 61 | //! writer.ok(5, "Tree"); 62 | //! 63 | //! // Uh oh! something went horribly wrong and we need to stop before 64 | //! // we print out the results from test 6! 65 | //! writer.bail_out_with_message("Destabilized warp core! Can't continue!"); 66 | //! ``` 67 | //! 68 | //! 69 | 70 | #![forbid(unsafe_code)] 71 | #![deny(clippy::all)] 72 | // Support using TAP without the standard library 73 | #![cfg_attr(not(feature = "std"), no_std)] 74 | #[cfg(all(feature = "alloc", not(feature = "std")))] 75 | extern crate alloc; 76 | 77 | /// Global constant for the "ok" 78 | const OK_SYMBOL: &str = "ok"; 79 | /// Global constant for the "not ok" 80 | const NOT_OK_SYMBOL: &str = "not ok"; 81 | 82 | pub mod tap_suite; 83 | pub mod tap_suite_builder; 84 | pub mod tap_test; 85 | pub mod tap_test_builder; 86 | #[cfg(feature = "std")] 87 | pub mod tap_writer; 88 | -------------------------------------------------------------------------------- /src/tap_suite.rs: -------------------------------------------------------------------------------- 1 | //! `TapSuite` -- A collection of `TapTest` objects renderable into a TAP text stream 2 | 3 | #[cfg(feature = "alloc")] 4 | use alloc::{format, string::String, vec, vec::Vec}; 5 | #[cfg(feature = "std")] 6 | use std::io::Write; 7 | 8 | use crate::tap_test::TapTest; 9 | 10 | /// Represents a collection of TAP tests (`TapTest`) which can be rendered into a (text) TAP stream. This orchestrates that rendering. 11 | #[derive(Debug, Clone, PartialEq, Eq)] 12 | pub struct TapSuite { 13 | /// The name of the suite. If this is a blank string, that's fine but it's considered a party foul. 14 | pub name: String, 15 | /// The collection of `TapTest` objects included in this test group, to be rendered into a TAP stream. 16 | pub tests: Vec, 17 | } 18 | 19 | impl TapSuite { 20 | /// Produce and arrange all text lines, in order, included in this TAP stream. This includes the leading plan line which is calculated based on the number of tests. 21 | pub fn lines(&self) -> Vec { 22 | // Make plan line 23 | let first_line = format!("1..{}", self.tests.len()); 24 | let mut all_lines = vec![first_line]; 25 | 26 | for (i, test) in self.tests.iter().enumerate() { 27 | let index = i as i64; // by default i is a usize. 28 | let tap = test.tap(index + 1); // TAP tests can't start with zero 29 | all_lines.extend(tap.iter().cloned()); 30 | } 31 | 32 | all_lines 33 | } 34 | } 35 | 36 | #[cfg(feature = "std")] 37 | impl TapSuite { 38 | /// Emit TAP stream to the provided sink, which must be `Write`. 39 | pub fn print(&self, mut sink: T) -> Result { 40 | let output = self.lines().join("\n"); 41 | match write!(&mut sink, "{}", output) { 42 | Ok(_) => Result::Ok(output), 43 | Err(reason) => Result::Err(reason.to_string()), 44 | } 45 | } 46 | } 47 | 48 | #[cfg(test)] 49 | mod tests { 50 | use super::TapSuite; 51 | use crate::tap_test_builder::TapTestBuilder; 52 | 53 | #[test] 54 | fn test_lines() { 55 | let passing_test = TapTestBuilder::new() 56 | .name("Panda Bamboo") 57 | .passed(true) 58 | .finalize(); 59 | 60 | let failing_test = TapTestBuilder::new() 61 | .name("Curry Noodle") 62 | .passed(false) 63 | .diagnostics(&vec!["Tree", "Flower"]) 64 | .finalize(); 65 | 66 | let tap_suite = TapSuite { 67 | name: "Example TAP Suite".to_string(), 68 | tests: vec![passing_test, failing_test], 69 | }; 70 | 71 | let expected = vec![ 72 | "1..2", 73 | "ok 1 Panda Bamboo", 74 | "not ok 2 Curry Noodle", 75 | "# Tree", 76 | "# Flower", 77 | ]; 78 | let actual = tap_suite.lines(); 79 | 80 | assert_eq!(expected, actual); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/tap_suite_builder.rs: -------------------------------------------------------------------------------- 1 | #[cfg(feature = "alloc")] 2 | use alloc::{ 3 | string::{String, ToString}, 4 | vec::Vec, 5 | }; 6 | 7 | use core::option::Option; 8 | 9 | use crate::tap_suite::TapSuite; 10 | use crate::tap_test::TapTest; 11 | 12 | /// Coordinator for constructing `TapSuite` objects using the builder pattern. 13 | /// 14 | /// # Examples 15 | /// 16 | /// ``` 17 | /// use testanything::tap_suite_builder::TapSuiteBuilder; 18 | /// use testanything::tap_test_builder::TapTestBuilder; 19 | /// 20 | /// // Make a Vec so we have something 21 | /// let tests = vec![TapTestBuilder::new() 22 | /// .name("Example TAP test") 23 | /// .passed(true) 24 | /// .finalize()]; 25 | /// 26 | /// let tap_suite_from_builder = TapSuiteBuilder::new() 27 | /// .name("Example TAP test suite") 28 | /// .tests(tests) 29 | /// .finalize(); 30 | /// 31 | /// ``` 32 | #[derive(Debug, Clone, Default)] 33 | pub struct TapSuiteBuilder { 34 | /// Name of test suite 35 | pub name: Option, 36 | /// Vector of type `Vec` which holds the actual tests 37 | pub tests: Option>, 38 | } 39 | 40 | impl TapSuiteBuilder { 41 | /// Produce a new builder object 42 | pub fn new() -> TapSuiteBuilder { 43 | TapSuiteBuilder { 44 | name: None, 45 | tests: None, 46 | } 47 | } 48 | /// Set the name 49 | pub fn name>(&mut self, s: S) -> &mut TapSuiteBuilder { 50 | self.name = Some(s.into()); 51 | self 52 | } 53 | /// Set the tests 54 | pub fn tests(&mut self, test_vec: Vec) -> &mut TapSuiteBuilder { 55 | self.tests = Some(test_vec); 56 | self 57 | } 58 | /// Produce the configured `TapSuite` object. Name defaults to a blank `String` and the tests default to an empty `Vec`. 59 | pub fn finalize(&mut self) -> TapSuite { 60 | TapSuite { 61 | name: self.name.take().unwrap_or_default(), 62 | tests: self.tests.take().unwrap_or_default(), 63 | } 64 | } 65 | } 66 | 67 | #[cfg(test)] 68 | mod test { 69 | use super::TapSuiteBuilder; 70 | use crate::tap_suite::TapSuite; 71 | use crate::tap_test_builder::TapTestBuilder; 72 | 73 | #[test] 74 | fn test_tap_suite_builder() { 75 | let tests = vec![TapTestBuilder::new() 76 | .name("Example TAP test") 77 | .passed(true) 78 | .finalize()]; 79 | 80 | let tap_suite_from_builder = TapSuiteBuilder::new() 81 | .name("Example TAP test suite") 82 | .tests(tests) 83 | .finalize(); 84 | 85 | let tap_suite_from_scratch = TapSuite { 86 | name: "Example TAP test suite".to_string(), 87 | tests: vec![TapTestBuilder::new() 88 | .name("Example TAP test") 89 | .passed(true) 90 | .finalize()], 91 | }; 92 | 93 | assert_eq!(tap_suite_from_builder, tap_suite_from_scratch); 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /src/tap_test.rs: -------------------------------------------------------------------------------- 1 | //! `TapTest` -- The core, representing an individual TAP test. 2 | 3 | #[cfg(feature = "alloc")] 4 | use alloc::{ 5 | string::{String, ToString}, 6 | vec, 7 | vec::Vec, 8 | }; 9 | use core::fmt::Write; 10 | use std::fmt; 11 | 12 | use crate::{NOT_OK_SYMBOL, OK_SYMBOL}; 13 | 14 | /// A test, a collection of which (a `TapSuite`) will be rendered into a TAP text stream. A `TapTest` knows how to render itself. 15 | #[derive(Debug, Clone, PartialEq, Eq)] 16 | pub struct TapTest { 17 | /// The name of the test, will be the primary text on a TAP test line 18 | pub name: String, 19 | /// Did this test pass? 20 | pub passed: bool, 21 | /// If this test merits additional comments (diagnostics, in TAP parlance), they will be rendered in the TAP stream beginning with a # mark. 22 | pub diagnostics: Vec, 23 | } 24 | 25 | impl TapTest { 26 | /// Based on the test passing status, yield either "ok" or "not ok". 27 | pub fn ok_string(&self) -> String { 28 | if self.passed { 29 | OK_SYMBOL 30 | } else { 31 | NOT_OK_SYMBOL 32 | } 33 | .to_string() 34 | } 35 | 36 | /// Produce a properly-formatted TAP line. This excludes diagnostics. 37 | pub fn status_line(&self, test_number: i64) -> String { 38 | let ok_string = self.ok_string(); 39 | let test_number_string = test_number.to_string(); 40 | let mut buf = 41 | String::with_capacity(ok_string.len() + test_number_string.len() + self.name.len()); 42 | write!(&mut buf, "{} {} {}", ok_string, test_number, self.name).unwrap(); 43 | buf 44 | } 45 | 46 | /// Produce all lines (inclusive of diagnostics) representing this test. This is the money, right here. 47 | pub fn tap(&self, test_number: i64) -> Vec { 48 | // Build the first line 49 | let mut lines = vec![self.status_line(test_number)]; 50 | // If there are diagnostics lines, format them. 51 | let formatted_diagnostics = self 52 | .diagnostics 53 | .iter() 54 | .map(|comment| self.format_diagnostics(comment)) 55 | .collect::>(); 56 | 57 | lines.extend(formatted_diagnostics.iter().cloned()); 58 | 59 | lines 60 | } 61 | 62 | /// Diagnostics should begin with a # mark 63 | pub fn format_diagnostics(&self, line: &str) -> String { 64 | let mut buf = String::with_capacity(line.len() + 2); 65 | write!(&mut buf, "# {}", line).unwrap(); 66 | buf 67 | } 68 | } 69 | 70 | impl From for String { 71 | fn from(tap_test: TapTest) -> Self { 72 | let mut buf = String::new(); 73 | write!( 74 | &mut buf, 75 | "TapTest(name: {}, passed: {}, diagnostics: {:?})", 76 | tap_test.name, tap_test.passed, tap_test.diagnostics 77 | ) 78 | .unwrap(); 79 | buf 80 | } 81 | } 82 | 83 | impl From<&TapTest> for String { 84 | fn from(tap_test: &TapTest) -> Self { 85 | let mut buf = String::new(); 86 | write!( 87 | &mut buf, 88 | "TapTest(name: {}, passed: {}, diagnostics: {:?})", 89 | tap_test.name, tap_test.passed, tap_test.diagnostics 90 | ) 91 | .unwrap(); 92 | buf 93 | } 94 | } 95 | 96 | impl fmt::Display for TapTest { 97 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 98 | write!(f, "{}", String::from(self)) 99 | } 100 | } 101 | 102 | #[cfg(test)] 103 | mod tests { 104 | use super::*; 105 | 106 | #[test] 107 | fn test_tap_test_status_string() { 108 | let tap_test_passing = TapTest { 109 | name: "Panda".to_string(), 110 | passed: true, 111 | diagnostics: vec!["Doing fine".to_string()], 112 | }; 113 | 114 | let expected_passing = OK_SYMBOL; 115 | let actual_passing = tap_test_passing.ok_string(); 116 | 117 | assert_eq!(expected_passing, actual_passing); 118 | 119 | let tap_test_failing = TapTest { 120 | name: "Panda".to_string(), 121 | passed: false, 122 | diagnostics: vec!["Doing fine".to_string()], 123 | }; 124 | 125 | let expected_failing = NOT_OK_SYMBOL; 126 | let actual_failing = tap_test_failing.ok_string(); 127 | 128 | assert_eq!(expected_failing, actual_failing); 129 | } 130 | 131 | #[test] 132 | fn test_format_first_line() { 133 | let tap_test_passing = TapTest { 134 | name: "Panda".to_string(), 135 | passed: true, 136 | diagnostics: vec!["Doing fine".to_string()], 137 | }; 138 | 139 | let expected_passing = "ok 42 Panda"; 140 | let actual_passing = tap_test_passing.status_line(42); 141 | 142 | assert_eq!(expected_passing, actual_passing); 143 | 144 | let tap_test_failing = TapTest { 145 | name: "Panda".to_string(), 146 | passed: false, 147 | diagnostics: vec!["Doing fine".to_string()], 148 | }; 149 | 150 | let expected_failing = "not ok 42 Panda"; 151 | let actual_failing = tap_test_failing.status_line(42); 152 | 153 | assert_eq!(expected_failing, actual_failing); 154 | } 155 | 156 | #[test] 157 | fn test_tap_lines() { 158 | let tap_test_passing = TapTest { 159 | name: "Panda".to_string(), 160 | passed: true, 161 | diagnostics: vec!["Doing fine".to_string()], 162 | }; 163 | 164 | let expected_passing = vec!["ok 42 Panda", "# Doing fine"]; 165 | let actual_passing = tap_test_passing.tap(42); 166 | 167 | assert_eq!(expected_passing, actual_passing); 168 | } 169 | 170 | #[test] 171 | fn test_format_diagnostics() { 172 | let tap_test_passing = TapTest { 173 | name: "Panda".to_string(), 174 | passed: true, 175 | diagnostics: vec!["Doing fine".to_string()], 176 | }; 177 | 178 | let expected_passing = "# Doing fine"; 179 | let actual_passing = tap_test_passing.format_diagnostics(&tap_test_passing.diagnostics[0]); 180 | 181 | assert_eq!(expected_passing, actual_passing); 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/tap_test_builder.rs: -------------------------------------------------------------------------------- 1 | //! `TapTestBuilder` -- Helper for creating a `TapTestSuite` using the builder pattern 2 | 3 | #[cfg(feature = "alloc")] 4 | use alloc::{ 5 | string::{String, ToString}, 6 | vec::Vec, 7 | }; 8 | use core::{default::Default, option::Option}; 9 | 10 | use crate::tap_test::TapTest; 11 | 12 | /// Coordinator for construction of `TapTest` objects using the builder pattern. 13 | /// 14 | /// # Examples 15 | /// 16 | /// ``` 17 | /// use testanything::tap_test_builder::TapTestBuilder; 18 | /// 19 | /// let tap_test = TapTestBuilder::new() 20 | /// .name("Panda test") 21 | /// .passed(true) 22 | /// .diagnostics(&vec!["Something something something"]) 23 | /// .finalize(); 24 | /// ``` 25 | #[derive(Debug, Default)] 26 | pub struct TapTestBuilder { 27 | name: Option, 28 | passed: Option, 29 | diagnostics: Option>, 30 | } 31 | 32 | impl TapTestBuilder { 33 | /// Produce a blank `TapTest` (the default is a passing test) 34 | pub fn new() -> TapTestBuilder { 35 | TapTestBuilder { 36 | name: None, 37 | passed: None, 38 | diagnostics: None, 39 | } 40 | } 41 | /// Set test name 42 | pub fn name>(&mut self, s: S) -> &mut TapTestBuilder { 43 | self.name = Some(s.into()); 44 | self 45 | } 46 | /// Set passed status 47 | pub fn passed(&mut self, status: bool) -> &mut TapTestBuilder { 48 | self.passed = Some(status); 49 | self 50 | } 51 | /// Set diagnostics. This can be any number of lines. 52 | pub fn diagnostics(&mut self, comments: &[&str]) -> &mut TapTestBuilder { 53 | self.diagnostics = Some(comments.iter().map(|s| String::from(*s)).collect()); 54 | self 55 | } 56 | /// Produce the configured `TapTest` object. Panics if you don't pass a passed status. 57 | pub fn finalize(&mut self) -> TapTest { 58 | TapTest { 59 | name: self 60 | .name 61 | .take() 62 | .unwrap_or_else(|| "A test has no name".to_string()), 63 | passed: self 64 | .passed 65 | .take() 66 | .expect("You build a test but didn't say whether or not it passed"), 67 | diagnostics: self.diagnostics.take().unwrap_or_default(), 68 | } 69 | } 70 | } 71 | 72 | #[cfg(test)] 73 | mod tests { 74 | use super::TapTestBuilder; 75 | use crate::tap_test::TapTest; 76 | 77 | #[test] 78 | fn test_tap_test_builder() { 79 | let tap_test_from_builder = TapTestBuilder::new() 80 | .name("Panda") 81 | .passed(true) 82 | .diagnostics(&vec!["Doing fine"]) 83 | .finalize(); 84 | 85 | let tap_test_from_scratch = TapTest { 86 | name: "Panda".to_string(), 87 | passed: true, 88 | diagnostics: vec!["Doing fine".to_string()], 89 | }; 90 | 91 | assert_eq!(tap_test_from_builder, tap_test_from_scratch); 92 | } 93 | 94 | #[test] 95 | fn test_tap_test_builder_with_no_name() { 96 | let bad_tap_test = TapTestBuilder::new().passed(true).finalize(); 97 | 98 | let expected = "A test has no name"; 99 | assert_eq!(bad_tap_test.name, expected); 100 | } 101 | 102 | #[test] 103 | #[should_panic] 104 | fn test_tap_test_builder_with_no_passed_status() { 105 | TapTestBuilder::new().name("This should break").finalize(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/tap_writer.rs: -------------------------------------------------------------------------------- 1 | //! `TapWriter` -- For writing TAP streams incrementally 2 | 3 | use super::{NOT_OK_SYMBOL, OK_SYMBOL}; 4 | 5 | /// A named TAP stream writer. This will print directly to STDOUT as you call methods. No waiting. 6 | /// See examples/stream.rs for usage. 7 | #[derive(Debug)] 8 | pub struct TapWriter { 9 | /// TAP stream name 10 | pub name: String, 11 | } 12 | 13 | impl TapWriter { 14 | /// Make me a new one from a name. Don't leave the name blank as it improves clarity. 15 | pub fn new(name: &str) -> TapWriter { 16 | TapWriter { 17 | name: name.to_string(), 18 | } 19 | } 20 | 21 | /// Print out the plan like "1..5". If you don't know the plan ahead of time, it can come at the very end. 22 | pub fn plan(&self, start: i32, finish: i32) { 23 | println!("{}..{}", start, finish); 24 | } 25 | 26 | /// Print the suite name as a diagnostic line. Surrounded by blank diagnostic lines because pretty. 27 | pub fn name(&self) { 28 | self.diagnostic(""); 29 | self.diagnostic(&self.name); 30 | self.diagnostic(""); 31 | } 32 | 33 | /// Emit a passing test line. 34 | pub fn ok(&self, test_number: i32, message: &str) { 35 | println!("{} {} {}", OK_SYMBOL, test_number, message); 36 | } 37 | 38 | /// Emit a failing test line. 39 | pub fn not_ok(&self, test_number: i32, message: &str) { 40 | println!("{} {} {}", NOT_OK_SYMBOL, test_number, message); 41 | } 42 | 43 | /// Emit a diagnostic message. Prefaced with a #. 44 | pub fn diagnostic(&self, message: &str) { 45 | println!("# {}", message); 46 | } 47 | 48 | /// Emergency stop! This should be the last thing in the TAP stream. Nothing may come after it. 49 | pub fn bail_out(&self) { 50 | self.bail_out_with_message(""); 51 | } 52 | 53 | /// In case you want to bail out with a message. Please use this instead of plain `bail_out`. 54 | pub fn bail_out_with_message(&self, message: &str) { 55 | println!("Bail out! {}", message); 56 | } 57 | } 58 | --------------------------------------------------------------------------------