1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
//! Isotest enables a very specific Rust unit testing pattern.
//!
//! Say you have some complex data type which is used in a lot of places.
//! You'd like to write functions that operate on that complex data, but these
//! functions only need to know about some small subset of that data type.
//! It could be convenient to write those functions generically over a trait which
//! contains methods that work with that subset of data, rather than taking the
//! full data type.
//!
//! There a few key benefits to doing this. Using a trait hides irrelevant details,
//! keeping the concern of the function limited to only what it needs to know.
//! This is a good application of the
//! [principle of least privilege](https://en.wikipedia.org/wiki/Principle_of_least_privilege).
//!
//! Using a trait also makes writing tests easier. If we used the full data type,
//! we would need to create test fixtures with a lot of extraneous arbitrary data which
//! won't even affect the function under test. By using a trait, we can write a much
//! simpler test-only data structure which implements the same trait, and use that instead
//! of the complex "real" data type in our tests. This keeps tests simpler and avoids the
//! need to generate lots of arbitrary throw-away data.
//!
//! Additionally, when debugging a failing test, it's a lot easier to get debug output on
//! just the data we care about, and not have all of the noise of the real data structure
//! included in the debug output.
//!
//! The only concern with this approach is that we're not testing the "real" data, and if there
//! is any problem with our test data type or its implementation of the common trait, then
//! we may not be testing what we think we are testing. This is where `isotest` comes in.
//!
//! `isotest` helps ensure that our implementation of the common trait is correct by
//! letting the user define functions to convert to and from the test data type, and
//! then providing a macro with a simple API to run tests for both types.
//! The user can write tests in terms of the simpler test data, but then that test gets run
//! for both test and real data, to ensure that assumptions implicit in the trait implementation
//! are not faulty. Thus, you get the benefit of simplifying your test writing while
//! still testing your logic with real data.

#![warn(missing_docs)]

use std::marker::PhantomData;

pub use paste;

/// Which of the two contexts each isotest body will run under.
#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum IsotestContext {
    /// The "test" context, using a minimal test struct
    Test,
    /// The "real" context, using the actual struct used in the rest of the code
    Real,
}

/// Trait that declares the relationship between a "test" and "real struct",
/// namely how to go back and forth between the two.
pub trait Iso: Sized {
    /// The real data which corresponds to the type this trait is defined for
    type Real: Clone + From<Self>;

    /// Return the test version of this data, mapping if necessary.
    ///
    /// The test data must be a subset of the real data, so that this
    /// transformation is never lossy.
    fn test(x: &Self::Real) -> Self;

    /// Return the real version of this data, mapping if necessary.
    ///
    /// In general, the real data is a superset of the test data,
    /// so some arbitrary data will need to be supplied to fill in
    /// the rest. In fact, it is best if truly random arbitrary
    /// data is used, as this will act as a fuzz test of your
    /// trait implementation.
    fn real(&self) -> Self::Real;
}

/// Helper to define a two-way [`Iso`] relationship between test data
/// and real data.
///
/// The macro mainly just helps you implement the trait succinctly,
/// but also throws in a free `From<Real> for Test` impl for you.
/// It also allows you to specify example cases on both the Test and
/// Real sides for which invariant tests will be generated.
///
/// It is important that the Iso implementation satisfies two invariants:
/// 1. for any Test data `t`, `t == Iso::test(Iso::real(t))`
/// 2. for any Real data `r`, `Iso::test(r) == Iso::test(Iso::real(Iso::test(r)))`
///
/// The `test_cases` and `real_cases` can be used to automatically generate tests
/// which check that these invariants are upheld for whatever cases are specified.
/// The tests are generated with the prefix `iso_impl_invariants_test__`.
///
/// You typically do not need to work with this trait directly. However,
/// It must be implemented for the two types that you use in
/// an [`isotest!`] invocation.
///
/// ```rust
/// use isotest::Iso;
///
/// #[derive(Clone, Debug, PartialEq)]
/// struct A(u8);
/// #[derive(Clone, Debug, PartialEq)]
/// struct B(u8, u8);
///
/// isotest::iso! {
///     A => |a| B(a.0, 42),
///     B => |b| A(b.0),
///     test_cases: [A(0), A(42)],
///     real_cases: [B(0, 0), B(42, 42)],
/// }
///
/// assert_eq!(A(1).real(), B(1, 42));
/// assert_eq!(A::test(&B(1, 2)), A(1));
/// ```
#[macro_export]
macro_rules! iso {
    (
        $a:ty => $forward:expr,
        $b:ty => $backward:expr
        $(, test_cases: [$($tc:expr),* $(,)?])?
        $(, real_cases: [$($rc:expr),* $(,)?])?
        $(,)?
    ) => {
        impl $crate::Iso for $a {
            type Real = $b;

            fn test(x: &$b) -> $a {
                let f: Box<dyn Fn($b) -> $a> = Box::new($backward);
                f(x.clone())
            }

            fn real(&self) -> $b {
                let f: Box<dyn Fn($a) -> $b> = Box::new($forward);
                f(self.clone())
            }
        }

        impl From<$a> for $b {
            fn from(a: $a) -> $b {
                use $crate::Iso;
                a.real()
            }
        }

        $($crate::paste::paste! {
            #[test]
            fn [< iso_impl_invariants_test__ $a:snake:lower __ $b:snake:lower >]() {
                $(
                    let test1: $a = $tc;
                    let test2 = $crate::roundtrip_test(&test1);
                    assert_eq!(test1, test2, "Iso test_case invariant test failed: {:?} != {:?}", test1, test2);
                )*
            }
        })?

        $($crate::paste::paste! {
            #[test]
            fn [< iso_impl_invariants_real__ $a:snake:lower __ $b:snake:lower >]() {
                $(
                    let real: $b = $rc;
                    let (test1, test2) = $crate::roundtrip_real::<$a, $b>(&real);
                    assert_eq!(test1, test2, "Iso real_case invariant test failed: {:?} != {:?}, real data = {:?}", test1, test2, real);
                )*
            }
        })?
    };
}

/// Roundtrip from test -> real -> test
pub fn roundtrip_test<A>(test: &A) -> A
where
    A: Iso + PartialEq + std::fmt::Debug,
{
    A::test(&test.real())
}

/// Roundtrip from real -> test -> real -> test, returning the two test items
pub fn roundtrip_real<A, B>(real: &B) -> (A, A)
where
    A: Iso<Real = B> + PartialEq + std::fmt::Debug,
    B: Clone + PartialEq + std::fmt::Debug,
{
    let test = A::test(&real);
    let test2 = A::test(&test.real());
    (test, test2)
}

/// Test the invariants of your Iso implementation.
/// This test must pass for any Test value you use.
pub fn assert_iso_invariants_test<A>(test: A)
where
    A: Iso + PartialEq + std::fmt::Debug,
{
    let test2 = A::test(&test.real());
    assert_eq!(
        test, test2,
        "test -> real -> test roundtrip should leave original value unchanged"
    );
}

/// Test the invariants of your Iso implementation.
/// This test must pass for any Real value you use.
pub fn assert_iso_invariants_real<A, B>(real: B)
where
    A: Iso<Real = B> + PartialEq + std::fmt::Debug,
    B: Clone + PartialEq + std::fmt::Debug,
{
    {
        let test = A::test(&real);
        let test2 = A::test(&test.real());
        assert_eq!(
            test, test2,
            "real -> test -> real -> test roundtrip should be idempotent"
        );
    }
}

/// Run the same closure for both the Test and Real versions of some data.
///
/// The macro takes a list of [`Iso`] implementors, followed by closure which
/// receives a small API which can handle each `Iso` type. See
/// [`IsoTestApi`] and [`IsoRealApi`] for descriptions of the methods.
/// ```rust
/// #[derive(Clone, Debug, PartialEq)]
/// struct A(u8);
/// #[derive(Clone, Debug, PartialEq)]
/// struct B(u8, u8);
///
/// trait X {
///     fn num(&self) -> u8;
/// }
///
/// impl X for A {
///     fn num(&self) -> u8 {
///         self.0
///     }
/// }
///
/// impl X for B {
///     fn num(&self) -> u8 {
///         self.0
///     }
/// }
///
/// isotest::iso! {
///     A => |a| B(a.0, 42),
///     B => |b| A(b.0),
/// };
///
/// isotest::isotest!(A => |iso| {
///     let mut a = iso.create(A(1));
///     assert_eq!(a.num(), 1);
///     iso.mutate(&mut a, |a| {
///         a.0 = 2;
///     });
///     assert_eq!(a.num(), 2);
/// });
/// ```
///
#[macro_export]
macro_rules! isotest {
    ( $($iso:ty),+ => $runner:expr) => {
        use $crate::Iso;
        {
            // This is the test using the "test" struct
            let run: Box<dyn Fn($( $crate::IsoTestApi<$iso>, )+ )> = Box::new($runner);
            run($( $crate::IsoTestApi::<$iso>::new(), )+);
        }
        {
            // This is the test using the "real" struct
            let run: Box<dyn Fn($( $crate::IsoRealApi<$iso>, )+ )> = Box::new($runner);
            run($( $crate::IsoRealApi::<$iso>::new(), )+);
        }
    };
}

/// A version of [`isotest!`] which returns `Future<Output = ()>` instead of `()`
/// and supports `async` syntax.
#[cfg(feature = "async")]
#[macro_export]
macro_rules! isotest_async {
    ($($iso:ty),+ => $runner:expr) => {
        use $crate::Iso;
        {
            // This is the test using the "test" struct
            let run: Box<dyn Fn($( $crate::IsoTestApi<$iso>, )+ )
                -> std::pin::Pin<Box<dyn futures::Future<Output = ()>>>,
            > = Box::new($runner);
            run($( $crate::IsoTestApi::<$iso>::new(), )+).await;
        }
        {
            // This is the test using the "real" struct
            let run: Box<dyn Fn($( $crate::IsoRealApi<$iso>, )+ )
                -> std::pin::Pin<Box<dyn futures::Future<Output = ()>>>,
            > = Box::new($runner);
            run($( $crate::IsoRealApi::<$iso>::new(), )+).await;
        }
    };
}

/// The API passed into an isotest in the Test context.
///
/// The `isotest!` macro is a bit sneaky, passing in APIs with different
/// function signatures for each context, so that the test can be written
/// the same lexically, but actually expand to two different tests
/// working with two different types.
pub struct IsoTestApi<A: Iso>(PhantomData<A>);

/// The API passed into an isotest in the Real context.
///
/// The `isotest!` macro is a bit sneaky, passing in APIs with different
/// function signatures for each context, so that the test can be written
/// the same lexically, but actually expand to two different tests
/// working with two different types.
pub struct IsoRealApi<A: Iso>(PhantomData<A>);

impl<A: Iso> IsoTestApi<A> {
    /// Constructor
    pub fn new() -> Self {
        Self(PhantomData)
    }

    /// Create test data from test data (identity function)
    pub fn create(&self, a: A) -> A {
        a
    }

    /// Update test data with a function over test data (simple map)
    pub fn update(&self, a: A, f: impl Fn(A) -> A) -> A {
        f(a)
    }

    /// Mutate test data with a function over test data (simple mutable map)
    pub fn mutate(&self, a: &mut A, f: impl Fn(&mut A)) {
        f(a)
    }

    /// Return the context we're in
    pub fn context(&self) -> IsotestContext {
        IsotestContext::Test
    }
}

impl<A: Iso> IsoRealApi<A> {
    /// Constructor
    pub fn new() -> Self {
        Self(PhantomData)
    }

    /// Create real data from test data
    pub fn create(&self, a: A) -> A::Real {
        a.real()
    }

    /// Update real data with a function over test data
    pub fn update(&self, x: A::Real, f: impl Fn(A) -> A) -> A::Real {
        f(A::test(&x)).real()
    }

    /// Mutate real data with a function over test data (simple mutable map)
    pub fn mutate(&self, x: &mut A::Real, f: impl Fn(&mut A)) {
        let mut t = A::test(x);
        f(&mut t);
        let _ = std::mem::replace(x, t.real());
    }

    /// Return the context we're in
    pub fn context(&self) -> IsotestContext {
        IsotestContext::Real
    }
}

/// The argument to an isotest `update` function
pub type Modify<A> = Box<dyn Fn(A) -> A>;

/// Creation function for the test context
pub type CreateTest<A> = Box<dyn Fn(A) -> A>;
/// Update function for the test context
pub type UpdateTest<A> = Box<dyn Fn(A, Modify<A>) -> A>;
/// Create function for the real context
pub type CreateReal<A, B> = Box<dyn Fn(A) -> B>;
/// Update function for the real context
pub type UpdateReal<A, B> = Box<dyn Fn(B, Modify<A>) -> B>;