mirror of
https://github.com/logos-storage/logos-storage-nim.git
synced 2026-05-12 06:19:33 +00:00
Start/stop node for each test instead of all tests
- add npm run testOnly "test name regex" to run specific tests from the cli - finds available ports for each node to avoid collisions - PORT_BASE env var can be set for containerisation at a later point
This commit is contained in:
parent
8a68a81f54
commit
ef989571b5
1
.gitignore
vendored
1
.gitignore
vendored
@ -52,3 +52,4 @@ tests/cbindings/data-dir
|
||||
tests/cbindings/downloaded_hello.txt
|
||||
|
||||
nimbledeps
|
||||
tests/js/node_modules
|
||||
|
||||
@ -344,9 +344,10 @@ export class StorageNode {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch content from the network into local store (background fetch).
|
||||
* Downloads the manifest described by CID, then downloads the dataset content (treeCid in the manifest), storing it locally.
|
||||
* Does not return progress updates; resolves when fetch is complete.
|
||||
* @param {string} cid
|
||||
* @returns {Promise<string>} the fetched manifest (json) content as a UTF-8 string
|
||||
*/
|
||||
fetch(cid) {
|
||||
return callAsync(cb => _lib.storage_fetch(this.#ctx, cid, cb, null));
|
||||
|
||||
1175
tests/js/package-lock.json
generated
1175
tests/js/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -2,10 +2,29 @@
|
||||
"name": "logos-storage-js-tests",
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"//": {
|
||||
"scripts": {
|
||||
"test": [
|
||||
"Runs all the tests",
|
||||
"Usage: `npm test`",
|
||||
"Debug: use `npm test --debug` to output TRACE logs and node config to stdout"
|
||||
],
|
||||
"testOnly": [
|
||||
"Run tests with names that match the regex name pattern. Useful for development.",
|
||||
"See https://mochajs.org/running/cli/#--grep-regexp--g-regexp for more info on the pattern syntax.",
|
||||
"Usage: `npm run testOnly \"$test name regex\"`",
|
||||
"Debug: also works with --debug (see above)"
|
||||
]
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"test": "node two-node-test.js"
|
||||
"test": "mocha --timeout 30000 two-node-test.js",
|
||||
"testOnly": "sh -c 'mocha --timeout 30000 --grep \"$1\" two-node-test.js' --"
|
||||
},
|
||||
"dependencies": {
|
||||
"koffi": "^2.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"mocha": "^11.7.5"
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,20 +10,31 @@
|
||||
*
|
||||
* Usage:
|
||||
* cd tests/js && npm install && npm test
|
||||
*
|
||||
* Environment:
|
||||
* PORT_BASE Base port for node listen/disc ports (default: 8792).
|
||||
* Set explicitly when running in containers so ports are
|
||||
* predictable and can be pre-declared in port mappings.
|
||||
*/
|
||||
|
||||
import { StorageNode } from './harness.js';
|
||||
import { describe, it, before, after, beforeEach, afterEach } from 'mocha';
|
||||
import assert from 'node:assert/strict';
|
||||
import net from 'node:net';
|
||||
import { rmSync, mkdirSync } from 'fs';
|
||||
import { tmpdir } from 'os';
|
||||
import path from 'path';
|
||||
import { StorageNode } from './harness.js';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const TMP = tmpdir();
|
||||
const PORT_BASE = parseInt(process.env.PORT_BASE ?? '8700', 10);
|
||||
|
||||
function nodeConfig(label, config) {
|
||||
const debug = Boolean(process.env.npm_config_debug);
|
||||
|
||||
function nodeConfig(label, config) {// { listenPort, discPort, bootstrapSpr = null }) {
|
||||
if (!config["data-dir"]) {
|
||||
config["data-dir"] = path.join(TMP, `libstorage-test-${label}`);
|
||||
}
|
||||
@ -33,234 +44,216 @@ function nodeConfig(label, config) {// { listenPort, discPort, bootstrapSpr = nu
|
||||
mkdirSync(config["data-dir"], { recursive: true });
|
||||
|
||||
if (!config["log-level"]) {
|
||||
config["log-level"] = 'ERROR';
|
||||
config["log-level"] = debug ? 'TRACE' :'ERROR';
|
||||
}
|
||||
|
||||
console.debug(`Node '${label}' config:`, config);
|
||||
if (debug) {
|
||||
console.debug(`Node '${label}' config:`, config);
|
||||
}
|
||||
|
||||
return JSON.stringify(config);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Minimal test runner
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
async function test(name, fn) {
|
||||
process.stdout.write(` ${name} ... `);
|
||||
try {
|
||||
await fn();
|
||||
console.log('PASS');
|
||||
passed++;
|
||||
} catch (err) {
|
||||
console.log(`FAIL\n ${err.message}`);
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
function assert(condition, message) {
|
||||
if (!condition) throw new Error(message ?? 'assertion failed');
|
||||
}
|
||||
|
||||
function assertEqual(actual, expected, label = '') {
|
||||
if (actual !== expected) {
|
||||
throw new Error(
|
||||
`${label ? label + ': ' : ''}expected ${JSON.stringify(expected)}, got ${JSON.stringify(actual)}`
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Finds the first free TCP port at or above `port`.
|
||||
* Tries to bind a server; if the port is in use, recurses with port + 1.
|
||||
*/
|
||||
function findFreePort(port) {
|
||||
return new Promise((resolve) => {
|
||||
const srv = net.createServer();
|
||||
srv.listen(port, '127.0.0.1', () => srv.close(() => resolve(port)));
|
||||
srv.on('error', () => resolve(findFreePort(port + 1)));
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Test suite
|
||||
// Single-node tests — node A started ONCE for the whole describe block
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function main() {
|
||||
console.log('\nlogos-storage two-node p2p test\n');
|
||||
describe('single-node tests', () => {
|
||||
let nodeA;
|
||||
|
||||
const nodeA = new StorageNode();
|
||||
const nodeB = new StorageNode();
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Setup: start node A, get its SPR, start node B bootstrapped to node A
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
console.log('Setting up nodes...');
|
||||
|
||||
await nodeA.create(nodeConfig('a', {
|
||||
"listen-port": 8792,
|
||||
"disc-port": 8794,
|
||||
"nat": "extip:127.0.0.1",
|
||||
// "log-level": 'TRACE'
|
||||
}));
|
||||
await nodeA.start();
|
||||
console.log(` node A version: ${nodeA.version()}`);
|
||||
|
||||
const sprA = await nodeA.spr();
|
||||
console.log(` node A SPR: ${sprA.slice(0, 40)}...`);
|
||||
|
||||
await nodeB.create(nodeConfig('b', {
|
||||
"listen-port": 8793,
|
||||
"disc-port": 8795,
|
||||
"nat": "extip:127.0.0.1",
|
||||
// "log-level": 'TRACE',
|
||||
"bootstrap-node": [sprA]
|
||||
}));
|
||||
await nodeB.start();
|
||||
console.log(` node B version: ${nodeB.version()}`);
|
||||
|
||||
// Allow time for the bootstrap connection to establish
|
||||
// console.log('\nWaiting for p2p connection...');
|
||||
// await new Promise(r => setTimeout(r, 3000));
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Single-node tests (mirrors tests/cbindings/storage.c single-node suite)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
console.log('\nSingle-node tests (node A):');
|
||||
|
||||
await test('version is non-empty', () => {
|
||||
assert(nodeA.version().length > 0, 'version should be non-empty');
|
||||
before(async () => {
|
||||
const [listenPort, discPort] = await Promise.all([
|
||||
findFreePort(PORT_BASE),
|
||||
findFreePort(PORT_BASE + 1),
|
||||
]);
|
||||
nodeA = new StorageNode();
|
||||
await nodeA.create(nodeConfig('a', {
|
||||
"listen-port": listenPort,
|
||||
"disc-port": discPort,
|
||||
"nat": "extip:127.0.0.1",
|
||||
}));
|
||||
await nodeA.start();
|
||||
});
|
||||
|
||||
await test('peer ID is a non-empty string', async () => {
|
||||
after(async () => {
|
||||
await nodeA.shutdown();
|
||||
});
|
||||
|
||||
it('version is non-empty', () => {
|
||||
assert.ok(nodeA.version().length > 0);
|
||||
});
|
||||
|
||||
it('peer ID is a non-empty string', async () => {
|
||||
const pid = await nodeA.peerId();
|
||||
assert(pid && pid.length > 0, 'peer ID should be non-empty');
|
||||
assert.ok(pid && pid.length > 0);
|
||||
});
|
||||
|
||||
await test('SPR contains "spr"', async () => {
|
||||
it('SPR contains "spr"', async () => {
|
||||
const spr = await nodeA.spr();
|
||||
assert(spr.includes('spr'), `SPR should contain "spr", got: ${spr}`);
|
||||
assert.ok(spr.includes('spr'), `SPR should contain "spr", got: ${spr}`);
|
||||
});
|
||||
|
||||
await test('debug info has id and addrs', async () => {
|
||||
const debugJson = await nodeA.debug();
|
||||
const info = JSON.parse(debugJson);
|
||||
assert(info.id && info.id.length > 0, 'debug.id should be present');
|
||||
assert(Array.isArray(info.addrs), 'debug.addrs should be an array');
|
||||
it('debug info has id and addrs', async () => {
|
||||
const info = JSON.parse(await nodeA.debug());
|
||||
assert.ok(info.id && info.id.length > 0);
|
||||
assert.ok(Array.isArray(info.addrs));
|
||||
});
|
||||
|
||||
const CONTENT = 'Hello from node A — p2p test content!';
|
||||
let cid;
|
||||
|
||||
await test('upload content on node A', async () => {
|
||||
cid = await nodeA.uploadContent(CONTENT);
|
||||
assert(cid && cid.length > 0, 'CID should be a non-empty string');
|
||||
console.log(`\n CID: ${cid}`);
|
||||
it('upload returns a non-empty CID', async () => {
|
||||
const cid = await nodeA.uploadContent('upload test content');
|
||||
assert.ok(cid && cid.length > 0);
|
||||
});
|
||||
|
||||
await test('manifest has expected fields', async () => {
|
||||
const manifestJson = await nodeA.downloadManifest(cid);
|
||||
const m = JSON.parse(manifestJson);
|
||||
assert(m.treeCid && m.treeCid.length > 0, 'manifest.treeCid should be present');
|
||||
assert(typeof m.datasetSize === 'number', 'manifest.datasetSize should be a number');
|
||||
assert(m.filename === 'test.txt', `manifest.filename should be "test.txt", got "${m.filename}"`);
|
||||
it('manifest has expected fields', async () => {
|
||||
const cid = await nodeA.uploadContent('manifest test content');
|
||||
const m = JSON.parse(await nodeA.downloadManifest(cid));
|
||||
assert.ok(m.treeCid && m.treeCid.length > 0);
|
||||
assert.equal(typeof m.datasetSize, 'number');
|
||||
assert.equal(m.filename, 'test.txt');
|
||||
});
|
||||
|
||||
await test('list includes uploaded CID manifest', async () => {
|
||||
it('list includes uploaded CID', async () => {
|
||||
const cid = await nodeA.uploadContent('list test content');
|
||||
const listJson = await nodeA.list();
|
||||
assert(listJson.includes(cid), `list should include CID ${cid}`);
|
||||
assert.ok(listJson.includes(cid));
|
||||
});
|
||||
|
||||
await test('space info has totalBlocks', async () => {
|
||||
const spaceJson = await nodeA.space();
|
||||
const s = JSON.parse(spaceJson);
|
||||
assert(typeof s.totalBlocks === 'number', 'space.totalBlocks should be a number');
|
||||
it('space info has totalBlocks', async () => {
|
||||
const s = JSON.parse(await nodeA.space());
|
||||
assert.equal(typeof s.totalBlocks, 'number');
|
||||
});
|
||||
|
||||
await test('content exists locally on node A', async () => {
|
||||
const exists = await nodeA.exists(cid);
|
||||
assert(exists === true, 'exists should return true for uploaded CID');
|
||||
it('exists returns true for uploaded CID', async () => {
|
||||
const cid = await nodeA.uploadContent('exists test content');
|
||||
assert.equal(await nodeA.exists(cid), true);
|
||||
});
|
||||
|
||||
await test('content is downloadable locally on node A', async () => {
|
||||
const downloaded = await nodeA.downloadContent(cid, true /* local only */);
|
||||
assertEqual(downloaded, CONTENT, 'local download on node A');
|
||||
it('local download returns uploaded content', async () => {
|
||||
const content = 'local download test content';
|
||||
const cid = await nodeA.uploadContent(content);
|
||||
assert.equal(await nodeA.downloadContent(cid, true), content);
|
||||
});
|
||||
|
||||
// Upload a second piece of content for delete/fetch tests to leave CONTENT available
|
||||
let cid2;
|
||||
await test('upload second content for delete test', async () => {
|
||||
cid2 = await nodeA.uploadContent('ephemeral content for delete test', 'ephemeral.txt');
|
||||
assert(cid2 && cid2.length > 0, 'CID2 should be non-empty');
|
||||
it('delete removes content from local store', async () => {
|
||||
const cid = await nodeA.uploadContent('ephemeral content for delete test', 'ephemeral.txt');
|
||||
await nodeA.delete(cid);
|
||||
assert.equal(await nodeA.exists(cid), false);
|
||||
});
|
||||
|
||||
await test('delete removes content from local store', async () => {
|
||||
await nodeA.delete(cid2);
|
||||
const exists = await nodeA.exists(cid2);
|
||||
assert(exists === false, 'exists should return false after delete');
|
||||
});
|
||||
|
||||
await test('cancel download does not crash', async () => {
|
||||
// Init a download then immediately cancel it — verifies the cancel path is wired correctly
|
||||
it('cancel download does not crash', async () => {
|
||||
const cid = await nodeA.uploadContent('cancel test content');
|
||||
await nodeA.downloadContent(cid, true); // ensure blocks are cached
|
||||
await nodeA.downloadCancel(cid);
|
||||
// If we get here without exception, the cancel path is working
|
||||
// reaching here without exception means the cancel path is working
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Two-node tests — fresh node pair started/stopped for EACH test
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('two-node tests', () => {
|
||||
let nodeA, nodeB;
|
||||
// add 10 to PORT_BASE to avoid collisions with single-node tests, which use PORT_BASE and PORT_BASE + 1
|
||||
let ports = {
|
||||
listenA: PORT_BASE + 10,
|
||||
discA: PORT_BASE + 11,
|
||||
listenB: PORT_BASE + 12,
|
||||
discB: PORT_BASE + 13
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
const [listenA, discA, listenB, discB] = await Promise.all([
|
||||
findFreePort(ports.listenA + 1),
|
||||
findFreePort(ports.discA + 1),
|
||||
findFreePort(ports.listenB + 1),
|
||||
findFreePort(ports.discB + 1),
|
||||
]);
|
||||
// update base for next test run to avoid collisions
|
||||
ports.listenA = listenA;
|
||||
ports.discA = discA;
|
||||
ports.listenB = listenB;
|
||||
ports.discB = discB;
|
||||
|
||||
nodeA = new StorageNode();
|
||||
await nodeA.create(nodeConfig('a', {
|
||||
"listen-port": ports.listenA,
|
||||
"disc-port": ports.discA,
|
||||
"nat": "extip:127.0.0.1",
|
||||
}));
|
||||
await nodeA.start();
|
||||
|
||||
const sprA = await nodeA.spr();
|
||||
|
||||
nodeB = new StorageNode();
|
||||
await nodeB.create(nodeConfig('b', {
|
||||
"listen-port": ports.listenB,
|
||||
"disc-port": ports.discB,
|
||||
"nat": "extip:127.0.0.1",
|
||||
"bootstrap-node": [sprA],
|
||||
}));
|
||||
await nodeB.start();
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// allSettled ensures node B is shut down even if node A's shutdown throws
|
||||
await Promise.allSettled([nodeA.shutdown(), nodeB.shutdown()]);
|
||||
});
|
||||
|
||||
it('node B can download content from node A over p2p', async () => {
|
||||
const content = 'Hello from node A — p2p test content!';
|
||||
const cid = await nodeA.uploadContent(content);
|
||||
assert.equal(await nodeB.downloadContent(cid, false), content);
|
||||
});
|
||||
|
||||
it('node B can fetch a file from node A, which exists locally', async () => {
|
||||
const content = 'Content for storage_fetch test';
|
||||
const cid = await nodeA.uploadContent(content, 'fetch-test.txt');
|
||||
|
||||
assert.equal(await nodeB.exists(cid), false);
|
||||
await nodeB.fetch(cid);
|
||||
assert.equal(await nodeB.exists(cid), true);
|
||||
assert.equal(await nodeB.downloadContent(cid, true), content);
|
||||
});
|
||||
|
||||
it('fetched content is equivalent to streamed content', async () => {
|
||||
const content = 'Content for storage_fetch test';
|
||||
const cid = await nodeA.uploadContent(content, 'fetch-test.txt');
|
||||
|
||||
await nodeB.fetch(cid);
|
||||
const fetched = await nodeB.downloadContent(cid, true); // gets fetched content from local store
|
||||
const streamed = await nodeB.downloadContent(cid, false); // streams content from network
|
||||
assert.equal(fetched, streamed);
|
||||
});
|
||||
|
||||
it('downloaded manifest is equivalent to fetched manifest', async () => {
|
||||
const content = 'Content for storage_fetch test';
|
||||
const cid = await nodeA.uploadContent(content, 'fetch-test.txt');
|
||||
|
||||
const fetched = await nodeB.fetch(cid);
|
||||
const downloaded = await nodeB.downloadManifest(cid)
|
||||
assert.equal(fetched, downloaded);
|
||||
});
|
||||
|
||||
it('node A has node B in its routing table', async () => {
|
||||
const content = 'Hello from node A — p2p test content!';
|
||||
const cid = await nodeA.uploadContent(content);
|
||||
assert.equal(await nodeB.downloadContent(cid), content);
|
||||
|
||||
const info = JSON.parse(await nodeA.debug());
|
||||
assert.ok(info.table?.nodes.length == 1);
|
||||
assert.equal(info.table?.nodes[0].peerId, await nodeB.peerId());
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Two-node tests
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
console.log('\nTwo-node tests:');
|
||||
|
||||
await test('node B can download content from node A over p2p', async () => {
|
||||
const downloaded = await nodeB.downloadContent(cid, false /* remote */);
|
||||
assertEqual(downloaded, CONTENT, 'remote download on node B');
|
||||
});
|
||||
|
||||
await test('storage_fetch: node B fetches into local store, then exists locally', async () => {
|
||||
// Upload fresh content on A so B definitely doesn't have it yet
|
||||
const fetchContent = 'Content for storage_fetch test';
|
||||
const fetchCid = await nodeA.uploadContent(fetchContent, 'fetch-test.txt');
|
||||
|
||||
// Verify B doesn't have it locally yet
|
||||
const beforeFetch = await nodeB.exists(fetchCid);
|
||||
assert(beforeFetch === false, 'B should not have the CID locally before fetch');
|
||||
|
||||
// Fetch into B's local store
|
||||
await nodeB.fetch(fetchCid);
|
||||
|
||||
// Now it should exist locally on B
|
||||
const afterFetch = await nodeB.exists(fetchCid);
|
||||
assert(afterFetch === true, 'B should have the CID locally after fetch');
|
||||
|
||||
// And be downloadable locally
|
||||
const downloaded = await nodeB.downloadContent(fetchCid, true /* local only */);
|
||||
assertEqual(downloaded, fetchContent, 'fetched content should match');
|
||||
});
|
||||
|
||||
await test('node B peer ID is non-empty', async () => {
|
||||
const pid = await nodeB.peerId();
|
||||
assert(pid && pid.length > 0, 'node B peer ID should be non-empty');
|
||||
});
|
||||
|
||||
await test('node A debug info shows node B in routing table', async () => {
|
||||
const debugJson = await nodeA.debug();
|
||||
const info = JSON.parse(debugJson);
|
||||
// After 3s connection, node B should appear in node A's routing table
|
||||
assert(Array.isArray(info.table?.nodes), 'debug.table.nodes should be an array');
|
||||
assert(info.table.nodes.length > 0, 'routing table should contain at least one peer (node B)');
|
||||
});
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Teardown
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
console.log('\nShutting down nodes...');
|
||||
await nodeA.shutdown();
|
||||
await nodeB.shutdown();
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// Summary
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
console.log(`\n${passed + failed} tests: ${passed} passed, ${failed} failed\n`);
|
||||
if (failed > 0) process.exit(1);
|
||||
}
|
||||
|
||||
main().catch(err => {
|
||||
console.error('\nFatal error:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user