openvibers/openvibe.shared Commits
Add parallel 'auto' to the test-runner and release 2.8.0 (#16)
Changes
@@@ -6,6 +6,15 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).## Unreleased++## 2.8.0 — 2026-10-04++++- `openvibe-shared/test-runner`: `parallel: 'auto'` resolves to `max(1, min(8, cpus - 1))` (before, any++ non-number made the runner start nothing); a numeric pin works as before and no option keeps the++ `min(4, cpus - 1)` default. `run()` also returns the resolved `parallel`, and `resolveParallel()` is++ exported. Each test file runs in its own process and owns its database (`createTestDb`: its own PGlite,++ or under `test:pg` its own PostgreSQL schema, roles and port), so files parallelize safely and a repo++ no longer needs `parallel: 1`.++## 2.7.0 — 2026-10-04- `openvibe-shared/notifications`: a new `CONFIRMATION_REQUESTED` type (category `system`, priority@@@ -2,7 +2,7 @@> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.--**Status:** alpha, v2.7.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed++**Status:** alpha, v2.8.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployedconsumer installs a tagged release and none keeps a vendored copy([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` namesthe tag it pins.@@@ -4,7 +4,7 @@"code": true,"wave": "W2","package": "openvibe-shared",-- "version": "2.7.0",++ "version": "2.8.0","plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)","deployed": false,"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",@@@ -1,12 +1,12 @@{"name": "openvibe-shared",-- "version": "2.7.0",++ "version": "2.8.0","lockfileVersion": 3,"requires": true,"packages": {"": {"name": "openvibe-shared",-- "version": "2.7.0",++ "version": "2.8.0","license": "MIT","bin": {"openvibe-build-assets": "scripts/build-assets.js"@@@ -1,6 +1,6 @@{"name": "openvibe-shared",-- "version": "2.7.0",++ "version": "2.8.0","description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site","main": "index.js","bin": {@@@ -11,6 +11,11 @@// npm test -- quota cache # only files whose name contains one of the words// npm test -- --strict # a skip fails the run too (or OV_TEST_STRICT=1)//++// Each test file runs in its own process and owns its database: openvibe-sdk/testing `createTestDb` gives it its++// own PGlite, or under `test:pg` its own PostgreSQL schema, roles and port. Files therefore parallelize safely and++// a repo needs no `parallel: 1` pin. `parallel` is a number (a pin), `'auto'` (min(8, cpus-1), at least 1) or unset++// (min(4, cpus-1), at least 1).++//// A test that cannot run something (no ffmpeg, no Chrome, no git history) prints one line per thing it// skipped, `<label>: skipped (<why>)`, and still exits 0. Such a file is listed with ○ and its reasons,// and is NOT counted as passed: the summary reads `148/150 test files passed, 2 skipped (…)`. Only a run@@@ -35,6 +40,15 @@ function skipsIn(output) {return out;}++/** How many files run at once: a numeric pin as given (at least 1), 'auto' by CPUs (1..8), else the default (1..4). */++function resolveParallel(parallel) {++ const cpus = os.cpus().length;++ if (parallel === 'auto') return Math.max(1, Math.min(8, cpus - 1));++ const n = Math.floor(Number(parallel));++ if (Number.isFinite(n) && n >= 1) return n;++ return Math.max(1, Math.min(4, cpus - 1));++}++function runOne(file, { dir, cwd, env, timeoutMs, nodeArgs }) {return new Promise((resolve) => {const started = Date.now();@@@ -55,7 +69,7 @@ function runOne(file, { dir, cwd, env, timeoutMs, nodeArgs }) {/*** Run the tests; resolves { results, passed, skipped, failed, strict, exitCode, summary } and prints as it goes.* opts: dir (required: the test directory), argv (default process.argv.slice(2)), cwd (default dir/..),-- * timeoutMs (60000), parallel (min(4, cpus-1)), env (added to process.env; NODE_ENV=test by default),++ * timeoutMs (60000), parallel (a number, 'auto' = min(8, cpus-1), default min(4, cpus-1); resolved value returned), env (added to process.env; NODE_ENV=test by default),* hide (a RegExp: output lines of a failed file left out, e.g. /^\[DB\] /), pad (file-name column, 44),* match (which files, default /\.test\.js$/), serial (files to run alone after parallel files),* nodeArgs ([]), log (console.log).@@@ -78,7 +92,7 @@ async function run(opts = {}) {dir, cwd: opts.cwd || path.join(dir, '..'), timeoutMs: opts.timeoutMs || 60000, nodeArgs: opts.nodeArgs || [],env: { ...process.env, NODE_ENV: 'test', ...(opts.env || {}) },};-- const parallel = Math.max(1, opts.parallel || Math.min(4, os.cpus().length - 1));++ const parallel = resolveParallel(opts.parallel);const pad = opts.pad || 44;const queue = [], serial = [];for (const file of files) (opts.serial && opts.serial.test(file) ? serial : queue).push(file);@@@ -110,7 +124,7 @@ async function run(opts = {}) {: `${passed}/${results.length} test files passed`;log(`\n${summary}`);const exitCode = failed.length || (strict && skipped.length) ? 1 : 0;-- return { results, passed, skipped: skipped.length, failed: failed.length, strict, exitCode, summary };++ return { results, passed, skipped: skipped.length, failed: failed.length, strict, exitCode, summary, parallel };}/** run(), then exit with its status: what a test/run.js calls. */@@@ -118,4 +132,4 @@ function main(opts) {run(opts).then((r) => process.exit(r.exitCode), (err) => { console.error(err); process.exit(1); });}--module.exports = { run, main, skipsIn, SKIP_RE };++module.exports = { run, main, skipsIn, resolveParallel, SKIP_RE };@@@ -6,7 +6,7 @@ const assert = require('assert');const fs = require('fs');const os = require('os');const path = require('path');--const { run, skipsIn } = require('../test-runner');++const { run, skipsIn, resolveParallel } = require('../test-runner');(async () => {const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-test-runner-'));@@@ -43,6 +43,19 @@ const { run, skipsIn } = require('../test-runner');assert.strictEqual(r.exitCode, 1);assert.match(r.summary, /strict: skips fail the run$/);++ // parallel: 'auto' resolves by CPU to a number >= 1 and still runs every matched file; pins and the default hold.++ const cpus = os.cpus().length;++ assert.strictEqual(resolveParallel('auto'), Math.max(1, Math.min(8, cpus - 1)));++ assert.strictEqual(resolveParallel(undefined), Math.max(1, Math.min(4, cpus - 1)));++ assert.strictEqual(resolveParallel(1), 1);++ assert.strictEqual(resolveParallel(3), 3);++ assert.strictEqual(resolveParallel('nonsense'), Math.max(1, Math.min(4, cpus - 1)));++ r = await run({ dir, argv: ['a-pass', 'e-mention'], log, parallel: 'auto' });++ assert.ok(Number.isInteger(r.parallel) && r.parallel >= 1);++ assert.deepStrictEqual([r.results.length, r.summary, r.exitCode], [2, '2/2 test files passed', 0]);++ r = await run({ dir, argv: ['a-pass', 'e-mention'], log, parallel: 1 });++ assert.deepStrictEqual([r.parallel, r.results.length], [1, 2]);++r = await run({ dir, argv: ['nothing-matches'], log });assert.strictEqual(r.exitCode, 1);assert.deepStrictEqual(skipsIn('x\n ACK test: skipped (ffmpeg not installed) \nrollback with newer writes: skipped (no git history)\nOK B: skipped — old style'),
@@ -6,6 +6,15 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
@@ -6,6 +6,15 @@ A release is the git tag `vX.Y.Z`; consumers pin the tag's tarball (see README).
## Unreleased
## Unreleased
+## 2.8.0 — 2026-10-04
+
+- `openvibe-shared/test-runner`: `parallel: 'auto'` resolves to `max(1, min(8, cpus - 1))` (before, any
+ non-number made the runner start nothing); a numeric pin works as before and no option keeps the
+ `min(4, cpus - 1)` default. `run()` also returns the resolved `parallel`, and `resolveParallel()` is
+ exported. Each test file runs in its own process and owns its database (`createTestDb`: its own PGlite,
+ or under `test:pg` its own PostgreSQL schema, roles and port), so files parallelize safely and a repo
+ no longer needs `parallel: 1`.
+
## 2.7.0 — 2026-10-04
## 2.7.0 — 2026-10-04
- `openvibe-shared/notifications`: a new `CONFIRMATION_REQUESTED` type (category `system`, priority
- `openvibe-shared/notifications`: a new `CONFIRMATION_REQUESTED` type (category `system`, priority
@@ -2,7 +2,7 @@
@@ -2,7 +2,7 @@
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
> Versioned UI, the OpenVibe Frame (the navbar, footer and "shipped" views every site sits in), SEO, legal and release-client packages every OpenVibe site renders.
-**Status:** alpha, v2.7.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
+**Status:** alpha, v2.8.0 (every release is a tag; see [CHANGELOG.md](CHANGELOG.md)). Every deployed
consumer installs a tagged release and none keeps a vendored copy
consumer installs a tagged release and none keeps a vendored copy
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
([docs/migration-plan.md](docs/migration-plan.md) is done); each repository's `package.json` names
the tag it pins.
the tag it pins.
@@ -4,7 +4,7 @@
@@ -4,7 +4,7 @@
"code": true,
"code": true,
"wave": "W2",
"wave": "W2",
"package": "openvibe-shared",
"package": "openvibe-shared",
- "version": "2.7.0",
+ "version": "2.8.0",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"plan": "OpenVibe End-to-End Realignment & Implementation Plan, revision 3 (20 Sep 2026)",
"deployed": false,
"deployed": false,
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
"deployment": "none of its own: a package released as git tags; OpenVibe.Network serves /shared/* from its pinned tag, and a site can serve its own pinned copy (openvibe-shared/serve)",
@@ -1,12 +1,12 @@
@@ -1,12 +1,12 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "2.7.0",
+ "version": "2.8.0",
"lockfileVersion": 3,
"lockfileVersion": 3,
"requires": true,
"requires": true,
"packages": {
"packages": {
"": {
"": {
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "2.7.0",
+ "version": "2.8.0",
"license": "MIT",
"license": "MIT",
"bin": {
"bin": {
"openvibe-build-assets": "scripts/build-assets.js"
"openvibe-build-assets": "scripts/build-assets.js"
@@ -1,6 +1,6 @@
@@ -1,6 +1,6 @@
{
{
"name": "openvibe-shared",
"name": "openvibe-shared",
- "version": "2.7.0",
+ "version": "2.8.0",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"description": "Shared navbar, footer, themes, icons, legal pages, SEO, auth and analytics for every OpenVibe site",
"main": "index.js",
"main": "index.js",
"bin": {
"bin": {
@@ -11,6 +11,11 @@
@@ -11,6 +11,11 @@
// npm test -- quota cache # only files whose name contains one of the words
// npm test -- quota cache # only files whose name contains one of the words
// npm test -- --strict # a skip fails the run too (or OV_TEST_STRICT=1)
// npm test -- --strict # a skip fails the run too (or OV_TEST_STRICT=1)
//
//
+// Each test file runs in its own process and owns its database: openvibe-sdk/testing `createTestDb` gives it its
+// own PGlite, or under `test:pg` its own PostgreSQL schema, roles and port. Files therefore parallelize safely and
+// a repo needs no `parallel: 1` pin. `parallel` is a number (a pin), `'auto'` (min(8, cpus-1), at least 1) or unset
+// (min(4, cpus-1), at least 1).
+//
// A test that cannot run something (no ffmpeg, no Chrome, no git history) prints one line per thing it
// A test that cannot run something (no ffmpeg, no Chrome, no git history) prints one line per thing it
// skipped, `<label>: skipped (<why>)`, and still exits 0. Such a file is listed with ○ and its reasons,
// skipped, `<label>: skipped (<why>)`, and still exits 0. Such a file is listed with ○ and its reasons,
// and is NOT counted as passed: the summary reads `148/150 test files passed, 2 skipped (…)`. Only a run
// and is NOT counted as passed: the summary reads `148/150 test files passed, 2 skipped (…)`. Only a run
@@ -35,6 +40,15 @@ function skipsIn(output) {
@@ -35,6 +40,15 @@ function skipsIn(output) {
return out;
return out;
}
}
+/** How many files run at once: a numeric pin as given (at least 1), 'auto' by CPUs (1..8), else the default (1..4). */
+function resolveParallel(parallel) {
+ const cpus = os.cpus().length;
+ if (parallel === 'auto') return Math.max(1, Math.min(8, cpus - 1));
+ const n = Math.floor(Number(parallel));
+ if (Number.isFinite(n) && n >= 1) return n;
+ return Math.max(1, Math.min(4, cpus - 1));
+}
+
function runOne(file, { dir, cwd, env, timeoutMs, nodeArgs }) {
function runOne(file, { dir, cwd, env, timeoutMs, nodeArgs }) {
return new Promise((resolve) => {
return new Promise((resolve) => {
const started = Date.now();
const started = Date.now();
@@ -55,7 +69,7 @@ function runOne(file, { dir, cwd, env, timeoutMs, nodeArgs }) {
@@ -55,7 +69,7 @@ function runOne(file, { dir, cwd, env, timeoutMs, nodeArgs }) {
/**
/**
* Run the tests; resolves { results, passed, skipped, failed, strict, exitCode, summary } and prints as it goes.
* Run the tests; resolves { results, passed, skipped, failed, strict, exitCode, summary } and prints as it goes.
* opts: dir (required: the test directory), argv (default process.argv.slice(2)), cwd (default dir/..),
* opts: dir (required: the test directory), argv (default process.argv.slice(2)), cwd (default dir/..),
- * timeoutMs (60000), parallel (min(4, cpus-1)), env (added to process.env; NODE_ENV=test by default),
+ * timeoutMs (60000), parallel (a number, 'auto' = min(8, cpus-1), default min(4, cpus-1); resolved value returned), env (added to process.env; NODE_ENV=test by default),
* hide (a RegExp: output lines of a failed file left out, e.g. /^\[DB\] /), pad (file-name column, 44),
* hide (a RegExp: output lines of a failed file left out, e.g. /^\[DB\] /), pad (file-name column, 44),
* match (which files, default /\.test\.js$/), serial (files to run alone after parallel files),
* match (which files, default /\.test\.js$/), serial (files to run alone after parallel files),
* nodeArgs ([]), log (console.log).
* nodeArgs ([]), log (console.log).
@@ -78,7 +92,7 @@ async function run(opts = {}) {
@@ -78,7 +92,7 @@ async function run(opts = {}) {
dir, cwd: opts.cwd || path.join(dir, '..'), timeoutMs: opts.timeoutMs || 60000, nodeArgs: opts.nodeArgs || [],
dir, cwd: opts.cwd || path.join(dir, '..'), timeoutMs: opts.timeoutMs || 60000, nodeArgs: opts.nodeArgs || [],
env: { ...process.env, NODE_ENV: 'test', ...(opts.env || {}) },
env: { ...process.env, NODE_ENV: 'test', ...(opts.env || {}) },
};
};
- const parallel = Math.max(1, opts.parallel || Math.min(4, os.cpus().length - 1));
+ const parallel = resolveParallel(opts.parallel);
const pad = opts.pad || 44;
const pad = opts.pad || 44;
const queue = [], serial = [];
const queue = [], serial = [];
for (const file of files) (opts.serial && opts.serial.test(file) ? serial : queue).push(file);
for (const file of files) (opts.serial && opts.serial.test(file) ? serial : queue).push(file);
@@ -110,7 +124,7 @@ async function run(opts = {}) {
@@ -110,7 +124,7 @@ async function run(opts = {}) {
: `${passed}/${results.length} test files passed`;
: `${passed}/${results.length} test files passed`;
log(`\n${summary}`);
log(`\n${summary}`);
const exitCode = failed.length || (strict && skipped.length) ? 1 : 0;
const exitCode = failed.length || (strict && skipped.length) ? 1 : 0;
- return { results, passed, skipped: skipped.length, failed: failed.length, strict, exitCode, summary };
+ return { results, passed, skipped: skipped.length, failed: failed.length, strict, exitCode, summary, parallel };
}
}
/** run(), then exit with its status: what a test/run.js calls. */
/** run(), then exit with its status: what a test/run.js calls. */
@@ -118,4 +132,4 @@ function main(opts) {
@@ -118,4 +132,4 @@ function main(opts) {
run(opts).then((r) => process.exit(r.exitCode), (err) => { console.error(err); process.exit(1); });
run(opts).then((r) => process.exit(r.exitCode), (err) => { console.error(err); process.exit(1); });
}
}
-module.exports = { run, main, skipsIn, SKIP_RE };
+module.exports = { run, main, skipsIn, resolveParallel, SKIP_RE };
@@ -6,7 +6,7 @@ const assert = require('assert');
@@ -6,7 +6,7 @@ const assert = require('assert');
const fs = require('fs');
const fs = require('fs');
const os = require('os');
const os = require('os');
const path = require('path');
const path = require('path');
-const { run, skipsIn } = require('../test-runner');
+const { run, skipsIn, resolveParallel } = require('../test-runner');
(async () => {
(async () => {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-test-runner-'));
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'ov-test-runner-'));
@@ -43,6 +43,19 @@ const { run, skipsIn } = require('../test-runner');
@@ -43,6 +43,19 @@ const { run, skipsIn } = require('../test-runner');
assert.strictEqual(r.exitCode, 1);
assert.strictEqual(r.exitCode, 1);
assert.match(r.summary, /strict: skips fail the run$/);
assert.match(r.summary, /strict: skips fail the run$/);
+ // parallel: 'auto' resolves by CPU to a number >= 1 and still runs every matched file; pins and the default hold.
+ const cpus = os.cpus().length;
+ assert.strictEqual(resolveParallel('auto'), Math.max(1, Math.min(8, cpus - 1)));
+ assert.strictEqual(resolveParallel(undefined), Math.max(1, Math.min(4, cpus - 1)));
+ assert.strictEqual(resolveParallel(1), 1);
+ assert.strictEqual(resolveParallel(3), 3);
+ assert.strictEqual(resolveParallel('nonsense'), Math.max(1, Math.min(4, cpus - 1)));
+ r = await run({ dir, argv: ['a-pass', 'e-mention'], log, parallel: 'auto' });
+ assert.ok(Number.isInteger(r.parallel) && r.parallel >= 1);
+ assert.deepStrictEqual([r.results.length, r.summary, r.exitCode], [2, '2/2 test files passed', 0]);
+ r = await run({ dir, argv: ['a-pass', 'e-mention'], log, parallel: 1 });
+ assert.deepStrictEqual([r.parallel, r.results.length], [1, 2]);
+
r = await run({ dir, argv: ['nothing-matches'], log });
r = await run({ dir, argv: ['nothing-matches'], log });
assert.strictEqual(r.exitCode, 1);
assert.strictEqual(r.exitCode, 1);
assert.deepStrictEqual(skipsIn('x\n ACK test: skipped (ffmpeg not installed) \nrollback with newer writes: skipped (no git history)\nOK B: skipped — old style'),
assert.deepStrictEqual(skipsIn('x\n ACK test: skipped (ffmpeg not installed) \nrollback with newer writes: skipped (no git history)\nOK B: skipped — old style'),