test: refactor notification tests on the basis of promises rather than polling

This commit is contained in:
steveluscher 2022-04-09 18:39:32 -07:00 committed by Steven Luscher
parent db50893fa1
commit 21a64db140
1 changed files with 146 additions and 151 deletions

View File

@ -20,7 +20,6 @@ import {
Message, Message,
} from '../src'; } from '../src';
import invariant from '../src/util/assert'; import invariant from '../src/util/assert';
import {DEFAULT_TICKS_PER_SLOT, NUM_TICKS_PER_SECOND} from '../src/timing';
import {MOCK_PORT, url} from './url'; import {MOCK_PORT, url} from './url';
import { import {
AccountInfo, AccountInfo,
@ -29,8 +28,10 @@ import {
BlockSignatures, BlockSignatures,
Commitment, Commitment,
ConfirmedBlock, ConfirmedBlock,
Context,
EpochInfo, EpochInfo,
InflationGovernor, InflationGovernor,
Logs,
SlotInfo, SlotInfo,
} from '../src/connection'; } from '../src/connection';
import {sleep} from '../src/util/sleep'; import {sleep} from '../src/util/sleep';
@ -3551,50 +3552,84 @@ describe('Connection', function () {
); );
}); });
it('account change notification', async () => { describe('given an open websocket connection', () => {
if (mockServer) { beforeEach(async () => {
console.log('non-live test skipped'); // Open the socket connection and wait for it to become pingable.
return; connection._rpcWebSocket.connect();
} // eslint-disable-next-line no-constant-condition
const connection = new Connection(url, 'confirmed'); while (true) {
const owner = Keypair.generate(); try {
await connection._rpcWebSocket.notify('ping');
let subscriptionId; break;
try { // eslint-disable-next-line no-empty
const notificationPromise = new Promise<AccountInfo<Buffer>>( } catch {}
resolve => { await sleep(100);
subscriptionId = connection.onAccountChange(
owner.publicKey,
resolve,
'confirmed',
);
},
);
connection.requestAirdrop(owner.publicKey, LAMPORTS_PER_SOL);
const notificationPayload = await notificationPromise;
expect(notificationPayload.lamports).to.eq(LAMPORTS_PER_SOL);
expect(notificationPayload.owner.equals(SystemProgram.programId)).to.be
.true;
} finally {
if (subscriptionId != null) {
connection.removeAccountChangeListener(subscriptionId);
} }
} });
});
it('program account change notification', async () => { it('account change notification', async () => {
connection._commitment = 'confirmed'; const connection = new Connection(url, 'confirmed');
const owner = Keypair.generate();
const owner = Keypair.generate(); let subscriptionId: number | undefined;
const programAccount = Keypair.generate(); try {
const balanceNeeded = await connection.getMinimumBalanceForRentExemption( const accountInfoPromise = new Promise<AccountInfo<Buffer>>(
0, resolve => {
); subscriptionId = connection.onAccountChange(
owner.publicKey,
resolve,
'confirmed',
);
},
);
connection.requestAirdrop(owner.publicKey, LAMPORTS_PER_SOL);
const accountInfo = await accountInfoPromise;
expect(accountInfo.lamports).to.eq(LAMPORTS_PER_SOL);
expect(accountInfo.owner.equals(SystemProgram.programId)).to.be.true;
} finally {
if (subscriptionId != null) {
await connection.removeAccountChangeListener(subscriptionId);
}
}
});
let notified = false; it('program account change notification', async () => {
const subscriptionId = connection.onProgramAccountChange( connection._commitment = 'confirmed';
SystemProgram.programId,
(keyedAccountInfo: KeyedAccountInfo) => { const owner = Keypair.generate();
const programAccount = Keypair.generate();
const balanceNeeded =
await connection.getMinimumBalanceForRentExemption(0);
let subscriptionId: number | undefined;
try {
const keyedAccountInfoPromise = new Promise<KeyedAccountInfo>(
resolve => {
subscriptionId = connection.onProgramAccountChange(
SystemProgram.programId,
resolve,
);
},
);
await helpers.airdrop({
connection,
address: owner.publicKey,
amount: LAMPORTS_PER_SOL,
});
const transaction = new Transaction().add(
SystemProgram.transfer({
fromPubkey: owner.publicKey,
toPubkey: programAccount.publicKey,
lamports: balanceNeeded,
}),
);
await sendAndConfirmTransaction(connection, transaction, [owner], {
commitment: 'confirmed',
});
const keyedAccountInfo = await keyedAccountInfoPromise;
if (keyedAccountInfo.accountId.equals(programAccount.publicKey)) { if (keyedAccountInfo.accountId.equals(programAccount.publicKey)) {
expect(keyedAccountInfo.accountInfo.lamports).to.eq(balanceNeeded); expect(keyedAccountInfo.accountInfo.lamports).to.eq(balanceNeeded);
expect( expect(
@ -3602,129 +3637,89 @@ describe('Connection', function () {
SystemProgram.programId, SystemProgram.programId,
), ),
).to.be.true; ).to.be.true;
notified = true;
} }
}, } finally {
); if (subscriptionId != null) {
await connection.removeProgramAccountChangeListener(subscriptionId);
await helpers.airdrop({ }
connection, }
address: owner.publicKey,
amount: LAMPORTS_PER_SOL,
}); });
try { it('slot notification', async () => {
const transaction = new Transaction().add( let subscriptionId: number | undefined;
SystemProgram.transfer({ try {
fromPubkey: owner.publicKey, const notifiedSlotInfo = await new Promise<SlotInfo>(resolve => {
toPubkey: programAccount.publicKey, subscriptionId = connection.onSlotChange(resolve);
lamports: balanceNeeded, });
}), expect(notifiedSlotInfo.parent).to.be.at.least(0);
); expect(notifiedSlotInfo.root).to.be.at.least(0);
await sendAndConfirmTransaction(connection, transaction, [owner], { expect(notifiedSlotInfo.slot).to.be.at.least(1);
commitment: 'confirmed', } finally {
}); if (subscriptionId != null) {
} catch (err) { await connection.removeSlotChangeListener(subscriptionId);
await connection.removeProgramAccountChangeListener(subscriptionId); }
throw err;
}
let i = 0;
while (!notified) {
if (++i === 30) {
throw new Error('Program change notification not observed');
} }
// Sleep for a 1/4 of a slot, notifications only occur after a block is
// processed
await sleep((250 * DEFAULT_TICKS_PER_SLOT) / NUM_TICKS_PER_SECOND);
}
await connection.removeProgramAccountChangeListener(subscriptionId);
});
it('slot notification', async () => {
let notifiedSlotInfo: SlotInfo | undefined;
const subscriptionId = connection.onSlotChange(slotInfo => {
notifiedSlotInfo = slotInfo;
}); });
// Wait for notification it('root notification', async () => {
let i = 0; let subscriptionId: number | undefined;
while (!notifiedSlotInfo) { try {
if (++i === 30) { const atLeastTwoRoots = await new Promise<number[]>(resolve => {
throw new Error('Slot change notification not observed'); const roots: number[] = [];
subscriptionId = connection.onRootChange(root => {
if (roots.length === 2) {
return;
}
roots.push(root);
if (roots.length === 2) {
// Collect at least two, then resolve.
resolve(roots);
}
});
});
expect(atLeastTwoRoots[1]).to.be.greaterThan(atLeastTwoRoots[0]);
} finally {
if (subscriptionId != null) {
await connection.removeRootChangeListener(subscriptionId);
}
} }
// Sleep for a 1/4 of a slot, notifications only occur after a block is
// processed
await sleep((250 * DEFAULT_TICKS_PER_SLOT) / NUM_TICKS_PER_SECOND);
}
expect(notifiedSlotInfo.parent).to.be.at.least(0);
expect(notifiedSlotInfo.root).to.be.at.least(0);
expect(notifiedSlotInfo.slot).to.be.at.least(1);
await connection.removeSlotChangeListener(subscriptionId);
});
it('root notification', async () => {
let roots: number[] = [];
const subscriptionId = connection.onRootChange(root => {
roots.push(root);
}); });
// Wait for mockCallback to receive a call it('logs notification', async () => {
let i = 0; let subscriptionId: number | undefined;
while (roots.length < 2) { const owner = Keypair.generate();
if (++i === 30) { try {
throw new Error('Root change notification not observed'); const logPromise = new Promise<[Logs, Context]>(resolve => {
} subscriptionId = connection.onLogs(
// Sleep for a 1/4 of a slot, notifications only occur after a block is
// processed
await sleep((250 * DEFAULT_TICKS_PER_SLOT) / NUM_TICKS_PER_SECOND);
}
expect(roots[1]).to.be.greaterThan(roots[0]);
await connection.removeRootChangeListener(subscriptionId);
});
it('logs notification', async () => {
let listener: number | undefined;
const owner = Keypair.generate();
const [logsRes, ctx] = await new Promise(resolve => {
let received = false;
listener = connection.onLogs(
owner.publicKey,
(logs, ctx) => {
if (!logs.err) {
received = true;
resolve([logs, ctx]);
}
},
'processed',
);
// Send transactions until the log subscription receives an event
(async () => {
while (!received) {
// Execute a transaction so that we can pickup its logs.
await connection.requestAirdrop(
owner.publicKey, owner.publicKey,
1 * LAMPORTS_PER_SOL, (logs, ctx) => {
if (!logs.err) {
resolve([logs, ctx]);
}
},
'processed',
); );
await sleep(1000); });
// Execute a transaction so that we can pickup its logs.
await connection.requestAirdrop(owner.publicKey, LAMPORTS_PER_SOL);
const [logsRes, ctx] = await logPromise;
expect(ctx.slot).to.be.greaterThan(0);
expect(logsRes.logs.length).to.eq(2);
expect(logsRes.logs[0]).to.eq(
'Program 11111111111111111111111111111111 invoke [1]',
);
expect(logsRes.logs[1]).to.eq(
'Program 11111111111111111111111111111111 success',
);
} finally {
if (subscriptionId != null) {
await connection.removeOnLogsListener(subscriptionId);
} }
})(); }
}); });
expect(ctx.slot).to.be.greaterThan(0); });
expect(logsRes.logs.length).to.eq(2);
expect(logsRes.logs[0]).to.eq(
'Program 11111111111111111111111111111111 invoke [1]',
);
expect(logsRes.logs[1]).to.eq(
'Program 11111111111111111111111111111111 success',
);
await connection.removeOnLogsListener(listener!);
}).timeout(60 * 1000);
it('https request', async () => { it('https request', async () => {
const connection = new Connection('https://api.mainnet-beta.solana.com'); const connection = new Connection('https://api.mainnet-beta.solana.com');