Refactored provider test cases to more reliable CI.

This commit is contained in:
Richard Moore 2020-09-16 19:38:01 -04:00
parent 86e0269a86
commit de8a717b4c
No known key found for this signature in database
GPG Key ID: 665176BE8E9DC651

View File

@ -8,8 +8,6 @@ import { ethers } from "ethers";
const bnify = ethers.BigNumber.from;
type Dictionary = { [ key: string ]: any };
type TestCases = {
addresses: Array<any>;
blocks: Array<any>;
@ -70,7 +68,7 @@ const blockchainData: { [ network: string ]: TestCases } = {
s: "0x269c3e5b3558267ad91b0a887d51f9f10098771c67b82ea6cb74f29638754f54",
v: 38,
creates: null,
raw: "0xf8d2808504a817c8008303d090946fc21092da55b392b045ed78f4732bff3c580e2c880186cc6acd4b0000b864f2c298be000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000067269636d6f6f000000000000000000000000000000000000000000000000000026a01e5605197a03e3f0a168f14749168dfeefc44c9228312dacbffdcbbb13263265a0269c3e5b3558267ad91b0a887d51f9f10098771c67b82ea6cb74f29638754f54",
//raw: "0xf8d2808504a817c8008303d090946fc21092da55b392b045ed78f4732bff3c580e2c880186cc6acd4b0000b864f2c298be000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000067269636d6f6f000000000000000000000000000000000000000000000000000026a01e5605197a03e3f0a168f14749168dfeefc44c9228312dacbffdcbbb13263265a0269c3e5b3558267ad91b0a887d51f9f10098771c67b82ea6cb74f29638754f54",
chainId: 1
}
],
@ -375,6 +373,8 @@ blockchainData["default"] = blockchainData.homestead;
function equals(name: string, actual: any, expected: any): void {
if (expected && expected.eq) {
if (actual == null) { assert.ok(false, name + " - actual big number null"); }
expected = ethers.BigNumber.from(expected);
actual = ethers.BigNumber.from(actual);
assert.ok(expected.eq(actual), name + " matches");
} else if (Array.isArray(expected)) {
@ -383,6 +383,7 @@ function equals(name: string, actual: any, expected: any): void {
for (let i = 0; i < expected.length; i++) {
equals("(" + name + " - item " + i + ")", actual[i], expected[i]);
}
} else if (typeof(expected) === "object") {
if (actual == null) {
if (expected === actual) { return; }
@ -405,35 +406,131 @@ function equals(name: string, actual: any, expected: any): void {
function waiter(duration: number): Promise<void> {
return new Promise((resolve) => {
setTimeout(resolve, duration);
const timer = setTimeout(resolve, duration);
if (timer.unref) { timer.unref(); }
});
}
function testProvider(providerName: string, networkName: string) {
// Delay (ms) after each test case to prevent the backends from throttling
const delay = 1000;
type ProviderDescription = {
name: string;
networks: Array<string>;
create: (network: string) => ethers.providers.Provider;
};
describe(("Read-Only " + providerName + " (" + networkName + ")"), function() {
this.retries(3);
type CheckSkipFunc = (provider: string, network: string, test: TestDescription) => boolean;
// Get the Provider based on the name of the provider we are testing and the network
let provider: ethers.providers.Provider = null;
if (networkName === "default") {
if (providerName === "getDefaultProvider") {
provider = ethers.getDefaultProvider();
} else {
provider = new (<any>(ethers.providers))[providerName]();
type TestDescription = {
name: string;
networks: Array<string>;
execute: (provider: ethers.providers.Provider) => Promise<void>;
attempts?: number;
timeout?: number;
extras?: Array<"nowait" | "funding">;
checkSkip?: CheckSkipFunc;
};
const allNetworks = [ "default", "homestead", "ropsten", "rinkeby", "kovan", "goerli" ];
const providerFunctions: Array<ProviderDescription> = [
{
name: "getDefaultProvider",
networks: allNetworks,
create: (network: string) => {
if (network == "default") {
return ethers.getDefaultProvider();
}
} else {
if (providerName === "getDefaultProvider") {
provider = ethers.getDefaultProvider(networkName);
} else {
provider = new (<any>(ethers.providers))[providerName](networkName);
return ethers.getDefaultProvider(network);
}
},
{
name: "AlchemyProvider",
networks: allNetworks,
create: (network: string) => {
if (network == "default") {
return new ethers.providers.AlchemyProvider();
}
return new ethers.providers.AlchemyProvider(network);
}
},
{
name: "CloudflareProvider",
networks: [ "homestead" ],
create: (network: string) => {
return new ethers.providers.AlchemyProvider(network);
}
},
{
name: "InfuraProvider",
networks: allNetworks,
create: (network: string) => {
if (network == "default") {
return new ethers.providers.InfuraProvider();
}
return new ethers.providers.InfuraProvider(network);
}
},
{
name: "EtherscanProvider",
networks: allNetworks,
create: (network: string) => {
if (network == "default") {
return new ethers.providers.EtherscanProvider();
}
return new ethers.providers.EtherscanProvider(network);
}
},
{
name: "NodesmithProvider",
networks: [ ],
create: (network: string) => {
throw new Error("not tested");
}
},
{
name: "Web3Provider",
networks: [ ],
create: (network: string) => {
throw new Error("not tested");
}
}
];
const tests: TestCases = blockchainData[networkName];
// This wallet can be funded and used for various test cases
const fundWallet = ethers.Wallet.createRandom();
const testFunctions: Array<TestDescription> = [ ];
Object.keys(blockchainData).forEach((network) => {
function addSimpleTest(name: string, func: (provider: ethers.providers.Provider) => Promise<any>, expected: any) {
testFunctions.push({
name: name,
networks: [ network ],
execute: async (provider: ethers.providers.Provider) => {
const value = await func(provider);
equals(name, expected, value);
}
});
}
function addObjectTest(name: string, func: (provider: ethers.providers.Provider) => Promise<any>, expected: any, checkSkip?: CheckSkipFunc) {
testFunctions.push({
name,
networks: [ network ],
checkSkip,
execute: async (provider: ethers.providers.Provider) => {
const value = await func(provider);
Object.keys(expected).forEach((key) => {
equals(`${ name }.${ key }`, value[key], expected[key]);
});
}
});
}
const tests: TestCases = blockchainData[network];
// And address test case can have any of the following:
// - balance
@ -442,82 +539,49 @@ function testProvider(providerName: string, networkName: string) {
// - ENS name
tests.addresses.forEach((test) => {
if (test.balance) {
it(`fetches address balance: ${ test.address }`, function() {
// Note: These tests could be fiddled with if someone sends ether
// to our address; we just have to live with jerks sending us
// money. *smile emoji*
this.timeout(60000);
return provider.getBalance(test.address).then((balance) => {
equals("Balance", test.balance, balance);
return waiter(delay);
});
});
addSimpleTest(`fetches account balance: ${ test.address }`, (provider: ethers.providers.Provider) => {
return provider.getBalance(test.address);
}, test.balance);
}
if (test.code) {
it(`fetches address code: ${ test.address }`, function() {
this.timeout(60000);
return provider.getCode(test.address).then((code) => {
equals("Code", test.code, code);
return waiter(delay);
});
});
addSimpleTest(`fetches account code: ${ test.address }`, (provider: ethers.providers.Provider) => {
return provider.getCode(test.address);
}, test.code);
}
if (test.storage) {
Object.keys(test.storage).forEach((position) => {
it(`fetches storage: ${ test.address }:${ position }`, function() {
this.timeout(60000);
return provider.getStorageAt(test.address, bnify(position)).then((value) => {
equals("Storage", test.storage[position], value);
return waiter(delay);
});
});
addSimpleTest(`fetches storage: ${ test.address }:${ position }`, (provider: ethers.providers.Provider) => {
return provider.getStorageAt(test.address, bnify(position));
}, test.storage[position]);
});
}
if (test.name) {
it(`fetches the ENS name: ${ test.name }`, function() {
this.timeout(60000);
return provider.resolveName(test.name).then((address) => {
equals("ENS Name", test.address, address);
return waiter(delay);
});
});
addSimpleTest(`fetches ENS name: ${ test.address }`, (provider: ethers.providers.Provider) => {
return provider.resolveName(test.name);
}, test.address);
}
});
tests.blocks.forEach((test) => {
function checkBlock(promise: Promise<any>): Promise<any> {
return promise.then((block) => {
for (let key in test) {
equals("Block " + key, block[key], test[key]);
}
return waiter(delay);
});
}
it(`fetches block (by number) #${ test.number }`, function() {
this.timeout(60000);
return checkBlock(provider.getBlock(test.number));
addObjectTest(`fetches block (by number) #${ test.number }`, (provider: ethers.providers.Provider) => {
return provider.getBlock(test.number);
}, test);
});
// Etherscan does not support getBlockByBlockhash... *sad emoji*
if (providerName === "EtherscanProvider") {
return;
}
it(`fetches block (by hash) ${ test.hash }`, function() {
this.timeout(60000);
return checkBlock(provider.getBlock(test.hash));
tests.blocks.forEach((test) => {
addObjectTest(`fetches block (by hash) ${ test.hash }`, (provider: ethers.providers.Provider) => {
return provider.getBlock(test.hash);
}, test, (provider: string, network: string, test: TestDescription) => {
return (provider === "EtherscanProvider");
});
});
tests.transactions.forEach((test) => {
function testTransaction(expected: Dictionary): Promise<void> {
const title = ("Transaction " + expected.hash.substring(0, 10) + " - ");
return provider.getTransaction(expected.hash).then((tx) => {
addObjectTest(`fetches transaction ${ test.hash }`, async (provider: ethers.providers.Provider) => {
const tx = await provider.getTransaction(test.hash);
// This changes with every block
assert.equal(typeof(tx.confirmations), "number", "confirmations is a number");
@ -526,62 +590,52 @@ function testProvider(providerName: string, networkName: string) {
assert.equal(typeof(tx.wait), "function", "wait is a function");
delete tx.wait
for (const key in tx) {
equals((title + key), (<any>tx)[key], expected[key]);
}
return waiter(delay);
});
}
it(`fetches transaction: ${ test.hash }`, function() {
this.timeout(60000);
return testTransaction(test);
return tx;
}, test, (provider: string, network: string, test: TestDescription) => {
return (provider === "EtherscanProvider");
});
});
tests.transactionReceipts.forEach((test) => {
function testTransactionReceipt(expected: Dictionary): Promise<void> {
const title = ("Receipt " + expected.transactionHash.substring(0, 10) + " - ");
return provider.getTransactionReceipt(expected.transactionHash).then(function(receipt) {
addObjectTest(`fetches transaction receipt ${ test.transactionHash }`, async (provider: ethers.providers.Provider) => {
const receipt = await provider.getTransactionReceipt(test.transactionHash);
if (test.status === null) {
assert.ok(receipt.status === undefined, "no status");
receipt.status = null;
}
// This changes with every block; so just make sure it is a number
assert.equal(typeof(receipt.confirmations), "number", "confirmations is a number");
delete receipt.confirmations;
for (const key in receipt) {
equals((title + key), (<any>receipt)[key], expected[key]);
}
//equals(("Receipt " + expected.transactionHash.substring(0, 10)), receipt, expected);
return waiter(delay);
return receipt;
}, test);
});
}
it(`fetches transaction receipt: ${ test.transactionHash }`, function() {
this.timeout(60000);
return testTransactionReceipt(test);
});
});
if (networkName === "ropsten") {
it("throws correct NONCE_EXPIRED errors", async function() {
this.timeout(60000);
});
(function() {
function addErrorTest(code: string, func: (provider: ethers.providers.Provider) => Promise<any>) {
testFunctions.push({
name: `throws correct ${ code } error`,
networks: [ "ropsten" ],
execute: async (provider: ethers.providers.Provider) => {
try {
const tx = await provider.sendTransaction("0xf86480850218711a0082520894000000000000000000000000000000000000000002801ba038aaddcaaae7d3fa066dfd6f196c8348e1bb210f2c121d36cb2c24ef20cea1fba008ae378075d3cd75aae99ab75a70da82161dffb2c8263dabc5d8adecfa9447fa");
console.log(tx);
assert.ok(false);
const value = await func(provider);
console.log(value);
assert.ok(false, "did not throw");
} catch (error) {
assert.equal(error.code, ethers.utils.Logger.errors.NONCE_EXPIRED);
assert.equal(error.code, code, "incorrect error thrown");
}
}
});
}
await waiter(delay);
addErrorTest(ethers.utils.Logger.errors.NONCE_EXPIRED, async (provider: ethers.providers.Provider) => {
return provider.sendTransaction("0xf86480850218711a0082520894000000000000000000000000000000000000000002801ba038aaddcaaae7d3fa066dfd6f196c8348e1bb210f2c121d36cb2c24ef20cea1fba008ae378075d3cd75aae99ab75a70da82161dffb2c8263dabc5d8adecfa9447fa");
});
it("throws correct INSUFFICIENT_FUNDS errors", async function() {
this.timeout(60000);
addErrorTest(ethers.utils.Logger.errors.INSUFFICIENT_FUNDS, async (provider: ethers.providers.Provider) => {
const txProps = {
to: "0x8ba1f109551bD432803012645Ac136ddd64DBA72",
@ -592,20 +646,10 @@ function testProvider(providerName: string, networkName: string) {
const wallet = ethers.Wallet.createRandom();
const tx = await wallet.signTransaction(txProps);
try {
await provider.sendTransaction(tx);
assert.ok(false);
} catch (error) {
assert.equal(error.code, ethers.utils.Logger.errors.INSUFFICIENT_FUNDS);
}
await waiter(delay);
return provider.sendTransaction(tx);
});
it("throws correct INSUFFICIENT_FUNDS errors (signer)", async function() {
this.timeout(60000);
addErrorTest(ethers.utils.Logger.errors.INSUFFICIENT_FUNDS, async (provider: ethers.providers.Provider) => {
const txProps = {
to: "0x8ba1f109551bD432803012645Ac136ddd64DBA72",
gasPrice: 9000000000,
@ -614,89 +658,119 @@ function testProvider(providerName: string, networkName: string) {
};
const wallet = ethers.Wallet.createRandom().connect(provider);
try {
await wallet.sendTransaction(txProps);
assert.ok(false);
} catch (error) {
assert.equal(error.code, ethers.utils.Logger.errors.INSUFFICIENT_FUNDS);
}
await waiter(delay);
return wallet.sendTransaction(txProps);
});
it("throws correct UNPREDICTABLE_GAS_LIMIT errors", async function() {
this.timeout(60000);
try {
await provider.estimateGas({
to: "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e" // ENS; no payable fallback
addErrorTest(ethers.utils.Logger.errors.UNPREDICTABLE_GAS_LIMIT, async (provider: ethers.providers.Provider) => {
return provider.estimateGas({
to: "0x00000000000C2E074eC69A0dFb2997BA6C7d2e1e" // ENS contract
});
assert.ok(false);
} catch (error) {
assert.equal(error.code, ethers.utils.Logger.errors.UNPREDICTABLE_GAS_LIMIT);
}
await waiter(delay);
});
})();
it("sends a transaction", async function() {
this.timeout(360000);
const wallet = ethers.Wallet.createRandom().connect(provider);
const funder = await ethers.utils.fetchJson(`https:/\/api.ethers.io/api/v1/?action=fundAccount&address=${ wallet.address.toLowerCase() }`);
await provider.waitForTransaction(funder.hash);
testFunctions.push({
name: "sends a transaction",
extras: [ "funding" ], // We need funding to the funWallet
timeout: 300, // 5 minutes
networks: [ "ropsten" ], // Only test on Ropsten
execute: async (provider: ethers.providers.Provider) => {
const wallet = fundWallet.connect(provider);
const addr = "0x8210357f377E901f18E45294e86a2A32215Cc3C9";
const gasPrice = 9000000000;
let balance = await provider.getBalance(wallet.address);
assert.ok(balance.eq(ethers.utils.parseEther("3.141592653589793238")), "balance is pi after funding");
const b0 = await provider.getBalance(wallet.address);
assert.ok(b0.gt(ethers.constants.Zero), "balance is non-zero");
const tx = await wallet.sendTransaction({
to: addr,
gasPrice: gasPrice,
value: balance.sub(21000 * gasPrice)
value: 123
});
await tx.wait();
balance = await provider.getBalance(wallet.address);
assert.ok(balance.eq(ethers.constants.Zero), "balance is zero after after sweeping");
await waiter(delay);
});
const b1 = await provider.getBalance(wallet.address);
assert.ok(b0.gt(b1), "balance is decreased");
}
});
describe("Test Provider Methods", function() {
let fundReceipt: Promise<ethers.providers.TransactionReceipt> = null;
const faucet = "0x8210357f377E901f18E45294e86a2A32215Cc3C9";
// Obviously many more cases to add here
// - getTransactionCount
// - getBlockNumber
// - getGasPrice
// - estimateGas
// - sendTransaction
// - call
// - getLogs
//
// Many of these are tLegacyParametersested in run-providers, which uses nodeunit, but
// also creates a local private key which must then be funded to
// execute the tests. I am working on a better test contract to deploy
// to all the networks to help test these.
before(async function() {
// Get some ether from the faucet
const provider = ethers.getDefaultProvider("ropsten");
const funder = await ethers.utils.fetchJson(`https:/\/api.ethers.io/api/v1/?action=fundAccount&address=${ fundWallet.address.toLowerCase() }`);
fundReceipt = provider.waitForTransaction(funder.hash);
fundReceipt.then((receipt) => {
console.log(`*** Funded: ${ fundWallet.address }`);
});
});
}
["default", "homestead", "ropsten", "rinkeby", "kovan", "goerli"].forEach(function(networkName) {
["getDefaultProvider", "AlchemyProvider", "CloudflareProvider", "InfuraProvider", "EtherscanProvider", "NodesmithProvider", "Web3Provider"].forEach(function(providerName) {
if (providerName === "NodesmithProvider") { return; }
if (providerName === "CloudflareProvider") { return; }
if (providerName === "Web3Provider") { return; }
after(async function() {
// Wait until the funding is complete
await fundReceipt;
if ((networkName !== "homestead" && networkName !== "default") && providerName === "CloudflareProvider") {
// Refund all unused ether to the faucet
const provider = ethers.getDefaultProvider("ropsten");
const gasPrice = await provider.getGasPrice();
const balance = await provider.getBalance(fundWallet.address);
fundWallet.connect(provider).sendTransaction({
to: faucet,
gasLimit: 21000,
gasPrice: gasPrice,
value: balance.sub(gasPrice.mul(21000))
});
});
providerFunctions.forEach(({ name, networks, create}) => {
networks.forEach((network) => {
const provider = create(network);
testFunctions.forEach((test) => {
// Skip tests not supported on this network
if (test.networks.indexOf(network) === -1) { return; }
if (test.checkSkip && test.checkSkip(name, network, test)) {
return;
}
testProvider(providerName, networkName);
// How many attempts to try?
const attempts = (test.attempts != null) ? test.attempts: 3;
const timeout = (test.timeout != null) ? test.timeout: 60;
const extras = (test.extras || []).reduce((accum, key) => {
accum[key] = true;
return accum;
}, <{ [ key: string ]: boolean }>{ });
it(`${ name }.${ network ? network: "default" } ${ test.name}`, async function() {
this.timeout(timeout * 1000 * attempts);
// Wait for the funding transaction to be mined
if (extras.funding) { await fundReceipt; }
// We wait at least 1 seconds between tests
if (!extras.nowait) { await waiter(1000); }
let error: Error = null;
for (let attempt = 0; attempt < attempts; attempt++) {
try {
return await Promise.race([
test.execute(provider),
waiter(timeout * 1000).then((resolve) => { throw new Error("timeout"); })
]);
} catch (attemptError) {
console.log(`*** Failed attempt ${ attempt + 1 }: ${ attemptError.message }`);
error = attemptError;
}
}
throw error;
});
});
});
});
});
/*
@ -720,7 +794,7 @@ describe("Test extra Etherscan operations", function() {
*/
describe("Test Basic Authentication", function() {
this.retries(3);
//this.retries(3);
// https://stackoverflow.com/questions/6509278/authentication-test-servers#16756383