parIRL=1 crashed the engine with SIGSEGV (CGI exitCode 139) at both GUI defaults, so the sputtering path has never actually worked. The first diagnosis blamed KO(600,MAXNL5,2) being shallower than the 2000-wide recoil lists. That is wrong: enlarging KO to 2000 does not help, because the cascade overruns all of them. The real cause is parERC=0, which is the GUI default and what tests/Si_1layer/Si_1layer_E19000.inp carries. The primary knock-on test is IF(DEN(IV).LE.ERC) GO TO 6, so with no cutoff every atom receiving any energy is respawned as a recoil on each cascade generation; the branching ratio exceeds one and NREC1/NREC2 grow geometrically (81, 153, 269, 455, 773, 1317, ...) instead of draining, so the list-2/list-1 loop at label 83 never terminates. Forcing a real cutoff converges, with a peak depth of only 103, so KO was left at 600. fortran/trimspNL.F: - Refuse parIRL>=1 with parERC<=0 after readGuiInput, before any transport work, with a message naming the field to change. - The IF(NREC2.GT.2000) guard now stops non-zero instead of falling through into the out-of-bounds write loop, so any future non-convergent cascade fails cleanly instead of corrupting memory. TrimSPlib.js / TrimSP.html, so this is caught before the engine is invoked: - handleRecoilModeChange, wired to the parIRL widget, fills parERC with TRIMSP_MIN_RECOIL_CUTOFF = 0.02 when recoils are switched on with a cutoff of 0, and says so. - A pre-run check refuses the run if the user then insists on parIRL=1 with parERC=0, reporting via the status bar and a transient warning and focusing parERC. Deliberately not wired to parERC, so retyping the cutoff is what insisting means and is not silently overridden; this also covers shared config links, which assign widget values directly. - The parIRL tooltip documents the requirement. Tests: tests/recoil-cutoff-test.js (12 cases) lifts the shipped cutoff code out of TrimSPlib.js and runs it against a document stub. Verified against a fresh build: parERC=0/parIRL=1 now exits 1 with the new message instead of 139, parERC=0.02 completes with parIRL=0 or 1, and parERC=0/parIRL=0 output is unchanged (.rge byte-identical). tests/cgi-tests.sh 18/18.
109 lines
4.1 KiB
JavaScript
109 lines
4.1 KiB
JavaScript
// Checks the GUI handling of the parIRL/parERC combination that used to crash
|
|
// the engine. A zero recoil cutoff makes the transport cascade diverge, so the
|
|
// GUI supplies a cutoff when recoils are switched on and refuses the run if the
|
|
// user insists on the zero cutoff.
|
|
//
|
|
// The real functions are lifted out of TrimSPlib.js and run against a minimal
|
|
// document stub, so this exercises shipped code rather than a copy of it.
|
|
// Run with: node tests/recoil-cutoff-test.js
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
const libPath = path.join(__dirname, '..', 'TrimSPlib.js');
|
|
const src = fs.readFileSync(libPath, 'utf8');
|
|
|
|
function grab(re) {
|
|
const m = src.match(re);
|
|
if (!m) {
|
|
throw new Error('pattern not found in ' + libPath + ': ' + re);
|
|
}
|
|
return m[0];
|
|
}
|
|
|
|
const fields = {};
|
|
let shown = [];
|
|
let statusMsg = null;
|
|
|
|
global.document = {
|
|
getElementById(id) {
|
|
if (!(id in fields)) {
|
|
fields[id] = { value: '', focus() { this.focused = true; } };
|
|
}
|
|
return fields[id];
|
|
}
|
|
};
|
|
global.showTransientWarning = (message) => { shown.push(message); };
|
|
global.updateStatusBar = (options) => {
|
|
if (options && options.run) { statusMsg = options.run; }
|
|
};
|
|
|
|
eval([
|
|
grab(/var TRIMSP_MIN_RECOIL_CUTOFF = [\d.]+;/),
|
|
grab(/function recoilCutoffIsUnusable\(\) \{[\s\S]*?\n\}/),
|
|
grab(/function handleRecoilModeChange\(\) \{[\s\S]*?\n\}/),
|
|
].join('\n\n'));
|
|
|
|
// The pre-run refusal, lifted verbatim out of runSimulationSequence. It keeps
|
|
// that function's return convention, where 0 means "aborted before the engine
|
|
// was invoked" and 1 means the run may proceed.
|
|
eval('function runGuardVerdict() {\n'
|
|
+ grab(/ if \(recoilCutoffIsUnusable\(\)\) \{[\s\S]*?\n \}\n/)
|
|
+ ' return 1;\n}\n');
|
|
|
|
function reset(irl, erc) {
|
|
for (const key of Object.keys(fields)) { delete fields[key]; }
|
|
const makeField = (value) => ({
|
|
value: value,
|
|
focus() { this.focused = true; },
|
|
});
|
|
fields.parIRL = makeField(String(irl));
|
|
fields.parERC = makeField(String(erc));
|
|
shown = [];
|
|
statusMsg = null;
|
|
}
|
|
|
|
const cases = [
|
|
// description, IRL, ERC, touchIRL, fills, refuses
|
|
['default GUI state', 0, '0.0', false, false, false],
|
|
['parIRL 0->1, parERC=0 default', 1, '0.0', true, true, false],
|
|
['user insists, edits parERC=0', 1, '0', false, false, true],
|
|
['user sets parERC back to 0', 1, '0.0', false, false, true],
|
|
['valid parIRL=1 with cutoff', 1, '0.02', false, false, false],
|
|
['parIRL 0->1, cutoff already set', 1, '0.02', true, false, false],
|
|
['valid parIRL=1, large cutoff', 1, '15', false, false, false],
|
|
['parIRL=0 with parERC=0', 0, '0.0', false, false, false],
|
|
['parIRL 0->1, negative parERC', 1, '-1', true, true, false],
|
|
['parIRL 0->1, blank parERC', 1, '', true, true, false],
|
|
['parIRL 0->1, non-numeric parERC', 1, 'abc', true, true, false],
|
|
['non-numeric parIRL treated as off', 'x', '0.0', true, false, false],
|
|
];
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
for (const [name, irl, erc, touchIRL, expectFilled, expectRefused] of cases) {
|
|
reset(irl, erc);
|
|
const before = fields.parERC.value;
|
|
// The handler is wired to the parIRL widget only, so it runs when the user
|
|
// changes parIRL and not when they go back and retype the cutoff themselves.
|
|
if (touchIRL) { handleRecoilModeChange(); }
|
|
const filled = fields.parERC.value !== before;
|
|
const refused = runGuardVerdict() === 0;
|
|
const refusedOk = (refused === expectRefused);
|
|
// A refused run must actually say something in both places.
|
|
const explained = !refused
|
|
|| (shown.length > 0 && typeof statusMsg === 'string' && statusMsg.length > 0);
|
|
|
|
const ok = (filled === expectFilled) && refusedOk && explained;
|
|
if (ok) { passed++; } else { failed++; }
|
|
console.log((ok ? 'PASS ' : 'FAIL ') + name.padEnd(34)
|
|
+ 'parERC=' + String(fields.parERC.value).padEnd(6)
|
|
+ (filled ? 'filled in' : 'left alone').padEnd(13)
|
|
+ (refused ? 'refused' : 'runs')
|
|
+ (shown.length ? ' | "' + shown[shown.length - 1] + '"' : ''));
|
|
}
|
|
|
|
console.log('\n' + passed + ' passed, ' + failed + ' failed');
|
|
process.exit(failed ? 1 : 0);
|