Skip to content

Commit

Permalink
test: Coverage more @hyperlane-xyz/utils test (#4758)
Browse files Browse the repository at this point in the history
### Description
Add more coverage `utils` package test
<!--
What's included in this PR?
-->

### Drive-by changes

<!--
Are there any minor or drive-by changes also included?
-->

### Related issues

<!--
- Fixes #[issue number here]
-->

### Backward compatibility

<!--
Are these changes backward compatible? Are there any infrastructure
implications, e.g. changes that would prohibit deploying older commits
using this infra tooling?

Yes/No
-->

### Testing

<!--
What kind of testing have these changes undergone?

None/Manual/Unit Tests
-->
More unittests function
  • Loading branch information
tiendn authored Oct 28, 2024
1 parent 5dabdf3 commit c622bfb
Show file tree
Hide file tree
Showing 17 changed files with 736 additions and 1 deletion.
3 changes: 3 additions & 0 deletions typescript/utils/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,12 @@
"devDependencies": {
"@types/lodash-es": "^4.17.12",
"@types/mocha": "^10.0.1",
"@types/sinon": "^17.0.1",
"@types/sinon-chai": "^3.2.12",
"chai": "4.5.0",
"mocha": "^10.2.0",
"prettier": "^2.8.8",
"sinon": "^13.0.2",
"typescript": "5.3.3"
},
"homepage": "https://www.hyperlane.xyz",
Expand Down
54 changes: 54 additions & 0 deletions typescript/utils/src/arrays.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { expect } from 'chai';

import { chunk, exclude, randomElement } from './arrays.js';

describe('Arrays utilities', () => {
describe('chunk', () => {
it('should split an array into chunks of the specified size', () => {
const result = chunk([1, 2, 3, 4, 5], 2);
expect(result).to.deep.equal([[1, 2], [3, 4], [5]]);
});

it('should return an empty array when input is empty', () => {
const result = chunk([], 2);
expect(result).to.deep.equal([]);
});

it('should handle chunk size larger than array length', () => {
const result = chunk([1, 2], 5);
expect(result).to.deep.equal([[1, 2]]);
});
});

describe('exclude', () => {
it('should exclude the specified item from the list', () => {
const result = exclude(2, [1, 2, 3, 2]);
expect(result).to.deep.equal([1, 3]);
});

it('should return the same list if item is not found', () => {
const result = exclude(4, [1, 2, 3]);
expect(result).to.deep.equal([1, 2, 3]);
});

it('should return an empty list if all items are excluded', () => {
const result = exclude(1, [1, 1, 1]);
expect(result).to.deep.equal([]);
});
});

describe('randomElement', () => {
beforeEach(() => {});

it('should return a random element from the list', () => {
const list = [10, 20, 30];
const result = randomElement(list);
expect(result).to.be.oneOf(list);
});

it('should handle an empty list gracefully', () => {
const result = randomElement([]);
expect(result).to.be.undefined;
});
});
});
146 changes: 146 additions & 0 deletions typescript/utils/src/async.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { expect } from 'chai';

import {
concurrentMap,
fetchWithTimeout,
pollAsync,
raceWithContext,
retryAsync,
runWithTimeout,
sleep,
timeout,
} from './async.js';

describe('Async Utilities', () => {
describe('sleep', () => {
it('should resolve after sleep duration', async () => {
const start = Date.now();
await sleep(100);
const duration = Date.now() - start;
expect(duration).to.be.at.least(100);
expect(duration).to.be.lessThan(200);
});
});

describe('timeout', () => {
it('should timeout a promise', async () => {
const promise = new Promise((resolve) => setTimeout(resolve, 200));
try {
await timeout(promise, 100);
throw new Error('Expected timeout error');
} catch (error: any) {
expect(error.message).to.equal('Timeout reached');
}
});
});

describe('runWithTimeout', () => {
it('should run a callback with a timeout', async () => {
const result = await runWithTimeout(100, async () => {
await sleep(50);
return 'success';
});
expect(result).to.equal('success');
});
});

describe('fetchWithTimeout', () => {
it('should fetch with timeout', async () => {
// Mock fetch for testing
global.fetch = async () => {
await sleep(50);
return new Response('ok');
};

const response = await fetchWithTimeout('https://example.com', {}, 100);
expect(await response.text()).to.equal('ok');
});
});

describe('retryAsync', () => {
it('should retry async function with exponential backoff', async () => {
let attempt = 0;
const runner = async () => {
attempt++;
if (attempt < 3) throw new Error('fail');
return 'success';
};

const result = await retryAsync(runner, 5, 10);
expect(result).to.equal('success');
});
});

describe('pollAsync', () => {
it('should poll async function until success', async () => {
let attempt = 0;
const runner = async () => {
attempt++;
if (attempt < 3) throw new Error('fail');
return 'success';
};

const result = await pollAsync(runner, 10, 5);
expect(result).to.equal('success');
});

it('should fail after reaching max retries', async () => {
let attempt = 0;
const runner = async () => {
attempt++;
throw new Error('fail');
};

try {
await pollAsync(runner, 10, 3); // Set maxAttempts to 3
throw new Error('Expected pollAsync to throw an error');
} catch (error: any) {
expect(attempt).to.equal(3); // Ensure it attempted 3 times
expect(error.message).to.equal('fail');
}
});
});

describe('raceWithContext', () => {
it('should race with context', async () => {
const promises = [
sleep(50).then(() => 'first'),
sleep(100).then(() => 'second'),
];

const result = await raceWithContext(promises);
expect(result.resolved).to.equal('first');
expect(result.index).to.equal(0);
});
});

describe('concurrentMap', () => {
it('should map concurrently with correct results', async () => {
const xs = [1, 2, 3, 4, 5, 6];
const mapFn = async (val: number) => {
await new Promise((resolve) => setTimeout(resolve, 50)); // Simulate async work
return val * 2;
};
const result = await concurrentMap(2, xs, mapFn);
expect(result).to.deep.equal([2, 4, 6, 8, 10, 12]);
});

it('should respect concurrency limit', async () => {
const xs = [1, 2, 3, 4, 5, 6];
const concurrency = 2;
let activeTasks = 0;
let maxActiveTasks = 0;

const mapFn = async (val: number) => {
activeTasks++;
maxActiveTasks = Math.max(maxActiveTasks, activeTasks);
await new Promise((resolve) => setTimeout(resolve, 50)); // Simulate async work
activeTasks--;
return val * 2;
};

await concurrentMap(concurrency, xs, mapFn);
expect(maxActiveTasks).to.equal(concurrency);
});
});
});
37 changes: 37 additions & 0 deletions typescript/utils/src/base58.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { expect } from 'chai';
import { utils } from 'ethers';

import { base58ToBuffer, bufferToBase58, hexOrBase58ToHex } from './base58.js';

describe('Base58 Utilities', () => {
describe('base58ToBuffer', () => {
it('should convert a base58 string to a buffer', () => {
const base58String = '3mJr7AoUXx2Wqd';
const expectedBuffer = Buffer.from(utils.base58.decode(base58String));
expect(base58ToBuffer(base58String)).to.deep.equal(expectedBuffer);
});
});

describe('bufferToBase58', () => {
it('should convert a buffer to a base58 string', () => {
const buffer = Buffer.from([1, 2, 3, 4]);
const expectedBase58String = utils.base58.encode(buffer);
expect(bufferToBase58(buffer)).to.equal(expectedBase58String);
});
});

describe('hexOrBase58ToHex', () => {
it('should return the hex string as is if it starts with 0x', () => {
const hexString = '0x1234abcd';
expect(hexOrBase58ToHex(hexString)).to.equal(hexString);
});

it('should convert a base58 string to a hex string', () => {
const base58String = '3mJr7AoUXx2Wqd';
const expectedHexString = utils.hexlify(
Buffer.from(utils.base58.decode(base58String)),
);
expect(hexOrBase58ToHex(base58String)).to.equal(expectedHexString);
});
});
});
74 changes: 74 additions & 0 deletions typescript/utils/src/base64.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { expect } from 'chai';
import Sinon from 'sinon';

import { fromBase64, toBase64 } from './base64.js';
import { rootLogger } from './logging.js';

describe('Base64 Utility Functions', () => {
let loggerStub: sinon.SinonStub;

beforeEach(() => {
loggerStub = Sinon.stub(rootLogger, 'error');
});

afterEach(() => {
loggerStub.restore();
});

describe('toBase64', () => {
it('should encode a valid object to a base64 string', () => {
const data = { key: 'value' };
const result = toBase64(data);
expect(result).to.be.a('string');
expect(result).to.equal(btoa(JSON.stringify(data)));
});

it('should return undefined for null or undefined input', () => {
expect(toBase64(null)).to.be.undefined;
expect(toBase64(undefined)).to.be.undefined;
});

it('should log an error for invalid input', () => {
toBase64(null);
expect(loggerStub.calledOnce).to.be.true;
expect(
loggerStub.calledWith(
'Unable to serialize + encode data to base64',
null,
),
).to.be.true;
});
});

describe('fromBase64', () => {
it('should decode a valid base64 string to an object', () => {
const data = { key: 'value' };
const base64String = btoa(JSON.stringify(data));
const result = fromBase64(base64String);
expect(result).to.deep.equal(data);
});

it('should return undefined for null or undefined input', () => {
expect(fromBase64(null as any)).to.be.undefined;
expect(fromBase64(undefined as any)).to.be.undefined;
});

it('should handle array input and decode the first element', () => {
const data = { key: 'value' };
const base64String = btoa(JSON.stringify(data));
const result = fromBase64([base64String, 'anotherString']);
expect(result).to.deep.equal(data);
});

it('should log an error for invalid base64 input', () => {
fromBase64('invalidBase64');
expect(loggerStub.calledOnce).to.be.true;
expect(
loggerStub.calledWith(
'Unable to decode + deserialize data from base64',
'invalidBase64',
),
).to.be.true;
});
});
});
Loading

0 comments on commit c622bfb

Please sign in to comment.