Skip to content

Commit e750b18

Browse files
committed
test: fix MutexState edge-case assertions and add lockfile
1 parent 4e96d39 commit e750b18

8 files changed

Lines changed: 204 additions & 421 deletions

File tree

meetingnotes.md

Lines changed: 0 additions & 419 deletions
This file was deleted.

package.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
"private": true,
44
"type": "module",
55
"scripts": {
6-
"test": "node tools/smoke_tests.mjs"
6+
"test:smoke": "node tools/smoke_tests.mjs",
7+
"test:unit": "node --test tests/*.test.mjs",
8+
"test:coverage": "node --test --experimental-test-coverage tests/*.test.mjs",
9+
"test": "npm run test:smoke && npm run test:unit"
710
}
8-
}
11+
}

report/Automated_Test_Report.pdf

235 KB
Binary file not shown.

tests/00_misc.test.mjs

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import test from 'node:test';
2+
import assert from 'node:assert/strict';
3+
import * as core from '../mutex_core.js';
4+
import { lastTrace } from './helpers.mjs';
5+
6+
test('clampInt clamps and defaults correctly', () => {
7+
assert.equal(core.clampInt(5.9, 2, 8), 5);
8+
assert.equal(core.clampInt(-1, 2, 8), 2);
9+
assert.equal(core.clampInt(99, 2, 8), 8);
10+
assert.equal(core.clampInt('not-a-number', 2, 8), 2);
11+
});
12+
13+
test('makeModel, getProc, anyInCS, alivePids, ringNextAlive basics', () => {
14+
const tr = core.makeModel(4, 'TokenRing');
15+
assert.equal(tr.algorithm, 'TokenRing');
16+
assert.equal(core.getProc(tr, 'P1').id, 'P1');
17+
assert.equal(core.anyInCS(tr), null);
18+
assert.deepEqual(core.alivePids(tr), ['P1', 'P2', 'P3', 'P4']);
19+
assert.equal(core.ringNextAlive(tr, 'P1'), 'P2');
20+
21+
core.crashProcess(tr, 'P2');
22+
assert.deepEqual(core.alivePids(tr), ['P1', 'P3', 'P4']);
23+
assert.equal(core.ringNextAlive(tr, 'P1'), 'P3');
24+
25+
const ra = core.makeModel(3, 'RA');
26+
assert.equal(ra.algorithm, 'RA');
27+
});
28+
29+
test('checkSafety detects >1 process in CS', () => {
30+
const model = core.makeModel(3, 'RA');
31+
core.getProc(model, 'P1').inCS = true;
32+
core.getProc(model, 'P2').inCS = true;
33+
const safety = core.checkSafety(model);
34+
assert.equal(safety.ok, false);
35+
assert.match(safety.message, />1 process/);
36+
});
37+
38+
test('public actions reject unsupported / not interactive cases', () => {
39+
const tr = core.makeModel(3, 'TokenRing');
40+
tr.mode = 'script';
41+
assert.equal(core.requestCS(tr, 'P1').ok, false);
42+
assert.equal(core.releaseCS(tr, 'P1').ok, false);
43+
assert.equal(core.dropToken(tr).ok, false);
44+
assert.equal(core.regenerateToken(tr).ok, false);
45+
assert.equal(core.crashProcess(tr, 'P1').ok, false);
46+
assert.equal(core.recoverProcess(tr, 'P1').ok, false);
47+
48+
const ra = core.makeModel(3, 'RA');
49+
assert.equal(core.dropToken(ra).reason, 'unsupported_algorithm');
50+
assert.equal(core.regenerateToken(ra).reason, 'unsupported_algorithm');
51+
assert.equal(core.dropNextMessage(core.makeModel(3, 'TokenRing')).reason, 'unsupported_algorithm');
52+
assert.equal(core.toggleDropNextSend(core.makeModel(3, 'TokenRing')).reason, 'unsupported_algorithm');
53+
});
54+
55+
test('stepOnce reports unsupported algorithm paths', () => {
56+
const model = core.makeModel(3, 'RA');
57+
model.algorithm = 'Bogus';
58+
const res = core.stepOnce(model);
59+
assert.equal(res.reason, 'unsupported_algorithm');
60+
assert.match(lastTrace(model), /Unsupported algorithm/);
61+
62+
model.mode = 'script';
63+
model.script.events = [{ t: 0, op: 'noop' }];
64+
model.script.index = 0;
65+
const res2 = core.stepOnce(model);
66+
assert.equal(res2.reason, 'unsupported_algorithm');
67+
assert.match(lastTrace(model), /Unsupported algorithm in script/);
68+
});

tests/10_tokenring.test.mjs

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import test from 'node:test';
2+
import assert from 'node:assert/strict';
3+
import * as core from '../mutex_core.js';
4+
import { stepN, stepUntil, lastTrace, assertSafety, inCS } from './helpers.mjs';
5+
6+
test('TokenRing basic request / entry / release / pass', () => {
7+
const model = core.makeModel(4, 'TokenRing');
8+
9+
assert.equal(core.requestCS(model, 'P1').ok, true);
10+
stepUntil(core, model, () => inCS(core, model, 'P1'), 50, 'P1 enters');
11+
assertSafety(core, model);
12+
13+
assert.equal(core.requestCS(model, 'P2').ok, true);
14+
stepN(core, model, 3);
15+
assert.equal(inCS(core, model, 'P1'), true);
16+
assert.equal(inCS(core, model, 'P2'), false);
17+
assert.match(lastTrace(model), /release required/);
18+
19+
assert.equal(core.releaseCS(model, 'P1').ok, true);
20+
stepUntil(core, model, () => inCS(core, model, 'P2'), 100, 'P2 enters');
21+
assertSafety(core, model);
22+
});
23+
24+
test('TokenRing invalid request / release cases', () => {
25+
const model = core.makeModel(3, 'TokenRing');
26+
assert.equal(core.requestCS(model, 'P9').reason, 'unknown_process');
27+
assert.equal(core.releaseCS(model, 'P1').reason, 'not_in_cs');
28+
29+
assert.equal(core.requestCS(model, 'P1').ok, true);
30+
assert.equal(core.requestCS(model, 'P1').reason, 'already_requesting');
31+
32+
stepUntil(core, model, () => inCS(core, model, 'P1'), 20, 'P1 enters');
33+
assert.equal(core.requestCS(model, 'P1').reason, 'already_in_cs');
34+
35+
core.crashProcess(model, 'P2');
36+
assert.equal(core.requestCS(model, 'P2').reason, 'crashed');
37+
});
38+
39+
test('TokenRing token loss and regeneration', () => {
40+
const model = core.makeModel(4, 'TokenRing');
41+
42+
assert.equal(core.dropToken(model).ok, true);
43+
assert.equal(model.token.lost, true);
44+
45+
assert.equal(core.requestCS(model, 'P1').ok, true);
46+
stepN(core, model, 2);
47+
assert.equal(inCS(core, model, 'P1'), false);
48+
assert.match(lastTrace(model), /token is lost/);
49+
50+
assert.equal(core.regenerateToken(model).ok, true);
51+
stepUntil(core, model, () => inCS(core, model, 'P1'), 20, 'P1 enters after regen');
52+
});
53+
54+
test('TokenRing crash / recover edge cases', () => {
55+
const model = core.makeModel(3, 'TokenRing');
56+
57+
assert.equal(model.token.holder, 'P1');
58+
assert.equal(core.crashProcess(model, 'P1').ok, true);
59+
assert.equal(model.token.lost, true);
60+
assert.equal(core.crashProcess(model, 'P1').reason, 'already_crashed');
61+
62+
assert.equal(core.recoverProcess(model, 'P1').ok, true);
63+
assert.equal(core.recoverProcess(model, 'P1').reason, 'not_crashed');
64+
65+
assert.equal(core.regenerateToken(model).ok, true);
66+
67+
core.requestCS(model, 'P1');
68+
stepUntil(core, model, () => inCS(core, model, 'P1'), 20, 'P1 enters');
69+
70+
core.crashProcess(model, 'P1');
71+
const r = core.stepOnce(model);
72+
assert.equal(r.reason, 'token_lost');
73+
assert.match(model.trace.map(e => e.text).join('\n'), /crashed in the critical section|token is lost/);
74+
});
75+
76+
test('TokenRing script-mode events cover normal and invalid paths', () => {
77+
const demo = {
78+
type: 'MutexDemo',
79+
algorithm: 'TokenRing',
80+
processes: ['P1', 'P2'],
81+
events: [
82+
{},
83+
{ t: 1, op: 'holdToken', on: 'P2' },
84+
{ t: 2, op: 'passToken', to: 'P1' },
85+
{ t: 3, op: 'requestCS', on: 'P1' },
86+
{ t: 4, op: 'releaseCS', on: 'P1' },
87+
{ t: 5, op: 'releaseCS', on: 'P2' },
88+
{ t: 6, op: 'dropToken' },
89+
{ t: 7, op: 'regenerateToken', on: 'P2' },
90+
{ t: 8, op: 'crash', on: 'P2' },
91+
{ t: 9, op: 'recover', on: 'P2' },
92+
{ t: 10, op: 'unknownOp' }
93+
]
94+
};
95+
96+
const model = core.loadFromJsonObject(demo);
97+
assert.equal(model.mode, 'script');
98+
99+
stepN(core, model, demo.events.length + 2);
100+
const trace = model.trace.map(e => e.text).join('\n');
101+
assert.match(trace, /Loaded scripted scenario/);
102+
assert.match(trace, /Script finished/);
103+
});

tests/20_ra.test.mjs

Whitespace-only changes.

tests/30_io.test.mjs

Whitespace-only changes.

tests/helpers.mjs

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import assert from 'node:assert/strict';
2+
3+
export function stepN(core, model, n) {
4+
for (let i = 0; i < n; i += 1) {
5+
core.stepOnce(model);
6+
}
7+
}
8+
9+
export function stepUntil(core, model, predicate, maxSteps = 2000, label = 'condition') {
10+
for (let i = 0; i < maxSteps; i += 1) {
11+
const result = core.stepOnce(model);
12+
if (predicate(result)) return { steps: i + 1, result };
13+
}
14+
throw new Error(`Timeout waiting for ${label} after ${maxSteps} steps`);
15+
}
16+
17+
export function lastTrace(model) {
18+
if (!Array.isArray(model.trace) || model.trace.length === 0) return '';
19+
return model.trace[model.trace.length - 1].text;
20+
}
21+
22+
export function assertSafety(core, model, msg = 'safety should be OK') {
23+
assert.equal(core.checkSafety(model).ok, true, msg);
24+
}
25+
26+
export function inCS(core, model, pid) {
27+
return !!core.getProc(model, pid)?.inCS;
28+
}

0 commit comments

Comments
 (0)