aboutsummaryrefslogtreecommitdiffstats
path: root/contracts/test-utils/src/test_with_reference.ts
blob: 75d15b0aa9ee1e473790f6643db1f9a74d9fca3d (plain) (blame)
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
import * as chai from 'chai';
import * as _ from 'lodash';

import { chaiSetup } from './chai_setup';

chaiSetup.configure();
const expect = chai.expect;

class Value<T> {
    public value: T;
    constructor(value: T) {
        this.value = value;
    }
}

// tslint:disable-next-line: max-classes-per-file
class ErrorMessage {
    public error: string;
    constructor(message: string) {
        this.error = message;
    }
}

type PromiseResult<T> = Value<T> | ErrorMessage;

// TODO(albrow): This seems like a generic utility function that could exist in
// lodash. We should replace it by a library implementation, or move it to our
// own.
async function evaluatePromiseAsync<T>(promise: Promise<T>): Promise<PromiseResult<T>> {
    try {
        return new Value<T>(await promise);
    } catch (e) {
        return new ErrorMessage(e.message);
    }
}

export async function testWithReferenceFuncAsync<P0, R>(
    referenceFunc: (p0: P0) => Promise<R>,
    testFunc: (p0: P0) => Promise<R>,
    values: [P0],
): Promise<void>;
export async function testWithReferenceFuncAsync<P0, P1, R>(
    referenceFunc: (p0: P0, p1: P1) => Promise<R>,
    testFunc: (p0: P0, p1: P1) => Promise<R>,
    values: [P0, P1],
): Promise<void>;
export async function testWithReferenceFuncAsync<P0, P1, P2, R>(
    referenceFunc: (p0: P0, p1: P1, p2: P2) => Promise<R>,
    testFunc: (p0: P0, p1: P1, p2: P2) => Promise<R>,
    values: [P0, P1, P2],
): Promise<void>;
export async function testWithReferenceFuncAsync<P0, P1, P2, P3, R>(
    referenceFunc: (p0: P0, p1: P1, p2: P2, p3: P3) => Promise<R>,
    testFunc: (p0: P0, p1: P1, p2: P2, p3: P3) => Promise<R>,
    values: [P0, P1, P2, P3],
): Promise<void>;
export async function testWithReferenceFuncAsync<P0, P1, P2, P3, P4, R>(
    referenceFunc: (p0: P0, p1: P1, p2: P2, p3: P3, p4: P4) => Promise<R>,
    testFunc: (p0: P0, p1: P1, p2: P2, p3: P3, p4: P4) => Promise<R>,
    values: [P0, P1, P2, P3, P4],
): Promise<void>;

/**
 * Tests the behavior of a test function by comparing it to the expected
 * behavior (defined by a reference function).
 *
 * First the reference function will be called to obtain an "expected result",
 * or if the reference function throws/rejects, an "expected error". Next, the
 * test function will be called to obtain an "actual result", or if the test
 * function throws/rejects, an "actual error". The test passes if at least one
 * of the following conditions is met:
 *
 * 1) Neither the reference function or the test function throw and the
 * "expected result" equals the "actual result".
 *
 * 2) Both the reference function and the test function throw and the "actual
 * error" message *contains* the "expected error" message.
 *
 * @param referenceFuncAsync a reference function implemented in pure
 * JavaScript/TypeScript which accepts N arguments and returns the "expected
 * result" or throws/rejects with the "expected error".
 * @param testFuncAsync a test function which, e.g., makes a call or sends a
 * transaction to a contract. It accepts the same N arguments returns the
 * "actual result" or throws/rejects with the "actual error".
 * @param values an array of N values, where each value corresponds in-order to
 * an argument to both the test function and the reference function.
 * @return A Promise that resolves if the test passes and rejects if the test
 * fails, according to the rules described above.
 */
export async function testWithReferenceFuncAsync(
    referenceFuncAsync: (...args: any[]) => Promise<any>,
    testFuncAsync: (...args: any[]) => Promise<any>,
    values: any[],
): Promise<void> {
    // Measure correct behaviour
    const expected = await evaluatePromiseAsync(referenceFuncAsync(...values));

    // Measure actual behaviour
    const actual = await evaluatePromiseAsync(testFuncAsync(...values));

    // Compare behaviour
    if (expected instanceof ErrorMessage) {
        // If we expected an error, check if the actual error message contains the
        // expected error message.
        if (!(actual instanceof ErrorMessage)) {
            throw new Error(
                `Expected error containing ${expected.error} but got no error\n\tTest case: ${_getTestCaseString(
                    referenceFuncAsync,
                    values,
                )}`,
            );
        }
        expect(actual.error).to.contain(
            expected.error,
            `${actual.error}\n\tTest case: ${_getTestCaseString(referenceFuncAsync, values)}`,
        );
    } else {
        // If we do not expect an error, compare actual and expected directly.
        expect(actual).to.deep.equal(expected, `Test case ${_getTestCaseString(referenceFuncAsync, values)}`);
    }
}

function _getTestCaseString(referenceFuncAsync: (...args: any[]) => Promise<any>, values: any[]): string {
    const paramNames = _getParameterNames(referenceFuncAsync);
    return JSON.stringify(_.zipObject(paramNames, values));
}

// Source: https://stackoverflow.com/questions/1007981/how-to-get-function-parameter-names-values-dynamically
function _getParameterNames(func: (...args: any[]) => any): string[] {
    return _.toString(func)
        .replace(/[/][/].*$/gm, '') // strip single-line comments
        .replace(/\s+/g, '') // strip white space
        .replace(/[/][*][^/*]*[*][/]/g, '') // strip multi-line comments
        .split('){', 1)[0]
        .replace(/^[^(]*[(]/, '') // extract the parameters
        .replace(/=[^,]+/g, '') // strip any ES6 defaults
        .split(',')
        .filter(Boolean); // split & filter [""]
}