Skip to content

Commit 80abcf1

Browse files
committed
feat: add functions
add functions and unit tests.
0 parents  commit 80abcf1

File tree

3 files changed

+229
-0
lines changed

3 files changed

+229
-0
lines changed

code-contract.test.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/**
2+
* @file Unit tests for code contract utility
3+
*/
4+
import {
5+
assertEquals,
6+
assertThrows,
7+
} from "https://deno.land/std/testing/asserts.ts";
8+
9+
import {
10+
codeContract,
11+
disableContract,
12+
enableContract,
13+
} from "./code-contract.ts";
14+
15+
//
16+
// Variables
17+
//
18+
19+
let sideEffectTarget = true;
20+
21+
//
22+
// Functions
23+
//
24+
25+
const func = (lhs: number, rhs: number) => (lhs + rhs);
26+
27+
//
28+
// Test cases
29+
//
30+
31+
Deno.test({
32+
name: "It should success the function call if the contract is kept",
33+
fn: () => {
34+
// arrange
35+
enableContract();
36+
sideEffectTarget = false;
37+
const contracted = codeContract(func, {
38+
pre: (lhs: number, rhs: number) => 0 < lhs && 0 < rhs,
39+
post: (result: number) => (result === 30),
40+
});
41+
42+
// act & assert
43+
assertEquals(contracted(10, 20), 30);
44+
},
45+
});
46+
Deno.test({
47+
name: "It should fail the function call if the pre condition is invalid",
48+
fn: () => {
49+
assertThrows(
50+
() => {
51+
// arrange
52+
enableContract();
53+
sideEffectTarget = false;
54+
const contracted = codeContract(func, {
55+
pre: (lhs: number, rhs: number) => 0 < lhs && 0 < rhs,
56+
});
57+
58+
// act & assert
59+
assertEquals(contracted(20, -10), 10);
60+
},
61+
);
62+
},
63+
});
64+
Deno.test({
65+
name: "It should fail the function call if the post condition is invalid",
66+
fn: () => {
67+
assertThrows(
68+
() => {
69+
// arrange
70+
enableContract();
71+
sideEffectTarget = false;
72+
const contracted = codeContract(func, {
73+
post: (result: number) => (result === 30),
74+
});
75+
76+
// act & assert
77+
assertEquals(contracted(20, 20), 40);
78+
},
79+
);
80+
},
81+
});
82+
Deno.test({
83+
name:
84+
"It should fail the function call if the invariant condition is invalid",
85+
fn: () => {
86+
assertThrows(
87+
() => {
88+
// arrange
89+
enableContract();
90+
sideEffectTarget = false;
91+
92+
const funcWithSideEffect = (
93+
lhs: number,
94+
rhs: number,
95+
) => (sideEffectTarget = true, lhs + rhs);
96+
const contracted = codeContract(funcWithSideEffect, {
97+
invariant: () => (sideEffectTarget === false),
98+
});
99+
100+
// act & assert
101+
assertEquals(contracted(20, 20), 40);
102+
},
103+
);
104+
},
105+
});
106+
Deno.test({
107+
name: "It should omit contract check if contract check is disabled",
108+
fn: () => {
109+
// arrange
110+
disableContract();
111+
sideEffectTarget = false;
112+
const contracted = codeContract(func, {
113+
pre: (lhs: number, rhs: number) => 0 < lhs && 0 < rhs,
114+
post: (result: number) => (result === 30),
115+
});
116+
117+
// act & assert
118+
assertEquals(contracted(-10, -20), -30);
119+
120+
enableContract();
121+
},
122+
});

code-contract.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/**
2+
* @file Code contract utility
3+
*/
4+
//
5+
// Types
6+
//
7+
8+
// deno-lint-ignore no-explicit-any
9+
type FunctionType<T> = ((...args: any[]) => T) & { name: string };
10+
type ContractType<TResult> = {
11+
// deno-lint-ignore no-explicit-any
12+
pre?: (...args: any[]) => boolean;
13+
post?: (result: TResult) => boolean;
14+
invariant?: () => boolean;
15+
};
16+
17+
//
18+
// Variables
19+
//
20+
21+
let checkContract = true;
22+
23+
//
24+
// Functions
25+
//
26+
27+
/**
28+
* Bind function with code contract
29+
* @param fn Function to bind
30+
* @param contract Code contract
31+
* @returns Bound function
32+
*/
33+
export function codeContract<
34+
T extends FunctionType<ReturnType<T>>,
35+
>(
36+
fn: T,
37+
contract: ContractType<ReturnType<T>> = {},
38+
): T {
39+
if (checkContract === false) {
40+
return fn;
41+
}
42+
return ((...args) => {
43+
const check = requireContractCheck();
44+
if (
45+
check !== false && contract.pre !== undefined &&
46+
contract.pre(...args) === false
47+
) {
48+
const msg = [
49+
"Code Contract: Failed to assert the pre condition.",
50+
`\tfunction: ${fn.name}`,
51+
`\targs: ${JSON.stringify(args)}`,
52+
];
53+
throw new Error(msg.join("\n"));
54+
}
55+
const result = fn(...args);
56+
if (
57+
check !== false && contract.post !== undefined &&
58+
contract.post(result) === false
59+
) {
60+
const msg = [
61+
"Code Contract: Failed to assert the post condition.",
62+
`\tfunction: ${fn.name}`,
63+
`\tresult: ${JSON.stringify(args)}`,
64+
];
65+
throw new Error(msg.join("\n"));
66+
}
67+
if (
68+
check !== false && contract.invariant !== undefined &&
69+
contract.invariant() === false
70+
) {
71+
const msg = [
72+
"Code Contract: Failed to assert the invariant.",
73+
`\tfunction: ${fn.name}`,
74+
];
75+
throw new Error(msg.join("\n"));
76+
}
77+
78+
return result;
79+
}) as T;
80+
}
81+
82+
/**
83+
* Enable contract
84+
*/
85+
export function enableContract() {
86+
checkContract = true;
87+
}
88+
89+
/**
90+
* Disable contract check
91+
*/
92+
export function disableContract() {
93+
checkContract = false;
94+
}
95+
96+
/**
97+
* Require contract check
98+
* @returns Require contract check flag
99+
*/
100+
function requireContractCheck() {
101+
return checkContract;
102+
}

tsconfig.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"compilerOptions": {
3+
"strict": true
4+
}
5+
}

0 commit comments

Comments
 (0)