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}