kat/lib.rs
1//! Testing framework for known answer tests.
2//!
3//! This crate aims to drastically reduce the boilerplate code
4//! associated with rust tests, as well as to make known-answer tests easier
5//! to write and extend.
6//!
7//! This framework splits the tests into the test implementation
8//! and data, which is stored in .toml files.
9//!
10//! Under the hood, Kat uses [Serde](https://docs.rs/serde/latest/serde/index.html)
11//! and [Toml-rs](https://docs.rs/toml/latest/toml/) to deserialize test data.
12//! Both need to be added as dependencies to your crate.
13//!
14//! ## Getting Started
15//! ## Toml file layout
16//! The toml file must contain two sections, the **global section**
17//! and the **test section** (or sections).
18//! ```no_run
19//! // In this section global variables are defined.
20//! [global]
21//! my_global_var = "This is a global variable"
22//!
23//! // In these sections we define test cases.
24//! // Each test owns its own data.
25//! // Though every test must have the same
26//! // data signature
27//! [[test]]
28//! id = 0 // int
29//! data = "This is data for test 0" // string
30//! input = "INPUT" // string
31//! expected = "INPUT" // string
32//!
33//! // Multiple tests can be defined with
34//! // consecutive "test" tables
35//! [[test]]
36//! id = 1 // int
37//! data = "This is data for test 1" // string
38//! input = "INPUT" // string
39//! expected = "INPUT" // string
40//! ```
41//! If you'd like a comprehensive list of types, that you can
42//! include in your toml file, then visit the
43//! [Toml Website](https://toml.io/en/v1.0.0)
44//!
45//! ## Writing the tests
46//! Writing the tests is just as straight forward as writing the data.
47//! This tutorial will go step by step, in order of definition, and is based
48//! on the earlier demonstrated toml file layout.
49//!
50//! Import the kat crate
51//! ---
52//! This can be done, either in your test files global namespace
53//! (e.g tests/my_test.rs), or in a submodule
54//! (e.g tests/my_test.rs::my_submodule).
55//! ```no_run
56//! // Import Kat
57//! use kat::*;
58//!```
59//! Configure the test file path
60//! ---
61//! The [kat_cfg] macro configures the filepath of your
62//! test file. The path will be interpreted, relative to the
63//! workspace root. The file extension can be ommited, since we only support toml.
64//! String quotes around the path are not needed, since kat will
65//! directly interpolate the path from the macro expression.
66//!
67//! ```no_run
68//! // "WORKSPACE_ROOT/tests/data/my_data.toml"
69//! kat_cfg!(tests/data/my_data);
70//! ```
71//! Global and Test variables
72//! ---
73//! Now we define the layout for our global and test variables.
74//! Define the variables, just like you would in a normal
75//! Rust struct.
76//!
77//! Since Kat, internally uses Serde to deserialize the variables,
78//! every type in [global] and [test] must derive Deserialize.
79//!
80//! More to deserialization of types, in the
81//! [Deserializing Types section](./#deserializing-types)
82//!
83//! The [global] and [test] macros will generate structs
84//! which will later be parsed as the test files content.
85//! ```no_run
86//! // Define global variables
87//! global! {
88//! // The name of the variable must match
89//! // the one defined in your data file.
90//! my_global_var: String
91//! }
92//!
93//! // Define our test specific variables.
94//! // The same conventions, as in the global!
95//! // macro apply.
96//! test! {
97//! id: usize,
98//! data: String,
99//! input: String,
100//! expected: String,
101//! }
102//!```
103//! Running the tests
104//! ---
105//! And finally we provide the runner for our tests.
106//!
107//! Depending on your IDE, you can see
108//! a "Run tests" hint (VS Code for example).
109//!
110//! The tests will be run in the module
111//! "YOUR_MODULE::kat_tests", and the main test function
112//! is simply called "tests".
113//!
114//! Inside the [run] macro, you get access to your global and
115//! test variables, inside the here named variables `globals` and `test_case`.
116//! Both can be named like you would any other variable.
117//!
118//! On top of that you can execute any statements inside the macro.
119//! Though, mutating `globals` and `test_case` is not possible, since
120//! they're internally defined as immutable aka read-only.
121//!
122//! ```no_run
123//! // Test Runner
124//! run! {
125//! // Test Runner
126//! //
127//! // Note the lambda like invocation syntax.
128//! // It's specified in the macro as a match, for
129//! // easier readability and familiarity.
130//! |globals, test_case| -> {
131//!
132//! // Now pass the statements you want to run
133//!
134//! // We can access the global variable.
135//! println!("{}", global.my_global_var);
136//!
137//! // In similar fashion, the test case.
138//! println!("{}", test_case.id);
139//!
140//! // Any statements can be executed
141//!
142//! // Assertions
143//! assert_eq!(test_case.input, test_case.expected);
144//!
145//! // Function call which is defined somewhere...
146//! my_super_expensive_function();
147//!
148//! // Also from other modules
149//! mymod::my_function();
150//!
151//! // Variables
152//! let x = 25;
153//!
154//! // Macros
155//! my_crate::some_macro!();
156//! }
157//! }
158//!
159//! ```
160//! ### Panics
161//! The runner panics, if the test file wasn't found,
162//! an IO Error occured (e.g File open unsuccessful),
163//! or if toml parsing was erroneous.
164//!
165//! ---
166//! All in all, we end up with a structure like this:
167//! ```no_run
168//! // Path configuration
169//! kat_cfg(...);
170//!
171//! // Define global variables
172//! global! {
173//! ...
174//! }
175//!
176//! // Define Test variables
177//! test! {
178//! ...
179//! }
180//!
181//! // Implement Test Runner
182//! run! {
183//! |global, test| -> {
184//! ...
185//! }
186//!
187//! }
188//! ```
189//! Runner attributes
190//! ---
191//! As per usual rust tests, you can annotate the [run] macro with
192//! [test attributes](https://doc.rust-lang.org/reference/attributes/testing.html).
193//! The initial `#[test]` attribute is already being added for you internally.
194//! ```no_run
195//! // Ignore tests
196//! run! {
197//! #[ignore = "not yet implemented"]
198//! |global, test| -> {
199//! ...
200//! }
201//! }
202//! ```
203//! ```no_run
204//! // Should panic
205//! //
206//! // Note, that when running the tests from
207//! // the 'run' hint in your IDE, the test will
208//! // still be logged as fail. The test will
209//! // only accept the panic, when run with
210//! // "Cargo test"
211//! run! {
212//! #[should_panic(expected = "values don't match")]
213//! |global, test| -> {
214//! assert_eq!(1, 2, "values don't match");
215//! }
216//! }
217//! ```
218//! Type Attributes
219//! ---
220//! Kat supports type attributes for both, types defined
221//! in the [global] and [test] macro.
222//! ```no_run
223//! // Global macro as an example
224//! global! {
225//! my_type: String,
226//!
227//! #[my_attribute]
228//! my_attributed_type: usize
229//! }
230//! ```
231//! Deserializing Types
232//! ---
233//! ### Common Types
234//! Kat provides the major toml types in its [types] module.
235//! However, Kat does not support deserialization of multi-type
236//! arrays. For this case it is encouraged to deserialize an array
237//! of tables.
238//! ```no_run
239//! use kat::{types, DeriveTable};
240//!
241//! // Kat provides a "DeriveTable" attribute,
242//! // which actually is an alias for Serde's
243//! // Deserialize proc-macro.
244//! //
245//! // This is how you define a table
246//! #[derive(DeriveTable)]
247//! struct MyTable {
248//! value: types::TomlInt
249//! }
250//!
251//! global! {
252//! toml_string: types::TomlString,
253//! toml_int: types::TomlInt,
254//! toml_float: types::TomlFloat,
255//! toml_date: types::TomlDate,
256//! toml_bool: types::TomlBool,
257//! toml_int_array: types::TomlArray<types::TomlInt>,
258//! toml_table: MyTable,
259//! }
260//!
261//! ...
262//! ```
263//! The test file would look something like this:
264//! ```no_run
265//! [global]
266//! toml_string = "Toml String"
267//! toml_int = 10
268//! toml_float = 3.1415
269//! toml_date = 1979-05-27
270//! toml_bool = true
271//! toml_int_array = [1, 2, 3, 4, 5]
272//! [global.toml_table]
273//! value = 22
274//!
275//! ...
276//! ```
277//!
278//! ### Deserializing Custom Types
279//! Since Kat internally deserializes its types with the help of Serde and Toml-rs,
280//! primitive types like `String` or `usize` can be parsed directly from toml, without
281//! any macro magic, because Serde or Toml-rs provide internal deserialization implementations.
282//! So technically you could deserialize custom types with serde attributes.
283//! ```no_run
284//! // Your custom type
285//! struct StringHolder(String);
286//! impl From<String> for StringHolder {
287//! fn from(s: String) -> Self {
288//! Self(s)
289//! }
290//! }
291//!
292//! // Generic String deserializer
293//! // Deserialize a [T] if it's String constructable
294//! fn deserialize_from_string<'de, D, T>(deserializer: D) -> Result<T, D::Error>
295//! where
296//! D: Deserializer<'de>,
297//! T: From<String>
298//! {
299//! let s = String::deserialize(deserializer)?;
300//! Ok(T::from(s))
301//! }
302//!
303//! global! {
304//! // Use Serde attribute
305//! #[serde(deserialize_with = "deserialize_from_string")]
306//! string_holder: StringHolder
307//! }
308//!
309//! ...
310//! ```
311//! However, this results in a lot of boilerplate code.
312//!
313//! Luckily, Kat provides you with streamlined ways, in which you can
314//! focus on the From implementation, and let Kat handle the code generation:
315//! ### Deserialize Custom Types: The Kat way
316//! Kat provides macros that generate the code needed to deserialize your value.
317//! ```no_run
318//! struct StringHolder(String);
319//!
320//! // Note again the lambda syntax for
321//! // familiarity and readability
322//! impl_deserialize_from_toml_string!(
323//! |s| -> StringHolder {
324//! StringHolder(s)
325//! }
326//! );
327//!
328//! // Now use it
329//! global! {
330//! string_holder: StringHolder
331//! }
332//! ```
333//! Inside Toml file
334//! ```no_run
335//! [global]
336//! string_holder = "Hey Ho!"
337//!
338//! ```
339//! Here, `s` denotes the variable name for the passed
340//! [TomlString](types::TomlString), you can name it whatever
341//! you wish for. Then follows an arrow with the type, the code is
342//! to be generated for, here `StringHolder`. And finally the function
343//! body.
344//!
345//! The function body, is essentially the body of the
346//! `impl From<TomlString> for StringHolder` implementation,
347//! this macro generates. The macro also generates a deserialize
348//! implementation.
349//!
350//! Macros like this exist for all types in the [types] module, but Table and Array.
351//! For these two, you will need to call the [impl_deserialize_from_deserializable] macro.
352//!
353//! The [impl_deserialize_from_deserializable] macro can deserialize a custom type
354//! from any type that implements Serde's Deserialize trait.
355//! ```no_run
356//!
357//! #[derive(DeriveTable)]
358//! struct MyTable {
359//! value: TomlInt
360//! }
361//!
362//! struct MyTableHolder(MyTable)
363//!
364//! // Denote the input type being typed.
365//! // As stated earlier, this macro
366//! // generates `impls` from any type that
367//! // is deserializable, so it needs the type
368//! // annotation.
369//! // The rest stays exactly the same.
370//! // Note, that MyTableHolder doesn't
371//! // have to be a tuple, this still is
372//! // simply a From<T> implementation
373//! impl_deserialize_from_deserializable!(
374//! |table: MyTable| -> MyTableHolder {
375//! MyTableHolder(table)
376//! }
377//! );
378//!
379//! // From Array
380//! struct MyArrayHolder(TomlArray<usize>);
381//! impl_deserialize_from_deserializable!(
382//! |array: TomlArray<usize>| -> MyArrayHolder {
383//! MyArrayHolder(array)
384//! }
385//! );
386//! ```
387//! With this macro, it's also possible to chain your custom types.
388//! ```no_run
389//! // MyArrayHolder from previous example
390//!
391//! struct HoldsArrayHolder(MyArrayHolder);
392//! impl_deserialize_from_deserializable!(
393//! |holder: MyArrayHolder| -> HoldsArrayHolder {
394//! HoldsArrayHolder(holder)
395//! }
396//! );
397//!
398//! ```
399//! This is possible, since the macro generated
400//! the code for the Deserialize trait for `MyArrayHolder`
401//!
402//! ## Final Notes
403//! It is discouraged to rename the crate, since many macros
404//! inside the crate use the `kat::` module namespace
405//! in order to directly depent on a type, thus not cluttering the global namespace.
406//!
407//! On top of that, many exported traits and macros use the `__XXX` prefix.
408//! These items typically abstract the code generation away, thus, are private.
409//! They should **not** be used directly.
410
411mod de;
412pub use de::*;
413
414/// Configure the test files location.
415#[macro_export]
416macro_rules! kat_cfg {
417 ($path1: tt$(/$path2: tt)*) => {
418 #[allow(dead_code)]
419 const __FILEPATH_SLICE: &'static [&'static str] = &[
420 env!("CARGO_MANIFEST_DIR", "Cargo manifest directory environment variable is undefinded"),
421 stringify!($path1),
422 $(stringify!($path2),)*
423 ];
424 };
425}
426
427/// Defines the global variables inside the test file.
428#[macro_export]
429macro_rules! global {
430 ($($data: tt)*) => {
431
432 #[derive(kat::DeriveTable)]
433 struct __KatGlobal {
434 $($data)*
435 }
436 };
437}
438
439/// Defines the test specific variables inside the test file.
440#[macro_export]
441macro_rules! test {
442 ($($data: tt)*) => {
443
444 #[derive(kat::DeriveTable)]
445 struct __KatTest {
446 $($data)*
447 }
448 };
449}
450
451/// Runs the tests.
452#[macro_export]
453macro_rules! run {
454 (
455 $(#[$attr:meta])*
456 |$global_data: ident, $test_data: ident| -> {
457 $($body: tt)*
458 }
459 ) => {
460 #[cfg(test)]
461 mod kat_tests {
462
463 use super::*;
464
465 #[derive(kat::DeriveTable)]
466 struct __KatFileLayout {
467 global: __KatGlobal,
468
469 #[serde(rename = "test")]
470 tests: Vec<__KatTest>
471 }
472
473 pub fn __read_file_as_string() -> Result<String, String> {
474 use std::path::PathBuf;
475 use std::fs::File;
476 use std::io::Read;
477
478 let mut filepath: PathBuf = __FILEPATH_SLICE.iter().collect();
479 filepath.set_extension("toml");
480
481 if !filepath.is_file() {
482 return Err(format!(
483 "{} is not a file, or wasn't found",
484 filepath.display(),
485 ))
486 }
487
488 let mut file = match File::open(&filepath) {
489 Ok(file) => file,
490 Err(err) => return Err(format!(
491 "File {} was found, but could not be opened: {}",
492 filepath.display(), err.to_string()
493 ))
494 };
495
496 let mut content = String::new();
497
498 if let Err(err) = file.read_to_string(&mut content) {
499 return Err(format!(
500 "File {} was found, but could not be read: {}",
501 filepath.display(), err.to_string()
502 ))
503 }
504
505 Ok(content)
506 }
507
508 #[test]
509 $(#[$attr])*
510 fn tests() {
511
512 let file_content = match __read_file_as_string() {
513 Ok(content) => content,
514 Err(err) => { panic!("Error: {}", err) }
515 };
516
517 let ($global_data, kat_tests) = {
518 let layout: __KatFileLayout = match toml::from_str(&file_content) {
519 Ok(k) => k,
520 Err(err) => { panic!("Unable to parse toml: {}", err.to_string()) }
521 };
522
523 (layout.global, layout.tests)
524 };
525
526 kat_tests.into_iter()
527 .for_each(|$test_data|{
528 { $($body)* }
529 });
530 }
531 }
532 };
533}