Fix parIRL=1 crash: zero recoil cutoff makes the cascade diverge
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.
This commit is contained in:
@@ -17,16 +17,95 @@ DONE - Cap valEnergy magnitude at 60000 eV (positive incidence mode and the
|
|||||||
value in runSimulationSequence() and CreateInpFile().
|
value in runSimulationSequence() and CreateInpFile().
|
||||||
- Warn when a shared configuration fixed the RanSeed: every scan point then uses
|
- Warn when a shared configuration fixed the RanSeed: every scan point then uses
|
||||||
the same generator, which can produce statistically misleading results.
|
the same generator, which can produce statistically misleading results.
|
||||||
- Cap client-controlled numberProj magnitude: an unbounded projectile count (e.g.
|
DONE (advisory, not a hard cap) - Warn on the web when numberProj exceeds 25000
|
||||||
1e9) does not terminate within the 300 s watchdog and can flood scan points
|
per point. Measured cost model on a 3.2 GHz core, single-threaded engine:
|
||||||
(empirically verified: 1e9 did not finish in 15 s). GUI ProjNumberLimit() is
|
runtime is linear in numberProj and linear in energy, and cost per projectile
|
||||||
currently dead code (defined at TrimSPlib.js:3516, never wired to an event).
|
is dominated by the number of nuclear collisions along the track, so the
|
||||||
|
expensive axis is the LIGHT projectiles, not the heavy ones. Projectile ->
|
||||||
|
Si, Maxwellian 60 keV, ZBL potential, ms/projectile: muon 3.04, H 3.30,
|
||||||
|
He 4.06 (worst), Li-8 3.91, Be-11 3.64, B-12 3.19, Mg-31 1.34, Ar 0.83.
|
||||||
|
Checked and found NOT to affect runtime: layer count (1..60 layers is flat,
|
||||||
|
the species lookup is a binary search), depth increment dz (0.1 A == 20 A),
|
||||||
|
and grazing incidence (ALPHA 89 deg is cheaper - the ion skims out).
|
||||||
|
NH=25000 at the worst case measured 98.6/99.5/100.1 s on the 1.3.2 binary and
|
||||||
|
101.5/100.7/101.7 s on a fresh 1.4.0 build, i.e. ~3x margin under the 300 s
|
||||||
|
TRIMSP_CMD_TIMEOUT. Deliberately warning-only: the value is advisory, the run
|
||||||
|
still starts, and the user keeps the final say. Web-only, because the
|
||||||
|
standalone app has no watchdog. TRIMSP_WEB_PROJ_ADVISORY in TrimSPlib.js;
|
||||||
|
uses the max of the sweep when scanning numberProj. The dead GUI
|
||||||
|
ProjNumberLimit() (TrimSPlib.js:3516) was left alone - it clamps at 5000,
|
||||||
|
which is below the 50000-100000 used by the publication test configs, so
|
||||||
|
wiring it up as-is would have clamped the regression suite.
|
||||||
- Persisting the effective per-point seed: the seed already lives in each
|
- Persisting the effective per-point seed: the seed already lives in each
|
||||||
archived *.inp (all three deck slots), so reproducibility does not require the
|
archived *.inp (all three deck slots), so reproducibility does not require the
|
||||||
*.seq header change; extending *.seq would shift the 999(A20,1x) layer slots
|
*.seq header change; extending *.seq would shift the 999(A20,1x) layer slots
|
||||||
and break getSequenceColumnInfo(). Surfacing it in run details instead is
|
and break getSequenceColumnInfo(). Surfacing it in run details instead is
|
||||||
deferred - not sure it is needed.
|
deferred - not sure it is needed.
|
||||||
|
|
||||||
|
Out-of-bounds recoil write in the sputtering path (parIRL >= 1)
|
||||||
|
=============================================================
|
||||||
|
|
||||||
|
FIXED - the engine now refuses the divergent input instead of corrupting memory.
|
||||||
|
|
||||||
|
Symptom, as originally reported: setting parIRL (the recoil/sputter output
|
||||||
|
level) to 1 crashed the engine with SIGSEGV, reproducibly 5/5 seeds, and end
|
||||||
|
to end through singleTrimSP.cgi as exitCode 139 / ok:false. parIRL=0 was never
|
||||||
|
affected.
|
||||||
|
|
||||||
|
Root cause was NOT the KO dimension. The first diagnosis blamed
|
||||||
|
KO(600,MAXNL5,2) at trimspNL.F:114 (debug: "Index '601' of dimension 1 of
|
||||||
|
array 'ko' above upper bound of 600"), on the grounds that the other
|
||||||
|
recoil-list arrays are 2000 wide. Enlarging KO to 2000 does not help: with
|
||||||
|
KO(2000) the run still dies, because the cascade overruns all of them.
|
||||||
|
|
||||||
|
The actual cause is a non-converging cascade driven by parERC = 0. The
|
||||||
|
primary knock-on test is `IF(DEN(IV).LE.ERC) GO TO 6`, so with a zero recoil
|
||||||
|
cutoff every atom that receives any energy at all is respawned as a recoil on
|
||||||
|
every cascade generation. The branching ratio exceeds one, so NREC1 and NREC2
|
||||||
|
grow geometrically instead of draining:
|
||||||
|
|
||||||
|
NREC2 = 81, 153, 269, 455, 773, 1317, ... (parERC=0, parIRL=1)
|
||||||
|
|
||||||
|
and the list-2/list-1 loop at label 83 never terminates. Instrumented runs
|
||||||
|
show the block is never even entered when parIRL=0. parERC defaults to 0.0
|
||||||
|
in the GUI (TrimSP.html:293) and tests/Si_1layer/Si_1layer_E19000.inp carries
|
||||||
|
0.0, so the failing combination is the default one - no unusual user action
|
||||||
|
is required to hit it.
|
||||||
|
|
||||||
|
Evidence that a real cutoff converges: forcing parERC to 0.02, 0.05 or 0.2
|
||||||
|
makes parIRL=1 finish normally (exit 0), and the peak cascade depth is only
|
||||||
|
103, well inside KO's 600. KO was therefore left at 600, and the resize was
|
||||||
|
reverted (it cost 7.7 MiB of .lbss and fixed nothing).
|
||||||
|
|
||||||
|
Changes in fortran/trimspNL.F:
|
||||||
|
- After readGuiInput, reject parIRL>=1 with parERC<=0 using a clear message
|
||||||
|
and STOP 1, before any transport work.
|
||||||
|
- The `IF(NREC2.GT.2000)` guard now stops with a non-zero status instead of
|
||||||
|
falling through into the out-of-bounds write loop, so any future
|
||||||
|
non-convergent cascade fails cleanly rather than corrupting memory.
|
||||||
|
|
||||||
|
The GUI now catches this before the engine is ever invoked:
|
||||||
|
- handleRecoilModeChange (wired to the parIRL widget) fills parERC with
|
||||||
|
TRIMSP_MIN_RECOIL_CUTOFF = 0.02 when recoil calculation is switched on with
|
||||||
|
a cutoff of 0, and says so in a transient warning.
|
||||||
|
- A pre-run check in runSimulationSequence refuses the run if the user then
|
||||||
|
insists on parIRL=1 with parERC=0, reports it in the status bar plus a
|
||||||
|
transient warning, and focuses the parERC field. The handler is deliberately
|
||||||
|
not wired to parERC, so retyping the cutoff is what "insisting" means and is
|
||||||
|
not silently overridden. This also covers shared-configuration links, which
|
||||||
|
assign widget values directly and so bypass the change handler.
|
||||||
|
- The parIRL tooltip documents the requirement.
|
||||||
|
|
||||||
|
Verified: parERC=0/parIRL=1 -> exit 1 with the new message; parERC=0.02 with
|
||||||
|
parIRL=0 or 1 -> exit 0; parERC=0 with parIRL=0 -> exit 0 and byte-identical
|
||||||
|
.rge (and an identical .out apart from timestamps/elapsed/filenames) versus
|
||||||
|
the pre-fix binary. tests/cgi-tests.sh 18/18, plus
|
||||||
|
tests/recoil-cutoff-test.js (12 cases over the shipped cutoff code) for the
|
||||||
|
GUI behaviour above.
|
||||||
|
|
||||||
|
Minimal repro: tests/Si_1layer/Si_1layer_E19000.inp with the last field of
|
||||||
|
line 2 changed from 0 to 1 (parERC is field 10 of line 1, still 0.0).
|
||||||
|
|
||||||
Deferred follow-ups from the C-list CGI hardening
|
Deferred follow-ups from the C-list CGI hardening
|
||||||
=================================================
|
=================================================
|
||||||
|
|
||||||
|
|||||||
+4
-2
@@ -395,12 +395,14 @@
|
|||||||
No recoils are generated (i.e., no sputtering effects);
|
No recoils are generated (i.e., no sputtering effects);
|
||||||
used to speed up the calculation if only projectile ranges are of interest.
|
used to speed up the calculation if only projectile ranges are of interest.
|
||||||
</li>
|
</li>
|
||||||
<li value="1">Calculate collision recoils.</li>
|
<li value="1">Calculate collision recoils. Requires a
|
||||||
|
parERC recoil cutoff above zero; 0.02 is filled in
|
||||||
|
automatically.</li>
|
||||||
</ol>
|
</ol>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<input name="parIRL" id="parIRL" type="number" step="1" min="0" max="1" value="0">
|
<input name="parIRL" id="parIRL" type="number" step="1" min="0" max="1" value="0" onchange="handleRecoilModeChange();">
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -3804,6 +3804,53 @@ var TRIMSP_ENERGY_CAP_EV = 60000;
|
|||||||
// silently truncated. Restrict the seed to |v| <= 2^31-1.
|
// silently truncated. Restrict the seed to |v| <= 2^31-1.
|
||||||
var TRIMSP_SEED_MAX_ABS = 2147483647;
|
var TRIMSP_SEED_MAX_ABS = 2147483647;
|
||||||
|
|
||||||
|
// Advisory projectile count for the web backend. This is not a hard limit: the
|
||||||
|
// engine is single threaded and its cost per projectile is dominated by the
|
||||||
|
// number of nuclear collisions along the track, so the same count can differ by
|
||||||
|
// more than 5x across the projectile menu. Measured worst case is helium into a
|
||||||
|
// light heavy-ish target at the 60 keV energy cap with the ZBL potential, at
|
||||||
|
// about 4.0 ms per projectile on a 3.2 GHz core, which puts 25000 points at
|
||||||
|
// roughly 100 s. The web backend watchdog (TRIMSP_CMD_TIMEOUT, 300 s) then
|
||||||
|
// still has about a 3x margin, so this count normally completes while slower or
|
||||||
|
// more heavily loaded deployments keep the watchdog as a backstop. The warning
|
||||||
|
// is advisory only; the user keeps the final say.
|
||||||
|
var TRIMSP_WEB_PROJ_ADVISORY = 25000;
|
||||||
|
|
||||||
|
// Recoil cutoff substituted when the user turns on recoil calculation with a
|
||||||
|
// cutoff of zero. A zero cutoff makes the transport respawn every recoiling
|
||||||
|
// atom on every cascade generation, so NREC1/NREC2 grow geometrically instead
|
||||||
|
// of draining and the run overruns the recoil arrays. The engine refuses that
|
||||||
|
// combination, so the GUI supplies a usable value instead of letting the run
|
||||||
|
// fail after the user presses Run.
|
||||||
|
var TRIMSP_MIN_RECOIL_CUTOFF = 0.02;
|
||||||
|
|
||||||
|
// True when the recoil calculation is enabled without a usable recoil cutoff.
|
||||||
|
// The parIRL and parERC widgets are plain number inputs and a shared
|
||||||
|
// configuration assigns their values directly, so this can be reached without
|
||||||
|
// going through the change handler below.
|
||||||
|
function recoilCutoffIsUnusable() {
|
||||||
|
let irl = Number(document.getElementById("parIRL").value);
|
||||||
|
let cutoff = Number(document.getElementById("parERC").value);
|
||||||
|
return Number.isFinite(irl) && irl >= 1
|
||||||
|
&& (!Number.isFinite(cutoff) || cutoff <= 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Called when the parIRL widget changes. Turning recoil calculation on with no
|
||||||
|
// cutoff is filled in for the user here, because that is a value they almost
|
||||||
|
// never intend to type. Deliberately not wired to parERC: retyping the cutoff
|
||||||
|
// afterwards is how a user insists on the combination, and the pre-run check
|
||||||
|
// then refuses the run instead of silently overriding them.
|
||||||
|
function handleRecoilModeChange() {
|
||||||
|
if (!recoilCutoffIsUnusable()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let cutoffField = document.getElementById("parERC");
|
||||||
|
cutoffField.value = TRIMSP_MIN_RECOIL_CUTOFF;
|
||||||
|
showTransientWarning(
|
||||||
|
"Recoil calculation needs a cutoff, so parERC was set to "
|
||||||
|
+ TRIMSP_MIN_RECOIL_CUTOFF + ".", cutoffField, 6000);
|
||||||
|
}
|
||||||
|
|
||||||
// Returns an empty string when valid, otherwise a message that asks the user
|
// Returns an empty string when valid, otherwise a message that asks the user
|
||||||
// to change the offending value.
|
// to change the offending value.
|
||||||
function validateEnergyValue(raw) {
|
function validateEnergyValue(raw) {
|
||||||
@@ -4056,6 +4103,46 @@ async function runSimulationSequence() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Advisory only, and web only: the standalone app runs the engine locally
|
||||||
|
// with no watchdog, so a long run is the user's own choice there. A number
|
||||||
|
// of projectiles that scales badly with the collision count can still exceed
|
||||||
|
// the backend watchdog, so warn once per run without blocking it. For a
|
||||||
|
// numberProj sweep the scan values are authoritative, otherwise the base
|
||||||
|
// value is what every point uses.
|
||||||
|
if (amIWeb()) {
|
||||||
|
const projValues = (ScanAttrib === "numberProj")
|
||||||
|
? SValues
|
||||||
|
: [Number(All["numberProj"])];
|
||||||
|
let worstProj = 0;
|
||||||
|
for (var projIndex = 0; projIndex < projValues.length; projIndex++) {
|
||||||
|
let projValue = Number(projValues[projIndex]);
|
||||||
|
if (Number.isFinite(projValue) && projValue > worstProj) {
|
||||||
|
worstProj = projValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (worstProj > TRIMSP_WEB_PROJ_ADVISORY) {
|
||||||
|
showTransientWarning(
|
||||||
|
"Warning: " + worstProj + " projectiles per point may exceed the web "
|
||||||
|
+ "time limit and fail. Runs above ~" + TRIMSP_WEB_PROJ_ADVISORY
|
||||||
|
+ " are safest; the run will start anyway.",
|
||||||
|
document.getElementById("numberProj"), 8000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hard stop, unlike the projectile advisory above: with a zero recoil
|
||||||
|
// cutoff the cascade does not converge, so the engine exits with an error
|
||||||
|
// instead of producing results. Refuse the run here and say which field to
|
||||||
|
// change, rather than letting the backend fail with a transport message.
|
||||||
|
if (recoilCutoffIsUnusable()) {
|
||||||
|
let cutoffField = document.getElementById("parERC");
|
||||||
|
let message = "parIRL=1 needs a parERC recoil cutoff above 0; "
|
||||||
|
+ TRIMSP_MIN_RECOIL_CUTOFF + " is a common choice.";
|
||||||
|
updateStatusBar({ run: message });
|
||||||
|
showTransientWarning("Not running: " + message, cutoffField, 8000);
|
||||||
|
cutoffField.focus();
|
||||||
|
return(0);
|
||||||
|
}
|
||||||
|
|
||||||
// Freeze the initial GUI state and expand the scan into independent jobs.
|
// Freeze the initial GUI state and expand the scan into independent jobs.
|
||||||
// Each job gets its own input file name and a deterministic seed offset so
|
// Each job gets its own input file name and a deterministic seed offset so
|
||||||
// the results are reproducible even when runs finish out of order.
|
// the results are reproducible even when runs finish out of order.
|
||||||
|
|||||||
@@ -111,6 +111,12 @@ C
|
|||||||
& ,ISPIS(MAXNL5),ISPOP(MAXNL5),ISPOS(MAXNL5)
|
& ,ISPIS(MAXNL5),ISPOP(MAXNL5),ISPOS(MAXNL5)
|
||||||
INTEGER*4 ITSP(MAXNL5),ISPALT(MAXNL)
|
INTEGER*4 ITSP(MAXNL5),ISPALT(MAXNL)
|
||||||
& ,ISPIPT(MAXNL5),ISPIST(MAXNL5),ISPOPT(MAXNL5),ISPOST(MAXNL5)
|
& ,ISPIPT(MAXNL5),ISPIST(MAXNL5),ISPOPT(MAXNL5),ISPOST(MAXNL5)
|
||||||
|
C KO flags each recoil as a primary knock-on or a cascade product. Its
|
||||||
|
C first index is a recoil-list index, and 600 is deep enough for every
|
||||||
|
C cutoff that yields a converging cascade (measured peak is 103 with
|
||||||
|
C ERC=0.02). See the ERC/IRL check after readGuiInput below: with a zero
|
||||||
|
C recoil cutoff the cascade does not converge at all, and it overruns
|
||||||
|
C these 600 entries as well as the 2000-deep recoil lists.
|
||||||
INTEGER*4 KO(600,MAXNL5,2)
|
INTEGER*4 KO(600,MAXNL5,2)
|
||||||
INTEGER*4 MEAB(MAXD2,22),MAGB(62,22),MEAGB(MAXD2,36,22)
|
INTEGER*4 MEAB(MAXD2,22),MAGB(62,22),MEAGB(MAXD2,36,22)
|
||||||
& ,MEABL(75,21),MEPB(MAXD2,MAXD2)
|
& ,MEABL(75,21),MEPB(MAXD2,MAXD2)
|
||||||
@@ -569,6 +575,24 @@ C GUI file layout is isolated from the transport code.
|
|||||||
& ED,BE,CH1,CH2,CH3,CH4,CH5)
|
& ED,BE,CH1,CH2,CH3,CH4,CH5)
|
||||||
|
|
||||||
|
|
||||||
|
C A zero recoil cutoff makes the cascade diverge. The primary knock-on
|
||||||
|
C test below is IF(DEN(IV).LE.ERC) GO TO 6, so with ERC=0 every atom that
|
||||||
|
C receives any energy at all is respawned as a recoil on every cascade
|
||||||
|
C generation. The branching ratio then exceeds one, NREC1 and NREC2 grow
|
||||||
|
C geometrically instead of draining, and the run overruns the recoil
|
||||||
|
C arrays. Measured on tests/Si_1layer/Si_1layer_E19000.inp with
|
||||||
|
C parIRL=1: NREC2 passes 2000 within a single projectile, whereas a
|
||||||
|
C cutoff of 0.02 keeps the peak at 103. parERC defaults to 0.0 in the
|
||||||
|
C GUI, so this combination is reachable without the user doing anything
|
||||||
|
C unusual. Refuse it here, before any transport work, instead of
|
||||||
|
C corrupting memory later on.
|
||||||
|
if (irl.ge.1 .and. erc.le.0.d0) then
|
||||||
|
write(*,*) 'ERROR : parIRL=1 requires a positive parERC cutoff.'
|
||||||
|
write(*,*) ' parERC=0 makes the recoil cascade diverge'
|
||||||
|
write(*,*) ' and overrun the recoil arrays. Set parERC to'
|
||||||
|
write(*,*) ' a positive value (0.02 is a common choice).'
|
||||||
|
STOP 1
|
||||||
|
endif
|
||||||
C=======================================================================
|
C=======================================================================
|
||||||
C OPEN OUTPUT FILES AND START CLOCK
|
C OPEN OUTPUT FILES AND START CLOCK
|
||||||
C=======================================================================
|
C=======================================================================
|
||||||
@@ -1401,11 +1425,18 @@ C
|
|||||||
MAXA=MAX0(MAXA,NREC2)
|
MAXA=MAX0(MAXA,NREC2)
|
||||||
NALL=NALL+NREC2
|
NALL=NALL+NREC2
|
||||||
NREC1=0
|
NREC1=0
|
||||||
|
C Guard against a cascade that does not fit the recoil lists. A
|
||||||
|
C non-converging cascade reaches this test with NREC2 far above any array
|
||||||
|
C bound, so the old fall-through into the processing loop below overran
|
||||||
|
C every recoil array and killed the run with SIGSEGV. A cascade that does
|
||||||
|
C not fit is not recoverable, so stop with a non-zero status rather than
|
||||||
|
C let the CGI report a truncated run as a success.
|
||||||
IF(NREC2.GT.2000) GO TO 8885
|
IF(NREC2.GT.2000) GO TO 8885
|
||||||
GO TO 8886
|
GO TO 8886
|
||||||
8885 WRITE(6,8887)
|
8885 WRITE(6,8887)
|
||||||
8887 FORMAT(1X,'ERROR : DIMENSION IN THE RECOIL LOOP ',
|
8887 FORMAT(1X,'ERROR : DIMENSION IN THE RECOIL LOOP ',
|
||||||
& 'MUST BE INCREASED')
|
& 'MUST BE INCREASED')
|
||||||
|
STOP 1
|
||||||
8886 CONTINUE
|
8886 CONTINUE
|
||||||
C
|
C
|
||||||
C PROCESS THE PARTICLES IN LIST 2
|
C PROCESS THE PARTICLES IN LIST 2
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
// 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);
|
||||||
Reference in New Issue
Block a user