tests: added initial provider tests.
This commit is contained in:
parent
74f7967be6
commit
d7c6252521
116
src.ts/_tests/create-provider.ts
Normal file
116
src.ts/_tests/create-provider.ts
Normal file
@ -0,0 +1,116 @@
|
||||
import {
|
||||
AlchemyProvider,
|
||||
AnkrProvider,
|
||||
CloudflareProvider,
|
||||
EtherscanProvider,
|
||||
InfuraProvider,
|
||||
// PocketProvider,
|
||||
|
||||
FallbackProvider,
|
||||
} from "../index.js";
|
||||
|
||||
import type { AbstractProvider } from "../index.js";
|
||||
|
||||
interface ProviderCreator {
|
||||
name: string;
|
||||
networks: Array<string>;
|
||||
create: (network: string) => null | AbstractProvider;
|
||||
};
|
||||
|
||||
const ethNetworks = [ "default", "homestead", "rinkeby", "ropsten", "goerli" ];
|
||||
//const maticNetworks = [ "matic", "maticmum" ];
|
||||
|
||||
const ProviderCreators: Array<ProviderCreator> = [
|
||||
{
|
||||
name: "AlchemyProvider",
|
||||
networks: ethNetworks,
|
||||
create: function(network: string) {
|
||||
return new AlchemyProvider(network, "YrPw6SWb20vJDRFkhWq8aKnTQ8JRNRHM");
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "AnkrProvider",
|
||||
networks: ethNetworks.concat([ "matic", "arbitrum" ]),
|
||||
create: function(network: string) {
|
||||
return new AnkrProvider(network);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "CloudflareProvider",
|
||||
networks: [ "default", "homestead" ],
|
||||
create: function(network: string) {
|
||||
return new CloudflareProvider(network);
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "EtherscanProvider",
|
||||
networks: ethNetworks,
|
||||
create: function(network: string) {
|
||||
return new EtherscanProvider(network, "FPFGK6JSW2UHJJ2666FG93KP7WC999MNW7");
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "InfuraProvider",
|
||||
networks: ethNetworks,
|
||||
create: function(network: string) {
|
||||
return new InfuraProvider(network, "49a0efa3aaee4fd99797bfa94d8ce2f1");
|
||||
}
|
||||
},
|
||||
/*
|
||||
{
|
||||
name: "PocketProvider",
|
||||
networks: ethNetworks,
|
||||
create: function(network: string) {
|
||||
const apiKeys: Record<string, string> = {
|
||||
homestead: "6004bcd10040261633ade990",
|
||||
ropsten: "6004bd4d0040261633ade991",
|
||||
rinkeby: "6004bda20040261633ade994",
|
||||
goerli: "6004bd860040261633ade992",
|
||||
};
|
||||
return new PocketProvider(network, apiKeys[network]);
|
||||
}
|
||||
},
|
||||
*/
|
||||
|
||||
{
|
||||
name: "FallbackProvider",
|
||||
networks: ethNetworks,
|
||||
create: function(network: string) {
|
||||
const providers: Array<AbstractProvider> = [];
|
||||
for (const creator of ProviderCreators) {
|
||||
if (creator.name === "FallbackProvider") { continue; }
|
||||
if (creator.networks.indexOf(network) >= 0) {
|
||||
const provider = creator.create(network);
|
||||
if (provider) { providers.push(provider); }
|
||||
}
|
||||
}
|
||||
return new FallbackProvider(providers);
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
export const providerNames = Object.freeze(ProviderCreators.map((c) => (c.name)));
|
||||
|
||||
function getCreator(provider: string): null | ProviderCreator {
|
||||
const creators = ProviderCreators.filter((c) => (c.name === provider));
|
||||
if (creators.length === 1) { return creators[0]; }
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getProviderNetworks(provider: string): Array<string> {
|
||||
const creator = getCreator(provider);
|
||||
if (creator) { return creator.networks; }
|
||||
return [ ];
|
||||
}
|
||||
|
||||
export function getProvider(provider: string, network: string): null | AbstractProvider {
|
||||
const creator = getCreator(provider);
|
||||
if (creator) { return creator.create(network); }
|
||||
return null;
|
||||
}
|
||||
|
||||
export function connect(network: string): AbstractProvider {
|
||||
const provider = getProvider("InfuraProvider", network);
|
||||
if (provider == null) { throw new Error(`could not connect to ${ network }`); }
|
||||
return provider;
|
||||
}
|
20
src.ts/_tests/test-provider-wildcard.ts
Normal file
20
src.ts/_tests/test-provider-wildcard.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import assert from "assert";
|
||||
|
||||
import { connect } from "./create-provider.js";
|
||||
|
||||
describe("Test EIP-2544 ENS wildcards", function() {
|
||||
const provider = connect("ropsten");
|
||||
|
||||
it("Resolves recursively", async function() {
|
||||
const resolver = await provider.getResolver("ricmoose.hatch.eth");
|
||||
assert.ok(resolver, "failed to get resolver");
|
||||
|
||||
assert.equal(resolver.address, "0x8fc4C380c5d539aE631daF3Ca9182b40FB21D1ae", "address");
|
||||
assert.equal(await resolver.supportsWildcard(), true, "supportsWildcard()");
|
||||
|
||||
// Test pass-through avatar
|
||||
assert.equal(await resolver.getAvatar(), "https:/\/static.ricmoo.com/uploads/profile-06cb9c3031c9.jpg", "getAvatar()");
|
||||
|
||||
assert.equal(await resolver.getAddress(), "0x4FaBE0A3a4DDd9968A7b4565184Ad0eFA7BE5411", "getAddress()");
|
||||
});
|
||||
});
|
30
src.ts/_tests/test-providers-avatar.ts
Normal file
30
src.ts/_tests/test-providers-avatar.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import assert from "assert";
|
||||
|
||||
import { connect } from "./create-provider.js";
|
||||
|
||||
describe("Resolve ENS avatar", function() {
|
||||
[
|
||||
{ title: "data", name: "data-avatar.tests.eth", value: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAAECAMAAACeL25MAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyVpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDYuMC1jMDAyIDc5LjE2NDQ4OCwgMjAyMC8wNy8xMC0yMjowNjo1MyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RSZWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZVJlZiMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDIyLjAgKE1hY2ludG9zaCkiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6NUQ4NTEyNUIyOEIwMTFFQzg0NTBDNTU2RDk1NTA5NzgiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6NUQ4NTEyNUMyOEIwMTFFQzg0NTBDNTU2RDk1NTA5NzgiPiA8eG1wTU06RGVyaXZlZEZyb20gc3RSZWY6aW5zdGFuY2VJRD0ieG1wLmlpZDo1RDg1MTI1OTI4QjAxMUVDODQ1MEM1NTZEOTU1MDk3OCIgc3RSZWY6ZG9jdW1lbnRJRD0ieG1wLmRpZDo1RDg1MTI1QTI4QjAxMUVDODQ1MEM1NTZEOTU1MDk3OCIvPiA8L3JkZjpEZXNjcmlwdGlvbj4gPC9yZGY6UkRGPiA8L3g6eG1wbWV0YT4gPD94cGFja2V0IGVuZD0iciI/PkbM0uMAAAAGUExURQAA/wAAAHtivz4AAAAOSURBVHjaYmDABAABBgAAFAABaEkyYwAAAABJRU5ErkJggg==" },
|
||||
{ title: "ipfs", name: "ipfs-avatar.tests.eth", value: "https:/\/gateway.ipfs.io/ipfs/QmQsQgpda6JAYkFoeVcj5iPbwV3xRcvaiXv3bhp1VuYUqw" },
|
||||
{ title: "url", name: "url-avatar.tests.eth", value: "https:/\/ethers.org/static/logo.png" },
|
||||
].forEach((test) => {
|
||||
it(`Resolves avatar for ${ test.title }`, async function() {
|
||||
this.timeout(60000);
|
||||
const provider = connect("ropsten");
|
||||
const avatar = await provider.getAvatar(test.name);
|
||||
assert.equal(test.value, avatar, "avatar url");
|
||||
});
|
||||
});
|
||||
|
||||
[
|
||||
{ title: "ERC-1155", name: "nick.eth", value: "https:/\/lh3.googleusercontent.com/hKHZTZSTmcznonu8I6xcVZio1IF76fq0XmcxnvUykC-FGuVJ75UPdLDlKJsfgVXH9wOSmkyHw0C39VAYtsGyxT7WNybjQ6s3fM3macE" },
|
||||
{ title: "ERC-721", name: "brantly.eth", value: "https:/\/api.wrappedpunks.com/images/punks/2430.png" }
|
||||
].forEach((test) => {
|
||||
it(`Resolves avatar for ${ test.title }`, async function() {
|
||||
this.timeout(60000);
|
||||
const provider = connect("homestead");
|
||||
const avatar = await provider.getAvatar(test.name);
|
||||
assert.equal(avatar, test.value, "avatar url");
|
||||
});
|
||||
});
|
||||
});
|
179
src.ts/_tests/test-providers-ccip.ts
Normal file
179
src.ts/_tests/test-providers-ccip.ts
Normal file
@ -0,0 +1,179 @@
|
||||
import assert from "assert";
|
||||
|
||||
import {
|
||||
concat, dataLength,
|
||||
keccak256,
|
||||
toArray,
|
||||
isCallException, isError
|
||||
} from "../index.js";
|
||||
|
||||
import { connect } from "./create-provider.js";
|
||||
|
||||
describe("Test CCIP execution", function() {
|
||||
|
||||
// This matches the verify method in the Solidity contract against the
|
||||
// processed data from the endpoint
|
||||
const verify = function(sender: string, data: string, result: string): void {
|
||||
const check = concat([
|
||||
toArray(dataLength(sender)), sender,
|
||||
toArray(dataLength(data)), data
|
||||
]);
|
||||
assert.equal(result, keccak256(check), "response is equal");
|
||||
}
|
||||
|
||||
const address = "0xAe375B05A08204C809b3cA67C680765661998886";
|
||||
const calldata = "0x1234";
|
||||
|
||||
it("testGet passes under normal operation", async function() {
|
||||
this.timeout(60000);
|
||||
|
||||
const provider = connect("ropsten");
|
||||
|
||||
// testGet(bytes callData = "0x1234")
|
||||
const tx = {
|
||||
to: address, enableCcipRead: true,
|
||||
data: "0xa5f3271e000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000"
|
||||
}
|
||||
|
||||
const result = await provider.call(tx);
|
||||
verify(address, calldata, result);
|
||||
});
|
||||
|
||||
it("testGet should fail with CCIP not explicitly enabled by overrides", async function() {
|
||||
this.timeout(60000);
|
||||
|
||||
const provider = connect("ropsten");
|
||||
|
||||
// testGet(bytes callData = "0x1234")
|
||||
const tx = {
|
||||
to: address,
|
||||
data: "0xa5f3271e000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000"
|
||||
}
|
||||
|
||||
await assert.rejects(async function() {
|
||||
const result = await provider.call(tx);
|
||||
console.log(result);
|
||||
}, (error: unknown) => {
|
||||
const offchainErrorData = "0x556f1830000000000000000000000000ae375b05a08204c809b3ca67c68076566199888600000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000140b1494be100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004068747470733a2f2f6574686572732e7269636d6f6f2e776f726b6572732e6465762f746573742d636369702d726561642f7b73656e6465727d2f7b646174617d00000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d4d79206578747261206461746100000000000000000000000000000000000000";
|
||||
return (isCallException(error) && error.data === offchainErrorData);
|
||||
});
|
||||
});
|
||||
|
||||
it("testGet should fail with CCIP explicitly disabled on provider", async function() {
|
||||
this.timeout(60000);
|
||||
|
||||
const provider = connect("ropsten");
|
||||
provider.disableCcipRead = true;
|
||||
|
||||
// testGetFail(bytes callData = "0x1234")
|
||||
const tx = {
|
||||
to: address, enableCcipRead: true,
|
||||
data: "0xa5f3271e000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000"
|
||||
}
|
||||
|
||||
await assert.rejects(async function() {
|
||||
const result = await provider.call(tx);
|
||||
console.log(result);
|
||||
}, (error: unknown) => {
|
||||
const offchainErrorData = "0x556f1830000000000000000000000000ae375b05a08204c809b3ca67c68076566199888600000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000140b1494be100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004068747470733a2f2f6574686572732e7269636d6f6f2e776f726b6572732e6465762f746573742d636369702d726561642f7b73656e6465727d2f7b646174617d00000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d4d79206578747261206461746100000000000000000000000000000000000000";
|
||||
return (isCallException(error) && error.data === offchainErrorData);
|
||||
});
|
||||
});
|
||||
|
||||
it("testGetFail should fail if all URLs 5xx", async function() {
|
||||
this.timeout(60000);
|
||||
|
||||
const provider = connect("ropsten");
|
||||
|
||||
// testGetFail(bytes callData = "0x1234")
|
||||
const tx = {
|
||||
to: address, enableCcipRead: true,
|
||||
data: "0x36f9cea6000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000"
|
||||
}
|
||||
|
||||
await assert.rejects(async function() {
|
||||
const result = await provider.call(tx);
|
||||
console.log(result);
|
||||
}, (error: unknown) => {
|
||||
const infoJson = '{"urls":["https:/\/ethers.ricmoo.workers.dev/status/500/{sender}/{data}"],"errorMessages":["hello world"]}';
|
||||
return (isError(error, "OFFCHAIN_FAULT") && error.reason === "500_SERVER_ERROR" &&
|
||||
JSON.stringify(error.info) === infoJson);
|
||||
});
|
||||
});
|
||||
|
||||
it("testGetSenderFail should fail if sender does not match", async function() {
|
||||
this.timeout(60000);
|
||||
|
||||
const provider = connect("ropsten");
|
||||
|
||||
// testGetSenderFail(bytes callData = "0x1234")
|
||||
const tx = {
|
||||
to: address, enableCcipRead: true,
|
||||
data: "0x64bff6d1000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000",
|
||||
}
|
||||
|
||||
await assert.rejects(async function() {
|
||||
const result = await provider.call(tx);
|
||||
console.log(result);
|
||||
}, (error: unknown) => {
|
||||
const errorArgsJson = '["0x0000000000000000000000000000000000000000",["https://ethers.ricmoo.workers.dev/test-ccip-read/{sender}/{data}"],"0x1234","0xb1494be1","0x4d792065787472612064617461"]';
|
||||
const offchainErrorData = "0x556f1830000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000140b1494be100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000004068747470733a2f2f6574686572732e7269636d6f6f2e776f726b6572732e6465762f746573742d636369702d726561642f7b73656e6465727d2f7b646174617d00000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000d4d79206578747261206461746100000000000000000000000000000000000000";
|
||||
return (isCallException(error) && error.data === offchainErrorData &&
|
||||
error.errorSignature === "OffchainLookup(address,string[],bytes,bytes4,bytes)" &&
|
||||
JSON.stringify(error.errorArgs) === errorArgsJson);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("testGetMissing should fail if early URL 4xx", async function() {
|
||||
this.timeout(60000);
|
||||
|
||||
const provider = connect("ropsten");
|
||||
|
||||
// testGetMissing(bytes callData = "0x1234")
|
||||
const tx = {
|
||||
to: address, enableCcipRead: true,
|
||||
data: "0x4ece8d7d000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000"
|
||||
}
|
||||
|
||||
await assert.rejects(async function() {
|
||||
const result = await provider.call(tx);
|
||||
console.log(result);
|
||||
}, (error: unknown) => {
|
||||
const infoJson = '{"url":"https:/\/ethers.ricmoo.workers.dev/status/404/{sender}/{data}","errorMessage":"hello world"}';
|
||||
return (isError(error, "OFFCHAIN_FAULT") && error.reason === "404_MISSING_RESOURCE" &&
|
||||
JSON.stringify(error.info || "") === infoJson);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it("testGetFallback passes if any URL returns correctly", async function() {
|
||||
this.timeout(60000);
|
||||
|
||||
const provider = connect("ropsten");
|
||||
|
||||
// testGetFallback(bytes callData = "0x1234")
|
||||
const tx = {
|
||||
to: address, enableCcipRead: true,
|
||||
data: "0xedf4a021000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000"
|
||||
}
|
||||
|
||||
const result = await provider.call(tx);
|
||||
verify(address, calldata, result);
|
||||
});
|
||||
|
||||
it("testPost passes under normal operation", async function() {
|
||||
this.timeout(60000);
|
||||
|
||||
const provider = connect("ropsten");
|
||||
|
||||
// testPost(bytes callData = "0x1234")
|
||||
const tx = {
|
||||
to: address, enableCcipRead: true,
|
||||
data: "0x66cab49d000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000021234000000000000000000000000000000000000000000000000000000000000"
|
||||
}
|
||||
|
||||
const result = await provider.call(tx);
|
||||
verify(address, calldata, result);
|
||||
});
|
||||
})
|
@ -30,3 +30,97 @@ export function log(context: any, text: string): void {
|
||||
console.log(text);
|
||||
}
|
||||
}
|
||||
|
||||
async function stall(duration: number): Promise<void> {
|
||||
return new Promise((resolve) => { setTimeout(resolve, duration); });
|
||||
}
|
||||
|
||||
export interface MochaRunnable {
|
||||
timeout: (value: number) => void;
|
||||
skip: () => void;
|
||||
}
|
||||
|
||||
const ATTEMPTS = 5;
|
||||
export async function retryIt(name: string, func: (this: MochaRunnable) => Promise<void>): Promise<void> {
|
||||
it(name, async function() {
|
||||
this.timeout(ATTEMPTS * 5000);
|
||||
|
||||
for (let i = 0; i < ATTEMPTS; i++) {
|
||||
try {
|
||||
await func.call(this);
|
||||
return;
|
||||
} catch (error: any) {
|
||||
if (error.message === "sync skip; aborting execution") {
|
||||
// Skipping a test; let mocha handle it
|
||||
throw error;
|
||||
} else if (error.code === "ERR_ASSERTION") {
|
||||
// Assertion error; let mocha scold us
|
||||
throw error;
|
||||
} else {
|
||||
if (i === ATTEMPTS - 1) {
|
||||
stats.pushRetry(i, name, error);
|
||||
} else {
|
||||
await stall(500 * (1 << i));
|
||||
stats.pushRetry(i, name, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// All hope is lost.
|
||||
throw new Error(`Failed after ${ ATTEMPTS } attempts; ${ name }`);
|
||||
});
|
||||
}
|
||||
|
||||
export interface StatSet {
|
||||
name: string;
|
||||
retries: Array<{ message: string, error: null | Error }>;
|
||||
}
|
||||
|
||||
const _guard = { };
|
||||
|
||||
export class Stats {
|
||||
#stats: Array<StatSet>;
|
||||
|
||||
constructor(guard: any) {
|
||||
if (guard !== _guard) { throw new Error("private constructor"); }
|
||||
this.#stats = [ ];
|
||||
}
|
||||
|
||||
#currentStats(): StatSet {
|
||||
if (this.#stats.length === 0) { throw new Error("no active stats"); }
|
||||
return this.#stats[this.#stats.length - 1];
|
||||
}
|
||||
|
||||
pushRetry(attempt: number, line: string, error: null | Error): void {
|
||||
const { retries } = this.#currentStats();
|
||||
|
||||
if (attempt > 0) { retries.pop(); }
|
||||
if (retries.length < 100) {
|
||||
retries.push({
|
||||
message: `${ attempt + 1 } failures: ${ line }`,
|
||||
error
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
start(name: string): void {
|
||||
this.#stats.push({ name, retries: [ ] });
|
||||
}
|
||||
|
||||
end(context?: any): void {
|
||||
let log = console.log.bind(console);
|
||||
if (context && typeof(context._ethersLog) === "function") {
|
||||
log = context._ethersLog;
|
||||
}
|
||||
const { name, retries } = this.#currentStats();
|
||||
if (retries.length === 0) { return; }
|
||||
log(`Warning: The following tests required retries (${ name })`);
|
||||
retries.forEach(({ error, message }) => {
|
||||
log(" " + message);
|
||||
if (error) { log(error); }
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const stats = new Stats(_guard);
|
||||
|
Loading…
Reference in New Issue
Block a user