Refactoring test case generation scripts.
This commit is contained in:
parent
15893537c3
commit
7fcae25a78
18
packages/testcases/generation/package.json
Normal file
18
packages/testcases/generation/package.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "private-generation-scripts",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"description": "Scripts to geenrate test cases.",
|
||||
"main": "index.js",
|
||||
"scripts": {
|
||||
"auto-build": "npm run build -- -w",
|
||||
"build": "tsc --build ./tsconfig.json"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^14.11.8",
|
||||
"eth-sig-util": "^2.5.3",
|
||||
"typescript": "3.8.3"
|
||||
},
|
||||
"author": "Richard Moore <me@ricmoo.com>",
|
||||
"license": "MIT"
|
||||
}
|
144
packages/testcases/generation/src.ts/abi-test.ts
Normal file
144
packages/testcases/generation/src.ts/abi-test.ts
Normal file
@ -0,0 +1,144 @@
|
||||
import { AbstractTest } from "./test";
|
||||
|
||||
export interface AbiType {
|
||||
name: string;
|
||||
type: string;
|
||||
|
||||
struct?: string;
|
||||
components?: Array<AbiType>;
|
||||
|
||||
create(): any;
|
||||
}
|
||||
|
||||
export abstract class AbstractAbiTest<T = any> extends AbstractTest<T> {
|
||||
_nextNames: Record<string, number>;
|
||||
|
||||
constructor(name: string) {
|
||||
super(name);
|
||||
this._nextNames = { };
|
||||
}
|
||||
|
||||
nextName(prefix?: string): string {
|
||||
if (prefix == null) { prefix = "p"; }
|
||||
if (!this._nextNames[prefix]) { this._nextNames[prefix] = 1; }
|
||||
return prefix + (this._nextNames[prefix]++);
|
||||
}
|
||||
|
||||
randomType(dynamicOrType?: boolean | string): AbiType {
|
||||
if (dynamicOrType == null) { dynamicOrType = true; }
|
||||
|
||||
let type: number | string = null;
|
||||
let dynamic = true;
|
||||
if (typeof(dynamicOrType) === "boolean") {
|
||||
dynamic = dynamicOrType;
|
||||
type = this.randomInteger(0, dynamic ? 8: 6);
|
||||
} else {
|
||||
type = dynamicOrType;
|
||||
}
|
||||
|
||||
const name = this.nextName();
|
||||
|
||||
switch (type) {
|
||||
|
||||
// Static
|
||||
|
||||
// address
|
||||
case 0: case "address":
|
||||
return { name, type: "address", create: () => {
|
||||
return this.randomAddress();
|
||||
} };
|
||||
|
||||
// bool
|
||||
case 1: case "bool":
|
||||
return { name, type: "bool", create: () => {
|
||||
return this.randomChoice([ false, true ]);
|
||||
} };
|
||||
|
||||
// intXX and uintXX
|
||||
case 2: case "number": {
|
||||
const signed = this.randomChoice([ false, true ]);
|
||||
const width = this.randomInteger(1, 33);
|
||||
return { name, type: `${ signed ? "": "u" }int${ width * 8 }`, create: () => {
|
||||
const bytes = this.randomBytes(width);
|
||||
let value = BigInt("0x" + bytes.toString("hex"));
|
||||
if (signed && (bytes[0] & 0x80)) {
|
||||
bytes[0] &= ~0x80;
|
||||
value = -BigInt("0x" + bytes.toString("hex"));
|
||||
}
|
||||
return value.toString();
|
||||
} };
|
||||
}
|
||||
|
||||
// bytesXX
|
||||
case 3: case "bytesX": {
|
||||
const width = this.randomInteger(1, 33);
|
||||
return { name, type: `bytes${ width }`, create: () => {
|
||||
return this.randomHexString(width);
|
||||
} };
|
||||
}
|
||||
|
||||
// Static or dynamic nested types
|
||||
|
||||
// Array
|
||||
case 4: case "array": {
|
||||
const baseType = this.randomType(dynamic);
|
||||
|
||||
let length = this.randomInteger(0, 4);
|
||||
if (length == 0) { length = null; }
|
||||
const lengthString = ((length == null) ? "": String(length))
|
||||
|
||||
let struct = undefined;
|
||||
let components = undefined;
|
||||
if (baseType.struct) {
|
||||
struct = `${ baseType.struct }[${ lengthString }]`;
|
||||
components = baseType.components;
|
||||
}
|
||||
|
||||
return { name, components, struct, type: `${ baseType.type }[${ lengthString }]`, create: () => {
|
||||
let l = length;
|
||||
if (l == null) { l = this.randomInteger(0, 4); }
|
||||
|
||||
const result = [ ];
|
||||
for (let i = 0; i < l; i++) {
|
||||
result.push(baseType.create());
|
||||
}
|
||||
return result;
|
||||
} };
|
||||
}
|
||||
|
||||
// Tuple
|
||||
case 5: case "tuple": {
|
||||
const components: Array<AbiType> = [ ];
|
||||
const length = this.randomInteger(1, 5);
|
||||
for (let i = 0; i < length; i++) {
|
||||
components.push(this.randomType(dynamic));
|
||||
}
|
||||
const struct = this.nextName("Struct");
|
||||
const type = `tuple(${ components.map(c => c.type).join(",") })`
|
||||
return { name, struct, type, components, create: () => {
|
||||
const result: Record<string, any> = { };
|
||||
components.forEach((type) => {
|
||||
result[type.name] = type.create();
|
||||
});
|
||||
return result;
|
||||
} };
|
||||
}
|
||||
|
||||
// Dynamic
|
||||
|
||||
// string
|
||||
case 6: case "string":
|
||||
return { name, type: "string", create: () => {
|
||||
return this.randomString(0, 64);
|
||||
} };
|
||||
|
||||
// bytes
|
||||
case 7: case "bytes":
|
||||
return { name, type: "bytes", create: () => {
|
||||
return this.randomHexString(0, 64);
|
||||
} };
|
||||
}
|
||||
|
||||
throw new Error("should not be reached");
|
||||
}
|
||||
}
|
139
packages/testcases/generation/src.ts/eip712-test.ts
Normal file
139
packages/testcases/generation/src.ts/eip712-test.ts
Normal file
@ -0,0 +1,139 @@
|
||||
import { TypedDataUtils } from "eth-sig-util";
|
||||
|
||||
import { AbstractAbiTest, AbiType } from "./abi-test";
|
||||
import { saveTests, TestCase } from "./test";
|
||||
|
||||
function fill(testcase: Partial<TestCase.Eip712>): TestCase.Eip712 {
|
||||
const domainType: Array<{ name: string, type:string }> = [];
|
||||
|
||||
if (testcase.domain.name != null) {
|
||||
domainType.push({ name: "name", type: "string" });
|
||||
}
|
||||
if (testcase.domain.version != null) {
|
||||
domainType.push({ name: "version", type: "string" });
|
||||
}
|
||||
if (testcase.domain.chainId != null) {
|
||||
domainType.push({ name: "chainId", type: "uint256" });
|
||||
}
|
||||
if (testcase.domain.verifyingContract != null) {
|
||||
domainType.push({ name: "verifyingContract", type: "address" });
|
||||
}
|
||||
if (testcase.domain.salt != null) {
|
||||
domainType.push({ name: "salt", type: "bytes32" });
|
||||
}
|
||||
|
||||
const typesWithDomain: Record<string, Array<{ name: string, type: string }>> = {
|
||||
EIP712Domain: domainType
|
||||
};
|
||||
for (const key in testcase.types) { typesWithDomain[key] = testcase.types[key]; }
|
||||
|
||||
|
||||
testcase.encoded = "0x" + TypedDataUtils.encodeData(testcase.primaryType, testcase.data, testcase.types).toString("hex");
|
||||
testcase.digest = "0x" + TypedDataUtils.sign({
|
||||
types: <any>typesWithDomain,
|
||||
domain: testcase.domain,
|
||||
primaryType: testcase.primaryType,
|
||||
message: testcase.data
|
||||
}, true).toString("hex");
|
||||
|
||||
return <TestCase.Eip712>testcase;
|
||||
}
|
||||
|
||||
export class Eip712Test extends AbstractAbiTest<TestCase.Eip712> {
|
||||
generateTest(): TestCase.Eip712 {
|
||||
const type = this.randomType("tuple");
|
||||
|
||||
const types: Record<string, Array<{ name: string, type: string }>> = { };
|
||||
function spelunk(type: AbiType): void {
|
||||
if (type.struct) {
|
||||
types[type.struct.split("[")[0]] = type.components.map((t) => {
|
||||
spelunk(t);
|
||||
return { name: t.name, type: (t.struct || t.type) };
|
||||
});;
|
||||
}
|
||||
}
|
||||
spelunk(type);
|
||||
|
||||
const primaryType = type.struct;
|
||||
const data = type.create();
|
||||
|
||||
const domain: any = { };
|
||||
if (this.randomChoice([ false, true])) {
|
||||
domain.name = this.randomString(1, 64);
|
||||
}
|
||||
if (this.randomChoice([ false, true])) {
|
||||
domain.version = [
|
||||
this.randomInteger(0, 50),
|
||||
this.randomInteger(0, 50),
|
||||
this.randomInteger(0, 50),
|
||||
].join(".");
|
||||
}
|
||||
if (this.randomChoice([ false, true])) {
|
||||
domain.chainId = this.randomInteger(0, 1337);
|
||||
}
|
||||
if (this.randomChoice([ false, true])) {
|
||||
domain.verifyingContract = this.randomAddress();
|
||||
}
|
||||
if (this.randomChoice([ false, true])) {
|
||||
domain.salt = this.randomHexString(32);
|
||||
}
|
||||
|
||||
return fill({
|
||||
domain,
|
||||
type: type.type,
|
||||
seed: this.seed,
|
||||
primaryType, types, data
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
const tests: Array<TestCase.Eip712> = [ ];
|
||||
|
||||
for (let i = 0; i < 1024; i++) {
|
||||
const test = new Eip712Test(String(i));
|
||||
tests.push(test.generateTest());
|
||||
}
|
||||
|
||||
tests.sort((a, b) => (a.type.length - b.type.length));
|
||||
tests.forEach((t, i) => { t.name = `random-${ i }`; });
|
||||
|
||||
tests.push({
|
||||
name: "EIP712 example",
|
||||
domain: {
|
||||
name: 'Ether Mail',
|
||||
version: '1',
|
||||
chainId: 1,
|
||||
verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC'
|
||||
},
|
||||
primaryType: "Mail",
|
||||
types: {
|
||||
Person: [
|
||||
{ name: 'name', type: 'string' },
|
||||
{ name: 'wallet', type: 'address' }
|
||||
],
|
||||
Mail: [
|
||||
{ name: 'from', type: 'Person' },
|
||||
{ name: 'to', type: 'Person' },
|
||||
{ name: 'contents', type: 'string' }
|
||||
]
|
||||
},
|
||||
data: {
|
||||
from: {
|
||||
name: 'Cow',
|
||||
wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826'
|
||||
},
|
||||
to: {
|
||||
name: 'Bob',
|
||||
wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB'
|
||||
},
|
||||
contents: 'Hello, Bob!'
|
||||
},
|
||||
encoded: "0xa0cedeb2dc280ba39b857546d74f5549c3a1d7bdc2dd96bf881f76108e23dac2fc71e5fa27ff56c350aa531bc129ebdf613b772b6604664f5d8dbe21b85eb0c8cd54f074a4af31b4411ff6a60c9719dbd559c221c8ac3492d9d872b041d703d1b5aadf3154a261abdd9086fc627b61efca26ae5702701d05cd2305f7c52a2fc8",
|
||||
digest: "0xbe609aee343fb3c4b28e1df9e632fca64fcfaede20f02e86244efddf30957bd2",
|
||||
privateKey: "0xc85ef7d79691fe79573b1a7064c19c1a9819ebdbd1faaab1a8ec92344438aaf4",
|
||||
signature: "0x4355c47d63924e8a72e509b65029052eb6c299d53a04e167c5775fd466751c9d07299936d304c153f6443dfa05f40ff007d72911b6f72307f996231605b915621c"
|
||||
});
|
||||
|
||||
saveTests("eip712", tests);
|
||||
}
|
91
packages/testcases/generation/src.ts/test.ts
Normal file
91
packages/testcases/generation/src.ts/test.ts
Normal file
@ -0,0 +1,91 @@
|
||||
import { createHash } from "crypto";
|
||||
import { saveTests as _saveTests } from "../../lib/index";
|
||||
import { ethers } from "../../../ethers";
|
||||
|
||||
import * as TestCase from "../../lib/testcases";
|
||||
export { TestCase };
|
||||
|
||||
function sha256(value: Buffer): Buffer {
|
||||
return createHash("sha256").update(value).digest();
|
||||
}
|
||||
|
||||
const words = "lorem ipsum dolor sit amet consectetur adipiscing elit sed do eiusmod tempor incididunt ut labore et dolore magna aliqua ut enim ad minim veniam quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur excepteur sint occaecat cupidatat non proident sunt in culpa qui officia deserunt mollit anim id est laborum".split(" ");
|
||||
|
||||
export abstract class AbstractTest<T = any> {
|
||||
readonly seed: string;
|
||||
_seed: Buffer;
|
||||
|
||||
constructor(seed: string) {
|
||||
this.seed = seed;
|
||||
this._seed = sha256(Buffer.from(seed));
|
||||
}
|
||||
|
||||
_nextWord(): Buffer {
|
||||
const result = this._seed;
|
||||
this._seed = sha256(this._seed);
|
||||
return result;
|
||||
}
|
||||
|
||||
randomBytes(lower: number, upper?: number): Buffer {
|
||||
if (!upper) { upper = lower; }
|
||||
|
||||
if (upper === 0 && upper === lower) { return Buffer.alloc(0); }
|
||||
|
||||
let result = this._nextWord();
|
||||
while (result.length < upper) {
|
||||
result = Buffer.concat([ result, this._nextWord() ]);
|
||||
}
|
||||
|
||||
const top = this._nextWord();
|
||||
const percent = ((top[0] << 16) | (top[1] << 8) | top[2]) / 0x01000000;
|
||||
|
||||
return result.slice(0, lower + Math.floor((upper - lower) * percent));
|
||||
}
|
||||
|
||||
randomFloat(): number {
|
||||
const top = this._nextWord();
|
||||
return ((top[0] << 16) | (top[1] << 8) | top[2]) / 0x01000000;
|
||||
}
|
||||
|
||||
randomInteger(lower: number, upper: number): number {
|
||||
return lower + Math.floor((upper - lower) * this.randomFloat());
|
||||
}
|
||||
|
||||
randomChoice<T>(choice: Array<T>): T {
|
||||
return choice[this.randomInteger(0, choice.length)];
|
||||
}
|
||||
|
||||
randomAddress(): string {
|
||||
while (true) {
|
||||
const address = this.randomHexString(20);
|
||||
if (address.match(/[a-f]/i)) {
|
||||
return ethers.utils.getAddress(address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
randomHexString(lower: number, upper?: number): string {
|
||||
return "0x" + this.randomBytes(lower, upper).toString("hex");
|
||||
}
|
||||
|
||||
randomString(lower: number, upper?: number): string {
|
||||
if (!upper) { upper = lower; }
|
||||
if (upper === 0 && upper === lower) { return ""; }
|
||||
|
||||
const length = this.randomInteger(lower, upper);
|
||||
|
||||
let result = "";
|
||||
while (result.length < length + 1) {
|
||||
result += this.randomChoice(words) + " ";
|
||||
}
|
||||
|
||||
return result.substring(0, length);
|
||||
}
|
||||
|
||||
abstract generateTest(): T;
|
||||
}
|
||||
|
||||
export function saveTests(tag: string, tests: Array<any>): void {
|
||||
// @TODO : copy defn files over for testcase.ts and testcase.d.ts
|
||||
_saveTests(tag, tests);
|
||||
}
|
1
packages/testcases/generation/thirdparty.d.ts
vendored
Normal file
1
packages/testcases/generation/thirdparty.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
|
31
packages/testcases/generation/tsconfig.json
Normal file
31
packages/testcases/generation/tsconfig.json
Normal file
@ -0,0 +1,31 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"lib": ["es2015", "es5", "dom"],
|
||||
"module": "commonjs",
|
||||
"target": "es2015",
|
||||
"moduleResolution": "node",
|
||||
|
||||
"rootDir": "./src.ts",
|
||||
"outDir": "./lib/",
|
||||
|
||||
"declaration": true,
|
||||
|
||||
"preserveSymlinks": true,
|
||||
|
||||
"preserveWatchOutput": true,
|
||||
"pretty": false,
|
||||
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitReturns": true,
|
||||
"noUnusedLocals": true
|
||||
},
|
||||
"include": [
|
||||
"./thirdparty.d.ts",
|
||||
"./src.ts/*.ts"
|
||||
],
|
||||
"exclude": [ ]
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user