batch delete: report an already-absent object as such
A delete run whose DELETE answered 6A88/6A82 is still a success (the
object is gone, the goal is met) but it is no longer reported as a plain
"OK": the attempt result is 'already_absent', the counts gain that
bucket ("Deleted: N - Already absent: M"), the verdict reads "Already
absent" and the notice says the card was already absent. A reinstall's
tolerated pre-delete keeps the install outcome (the install is the
point).
1024 Python + 1063 frontend; simple-v471.
This commit is contained in:
@@ -22,6 +22,15 @@ every change carries a descriptive commit message.
|
|||||||
Start click (browser autoplay policy); reduced-motion users get the static card-above-
|
Start click (browser autoplay policy); reduced-motion users get the static card-above-
|
||||||
the-slot picture.
|
the-slot picture.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- A **batch delete that finds the object already gone** is still a success — the goal is met
|
||||||
|
and the run continues — but it is no longer reported as a plain "OK": the attempt's verdict
|
||||||
|
reads **Already absent**, the delete run's counts separate real deletions from no-ops
|
||||||
|
(`Deleted: N · Already absent: M`), and the notice says the card was already absent. The
|
||||||
|
counts gain an `already_absent` bucket (the attempt result is `already_absent`); a
|
||||||
|
reinstall's tolerated pre-delete is *not* affected — its outcome is still the install.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
- The prebuilt `style.css` had gone stale: a rebuild restored utility classes the UI
|
- The prebuilt `style.css` had gone stale: a rebuild restored utility classes the UI
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -324,7 +324,7 @@
|
|||||||
<tr><td class="py-1 px-2">Install Package (.cap file)</td><td class="py-1 px-2">Sends a <code class="font-mono text-sm">.cap</code> file to the card via the server: INSTALL[for load] → LOAD ×N → INSTALL[for install (+make selectable)]. The load file is split into LOAD APDUs of the <strong>LOAD block size</strong> (1–240 bytes of payload, 240 by default); each secured packet is delivered as a single SMS or, when larger, as a concatenated SMS-PP download of up to 5 segments, so a large <code class="font-mono text-sm">.cap</code> simply takes several SMS. As soon as the file is selected it is validated and its <strong>memory requirements are estimated</strong> in the form: the <strong>NVRAM requirement</strong> (the code image + persistent data, the GlobalPlatform Card Spec C6+C8 style total) and the RAM estimate. The same box reports the <strong>libraries the CAP is linked against</strong> (its Import component: library name, AID and the minimum version <code class="font-mono text-sm">≥ x.y</code>, with standard names and families resolved and a Java Card SDK hint where known), the package/applet identity and the component breakdown (sizes and shares in load-file order) — grouped under the <strong>Package</strong>, <strong>Requires</strong>, <strong>Memory</strong> and <strong>Components</strong> headings. The <strong>Platform</strong> line names the minimum Java Card level the card must provide (derived from the <code class="font-mono text-sm">javacard.framework</code> import; the header <code class="font-mono text-sm">int</code> flag additionally requires int support, Java Card 2.2+) and the <strong>ETSI release</strong> the UICC API imports come from (<code class="font-mono text-sm">uicc.toolkit</code> version → REL-N, per the workspace <code class="font-mono text-sm">sim-tools</code> corpus; shown with its basis in brackets): the JCRE checks every import against its own packages when it verifies the load file (JC VM spec 4.5.2), so a card whose platform is older rejects the LOAD as soon as the <strong>Import</strong> component is complete (the step line carries the card's remote SW and names the component the block completed, e.g. <em>completes Import</em>): the failure hint and the popup then point at the <strong>CAP compatibility test</strong> (one click — the same <code class="font-mono text-sm">.cap</code> is still selected) to find the missing package and version; a smaller LOAD block only helps when the block exceeds the card's buffer. A corrupt or wrong-format file fails the analysis, and <strong>Execute</strong> stays disabled until a successful analysis. The estimate is informational only and is never used for installation: NVRAM is the code image plus the persistent data (the GlobalPlatform C6+C8 style total, Table 11-48 — card memory management, rounding and the registry entry are not included, and the static field image is counted in both parts); the suggested <strong>C6</strong> follows the load file, and the suggested quotas are decimal byte counts (the hex in parentheses is how the C7/C8 install parameters encode them: C6 = non-volatile code, C7 = volatile RAM, C8 = non-volatile data). The <strong>Delete the package before installing (with its applications)</strong> checkbox prepends a cascade GP <code class="font-mono text-sm">DELETE</code> on the package AID (the load file with its modules and applications, P2=<code class="font-mono text-sm">0x80</code>) — a leftover package blocks INSTALL[for load]; 'not found' (a fresh card) proceeds, any other delete failure stops the chain with its reason.</td></tr>
|
<tr><td class="py-1 px-2">Install Package (.cap file)</td><td class="py-1 px-2">Sends a <code class="font-mono text-sm">.cap</code> file to the card via the server: INSTALL[for load] → LOAD ×N → INSTALL[for install (+make selectable)]. The load file is split into LOAD APDUs of the <strong>LOAD block size</strong> (1–240 bytes of payload, 240 by default); each secured packet is delivered as a single SMS or, when larger, as a concatenated SMS-PP download of up to 5 segments, so a large <code class="font-mono text-sm">.cap</code> simply takes several SMS. As soon as the file is selected it is validated and its <strong>memory requirements are estimated</strong> in the form: the <strong>NVRAM requirement</strong> (the code image + persistent data, the GlobalPlatform Card Spec C6+C8 style total) and the RAM estimate. The same box reports the <strong>libraries the CAP is linked against</strong> (its Import component: library name, AID and the minimum version <code class="font-mono text-sm">≥ x.y</code>, with standard names and families resolved and a Java Card SDK hint where known), the package/applet identity and the component breakdown (sizes and shares in load-file order) — grouped under the <strong>Package</strong>, <strong>Requires</strong>, <strong>Memory</strong> and <strong>Components</strong> headings. The <strong>Platform</strong> line names the minimum Java Card level the card must provide (derived from the <code class="font-mono text-sm">javacard.framework</code> import; the header <code class="font-mono text-sm">int</code> flag additionally requires int support, Java Card 2.2+) and the <strong>ETSI release</strong> the UICC API imports come from (<code class="font-mono text-sm">uicc.toolkit</code> version → REL-N, per the workspace <code class="font-mono text-sm">sim-tools</code> corpus; shown with its basis in brackets): the JCRE checks every import against its own packages when it verifies the load file (JC VM spec 4.5.2), so a card whose platform is older rejects the LOAD as soon as the <strong>Import</strong> component is complete (the step line carries the card's remote SW and names the component the block completed, e.g. <em>completes Import</em>): the failure hint and the popup then point at the <strong>CAP compatibility test</strong> (one click — the same <code class="font-mono text-sm">.cap</code> is still selected) to find the missing package and version; a smaller LOAD block only helps when the block exceeds the card's buffer. A corrupt or wrong-format file fails the analysis, and <strong>Execute</strong> stays disabled until a successful analysis. The estimate is informational only and is never used for installation: NVRAM is the code image plus the persistent data (the GlobalPlatform C6+C8 style total, Table 11-48 — card memory management, rounding and the registry entry are not included, and the static field image is counted in both parts); the suggested <strong>C6</strong> follows the load file, and the suggested quotas are decimal byte counts (the hex in parentheses is how the C7/C8 install parameters encode them: C6 = non-volatile code, C7 = volatile RAM, C8 = non-volatile data). The <strong>Delete the package before installing (with its applications)</strong> checkbox prepends a cascade GP <code class="font-mono text-sm">DELETE</code> on the package AID (the load file with its modules and applications, P2=<code class="font-mono text-sm">0x80</code>) — a leftover package blocks INSTALL[for load]; 'not found' (a fresh card) proceeds, any other delete failure stops the chain with its reason.</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Batch install (.cap file, per card)</td><td class="py-1 px-2">The bulk workflow: one <code class="font-mono text-sm">.cap</code> and one set of install parameters, applied to card after card. <strong>Start batch</strong> needs no card — the server owns the loop: it waits for a card, installs the moment the equip finishes, then waits for the next one. Every card's <strong>preset is matched by ICCID</strong> and supplies the keyset (the ISD role TAR's default, v3.26.0), the keys and the counter; a card with no matching preset or no usable ISD keyset is reported and skipped. Each attempt lands in the list with its time, ICCID, preset, verdict and duration; a failure shows the failing step and the remote status word — the batch never retries and never stops on a failure (failures are yours to manage afterwards). A card is attempted <strong>once per run</strong>: re-equipping it reports “already processed” — restart the batch to retry it. <strong>Stop</strong> finishes the current card first (an interrupted LOAD chain can leave a half-loaded package) and then ends the run; <strong>Clear</strong> drops the finished report. The loop runs in the server, so closing the tab, switching views or reloading the page does not interrupt it; a server restart ends it. A batch and a test run exclude each other. The batch shares the install form, so the <strong>Delete the package before installing</strong> option applies to every card.</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Batch install (.cap file, per card)</td><td class="py-1 px-2">The bulk workflow: one <code class="font-mono text-sm">.cap</code> and one set of install parameters, applied to card after card. <strong>Start batch</strong> needs no card — the server owns the loop: it waits for a card, installs the moment the equip finishes, then waits for the next one. Every card's <strong>preset is matched by ICCID</strong> and supplies the keyset (the ISD role TAR's default, v3.26.0), the keys and the counter; a card with no matching preset or no usable ISD keyset is reported and skipped. Each attempt lands in the list with its time, ICCID, preset, verdict and duration; a failure shows the failing step and the remote status word — the batch never retries and never stops on a failure (failures are yours to manage afterwards). A card is attempted <strong>once per run</strong>: re-equipping it reports “already processed” — restart the batch to retry it. <strong>Stop</strong> finishes the current card first (an interrupted LOAD chain can leave a half-loaded package) and then ends the run; <strong>Clear</strong> drops the finished report. The loop runs in the server, so closing the tab, switching views or reloading the page does not interrupt it; a server restart ends it. A batch and a test run exclude each other. The batch shares the install form, so the <strong>Delete the package before installing</strong> option applies to every card.</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Batch reinstall app (per card)</td><td class="py-1 px-2">The repair workflow: for every equipped card, delete the applet instance (tolerating 'not found') and run INSTALL [for install] again from the already loaded package — no <code class="font-mono text-sm">.cap</code> and no LOAD. The fields are the single INSTALL [for install] parameters (Load File / Module / Instance AIDs, privileges, the toolkit/STK parameters, <em>make selectable</em>); the preset, keyset and counter come from each card's own preset like the other batch operations, and the attempts list shows both verdicts (delete and install).</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Batch reinstall app (per card)</td><td class="py-1 px-2">The repair workflow: for every equipped card, delete the applet instance (tolerating 'not found') and run INSTALL [for install] again from the already loaded package — no <code class="font-mono text-sm">.cap</code> and no LOAD. The fields are the single INSTALL [for install] parameters (Load File / Module / Instance AIDs, privileges, the toolkit/STK parameters, <em>make selectable</em>); the preset, keyset and counter come from each card's own preset like the other batch operations, and the attempts list shows both verdicts (delete and install).</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Batch delete (per card)</td><td class="py-1 px-2">Deletes one object on every equipped card: an application (instance only, P2=<code class="font-mono text-sm">0x00</code>) or a package with its applications (cascade, P2=<code class="font-mono text-sm">0x80</code>). Fields: the AID and the target. A 'not found' result (the object is already gone) is reported as such and counted OK; any other failure is reported with the card's status word.</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">Batch delete (per card)</td><td class="py-1 px-2">Deletes one object on every equipped card: an application (instance only, P2=<code class="font-mono text-sm">0x00</code>) or a package with its applications (cascade, P2=<code class="font-mono text-sm">0x80</code>). Fields: the AID and the target. A 'not found' result (the object is already gone) is reported as <strong>Already absent</strong> and counted separately from real deletions — it is still a success (the goal is met) and the run continues; any other failure is reported with the card's status word.</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">CAP compatibility test</td><td class="py-1 px-2">Answers one question without installing anything: <strong>can this card satisfy the CAP's import list?</strong> The server runs the format check, INSTALL[for load] and the LOAD blocks only up to the block that completes the <strong>Import</strong> component — the point where the JCRE verifies the import list — then stops: no last-block flag and no INSTALL[for install], so nothing is committed and nothing has to be deleted afterwards. The <strong>Import versions to test</strong> list is pre-filled from the CAP analysis: <code class="font-mono text-sm">AID=version</code> rewrites an import's requested version, <code class="font-mono text-sm">+AID=version</code> appends a synthetic import (a card-capability query: the LOAD passes only if the card exports that package at ≥ that version), and <strong>Set all 0.0</strong> drops every requirement. Two-step use: press Set all 0.0 — if the test passes, the import list was the blocker; then restore one line at a time to find the package and version the card lacks. The verdict states it covers the <strong>LOAD/import gate only</strong>: the link gate (whether the applet's actual API calls exist) is what a real install checks at INSTALL[for install]. The run and its result appear in a popup (like the install): the step lines, the verdict and a per-AID detail block — on a rejection it names the CAP section the failing block covers (load-file byte range and components, and whether that block completes <strong>Import</strong>), the remote status word with its meaning, the import list exactly as tested (CAP versions, probe rewrites, appended synthetic entries, unmatched lines) and what the probe state implies; on success it lists the import list that passed.</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">CAP compatibility test</td><td class="py-1 px-2">Answers one question without installing anything: <strong>can this card satisfy the CAP's import list?</strong> The server runs the format check, INSTALL[for load] and the LOAD blocks only up to the block that completes the <strong>Import</strong> component — the point where the JCRE verifies the import list — then stops: no last-block flag and no INSTALL[for install], so nothing is committed and nothing has to be deleted afterwards. The <strong>Import versions to test</strong> list is pre-filled from the CAP analysis: <code class="font-mono text-sm">AID=version</code> rewrites an import's requested version, <code class="font-mono text-sm">+AID=version</code> appends a synthetic import (a card-capability query: the LOAD passes only if the card exports that package at ≥ that version), and <strong>Set all 0.0</strong> drops every requirement. Two-step use: press Set all 0.0 — if the test passes, the import list was the blocker; then restore one line at a time to find the package and version the card lacks. The verdict states it covers the <strong>LOAD/import gate only</strong>: the link gate (whether the applet's actual API calls exist) is what a real install checks at INSTALL[for install]. The run and its result appear in a popup (like the install): the step lines, the verdict and a per-AID detail block — on a rejection it names the CAP section the failing block covers (load-file byte range and components, and whether that block completes <strong>Import</strong>), the remote status word with its meaning, the import list exactly as tested (CAP versions, probe rewrites, appended synthetic entries, unmatched lines) and what the probe state implies; on success it lists the import list that passed.</td></tr>
|
||||||
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">INSTALL [for install] (loaded package)</td><td class="py-1 px-2">For a package that is already loaded on the card (its Load File AID and Module AID, privileges, install/STK parameters and an optional <em>make selectable</em>). The applet instance is deleted first (tolerating 'not found' — a fresh card just installs; any other delete failure stops the chain with its reason), so the install parameters can be iterated without re-loading the <code class="font-mono text-sm">.cap</code>.</td></tr>
|
<tr class="border-b border-gray-200 dark:border-slate-700"><td class="py-1 px-2">INSTALL [for install] (loaded package)</td><td class="py-1 px-2">For a package that is already loaded on the card (its Load File AID and Module AID, privileges, install/STK parameters and an optional <em>make selectable</em>). The applet instance is deleted first (tolerating 'not found' — a fresh card just installs; any other delete failure stops the chain with its reason), so the install parameters can be iterated without re-loading the <code class="font-mono text-sm">.cap</code>.</td></tr>
|
||||||
<tr><td class="py-1 px-2">INSTALL [for make selectable]</td><td class="py-1 px-2">One INSTALL [for make selectable] APDU (the instance AID) for an INSTALLED, non-selectable applet instance.</td></tr>
|
<tr><td class="py-1 px-2">INSTALL [for make selectable]</td><td class="py-1 px-2">One INSTALL [for make selectable] APDU (the instance AID) for an INSTALLED, non-selectable applet instance.</td></tr>
|
||||||
|
|||||||
+10
-3
@@ -10321,7 +10321,9 @@ function ramBatchModalRender(st, okLabel, actLabel) {
|
|||||||
document.getElementById('ram-op-bar').style.width = pct + '%';
|
document.getElementById('ram-op-bar').style.width = pct + '%';
|
||||||
// the counts and the last finished card's verdict
|
// the counts and the last finished card's verdict
|
||||||
const c = st.counts || {};
|
const c = st.counts || {};
|
||||||
let info = okLabel + ': ' + (c.installed || 0) + ' \u00b7 ' + t('Failed') + ': ' + (c.failed || 0)
|
let info = okLabel + ': ' + (c.installed || 0);
|
||||||
|
if (c.already_absent) info += ' \u00b7 ' + t('Already absent') + ': ' + c.already_absent;
|
||||||
|
info += ' \u00b7 ' + t('Failed') + ': ' + (c.failed || 0)
|
||||||
+ ' \u00b7 ' + t('Skipped') + ': ' + (c.skipped || 0);
|
+ ' \u00b7 ' + t('Skipped') + ': ' + (c.skipped || 0);
|
||||||
if (st.attempts_total) info += ' \u00b7 ' + st.attempts_total + ' ' + t('attempts');
|
if (st.attempts_total) info += ' \u00b7 ' + st.attempts_total + ' ' + t('attempts');
|
||||||
if (st.notice) info += '\n' + st.notice;
|
if (st.notice) info += '\n' + st.notice;
|
||||||
@@ -10544,8 +10546,9 @@ function ramBatchRender() {
|
|||||||
const counts = document.getElementById('ram-batch-counts');
|
const counts = document.getElementById('ram-batch-counts');
|
||||||
if (counts) {
|
if (counts) {
|
||||||
const c = st.counts || {};
|
const c = st.counts || {};
|
||||||
let text = okLabel + ': ' + (c.installed || 0)
|
let text = okLabel + ': ' + (c.installed || 0);
|
||||||
+ ' \u00b7 ' + t('Failed') + ': ' + (c.failed || 0)
|
if (c.already_absent) text += ' \u00b7 ' + t('Already absent') + ': ' + c.already_absent;
|
||||||
|
text += ' \u00b7 ' + t('Failed') + ': ' + (c.failed || 0)
|
||||||
+ ' \u00b7 ' + t('Skipped') + ': ' + (c.skipped || 0);
|
+ ' \u00b7 ' + t('Skipped') + ': ' + (c.skipped || 0);
|
||||||
if (st.attempts_total) text += ' \u00b7 ' + st.attempts_total + ' ' + t('attempts');
|
if (st.attempts_total) text += ' \u00b7 ' + st.attempts_total + ' ' + t('attempts');
|
||||||
if (active && _ramBatchProgress && _ramBatchProgress.active && _ramBatchProgress.name) {
|
if (active && _ramBatchProgress && _ramBatchProgress.active && _ramBatchProgress.name) {
|
||||||
@@ -10581,7 +10584,10 @@ function ramBatchAttemptsHtml(attempts) {
|
|||||||
for (let i = attempts.length - 1; i >= 0; i--) {
|
for (let i = attempts.length - 1; i >= 0; i--) {
|
||||||
const a = attempts[i];
|
const a = attempts[i];
|
||||||
const when = a.ts ? new Date(a.ts * 1000).toLocaleTimeString() : '';
|
const when = a.ts ? new Date(a.ts * 1000).toLocaleTimeString() : '';
|
||||||
|
// A delete that found the object already gone is a success, but not a
|
||||||
|
// deletion: say so instead of a plain OK (v3.36.0).
|
||||||
const verdict = a.result === 'installed' ? t('OK')
|
const verdict = a.result === 'installed' ? t('OK')
|
||||||
|
: a.result === 'already_absent' ? t('Already absent')
|
||||||
: (a.result === 'failed' ? t('Failed') : t('Skipped'));
|
: (a.result === 'failed' ? t('Failed') : t('Skipped'));
|
||||||
const tone = a.result === 'installed' ? 'text-emerald-600 dark:text-emerald-400'
|
const tone = a.result === 'installed' ? 'text-emerald-600 dark:text-emerald-400'
|
||||||
: (a.result === 'failed' ? 'text-red-600 dark:text-red-400' : 'text-amber-600 dark:text-amber-400');
|
: (a.result === 'failed' ? 'text-red-600 dark:text-red-400' : 'text-amber-600 dark:text-amber-400');
|
||||||
@@ -26904,6 +26910,7 @@ const LANG_RU = {
|
|||||||
'Stopping after the current card...': 'Остановка после текущей карты…',
|
'Stopping after the current card...': 'Остановка после текущей карты…',
|
||||||
'Insert next card. Waiting for a card...': 'Вставьте следующую карту. Ожидание карты…',
|
'Insert next card. Waiting for a card...': 'Вставьте следующую карту. Ожидание карты…',
|
||||||
'Insert next card': 'Вставьте следующую карту',
|
'Insert next card': 'Вставьте следующую карту',
|
||||||
|
'Already absent': 'Уже отсутствует',
|
||||||
'Sound on': 'Звук включён',
|
'Sound on': 'Звук включён',
|
||||||
'Sound off': 'Звук выключен',
|
'Sound off': 'Звук выключен',
|
||||||
'Installed': 'Установлено',
|
'Installed': 'Установлено',
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
const CACHE = 'simple-v470';
|
const CACHE = 'simple-v471';
|
||||||
const URLS = [
|
const URLS = [
|
||||||
'index.html',
|
'index.html',
|
||||||
'help.html',
|
'help.html',
|
||||||
|
|||||||
@@ -735,3 +735,29 @@ test('the waiting cue is wired into the poll render, the Start gesture and the C
|
|||||||
assert.match(css, /\.ram-insert-glow/);
|
assert.match(css, /\.ram-insert-glow/);
|
||||||
assert.match(css, /prefers-reduced-motion: reduce/);
|
assert.match(css, /prefers-reduced-motion: reduce/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('an already-absent delete is reported as such, not as a deletion (v3.36.0)', () => {
|
||||||
|
clearEls();
|
||||||
|
setPanelEls();
|
||||||
|
setModalEls();
|
||||||
|
app.resetBatchModal();
|
||||||
|
app.setLast({ active: false, phase: 'finished', kind: 'delete',
|
||||||
|
attempts: [{
|
||||||
|
iccid: '8970119000000115026', preset: 'Alfa', result: 'already_absent',
|
||||||
|
steps: [{ name: 'DELETE (application)', sw: '6A88', ok: true, not_found: true }],
|
||||||
|
steps_total: 1, duration_ms: 1200,
|
||||||
|
}],
|
||||||
|
counts: { installed: 2, already_absent: 1, failed: 0, skipped: 0 },
|
||||||
|
attempts_total: 3, notice: 'Card 8970119000000115026 was already absent.' });
|
||||||
|
app.ramBatchRender();
|
||||||
|
// the counts separate a real deletion from a no-op
|
||||||
|
assert.match(els['ram-batch-counts'].textContent, /Deleted: 2/);
|
||||||
|
assert.match(els['ram-batch-counts'].textContent, /Already absent: 1/);
|
||||||
|
// the attempt's verdict says it - not a plain OK - and keeps the step line
|
||||||
|
assert.match(els['ram-batch-attempts'].innerHTML, /Already absent/);
|
||||||
|
assert.match(els['ram-batch-attempts'].innerHTML, /6A88 \(not found\)/);
|
||||||
|
// the modal carries the same counts
|
||||||
|
app.ramBatchModalOpen();
|
||||||
|
assert.match(els['ram-op-batch-info'].textContent, /Deleted: 2/);
|
||||||
|
assert.match(els['ram-op-batch-info'].textContent, /Already absent: 1/);
|
||||||
|
});
|
||||||
|
|||||||
@@ -2914,7 +2914,7 @@ def _ram_batch_new_state(plan):
|
|||||||
return {'active': True, 'phase': 'waiting_card', 'stop': False,
|
return {'active': True, 'phase': 'waiting_card', 'stop': False,
|
||||||
'started': time.time(), 'stopped': None, 'plan': plan,
|
'started': time.time(), 'stopped': None, 'plan': plan,
|
||||||
'session': None, 'seen': {}, 'attempts': [], 'attempts_total': 0,
|
'session': None, 'seen': {}, 'attempts': [], 'attempts_total': 0,
|
||||||
'counts': {'installed': 0, 'failed': 0, 'skipped': 0},
|
'counts': {'installed': 0, 'already_absent': 0, 'failed': 0, 'skipped': 0},
|
||||||
'current': None, 'notice': ''}
|
'current': None, 'notice': ''}
|
||||||
|
|
||||||
|
|
||||||
@@ -2939,6 +2939,8 @@ def _ram_batch_record(state, rec):
|
|||||||
verb = {'delete': 'deleted',
|
verb = {'delete': 'deleted',
|
||||||
'reinstall-app': 'reinstalled'}.get(kind, 'installed')
|
'reinstall-app': 'reinstalled'}.get(kind, 'installed')
|
||||||
state['notice'] = 'Card %s %s.' % (who, verb)
|
state['notice'] = 'Card %s %s.' % (who, verb)
|
||||||
|
elif result == 'already_absent':
|
||||||
|
state['notice'] = 'Card %s was already absent.' % who
|
||||||
elif result == 'failed':
|
elif result == 'failed':
|
||||||
step = rec.get('failed_step') or ''
|
step = rec.get('failed_step') or ''
|
||||||
sw = rec.get('por_sw') or rec.get('sw') or ''
|
sw = rec.get('por_sw') or rec.get('sw') or ''
|
||||||
@@ -2947,8 +2949,9 @@ def _ram_batch_record(state, rec):
|
|||||||
else:
|
else:
|
||||||
state['notice'] = 'Card %s skipped: %s.' % (who, rec.get('reason') or '')
|
state['notice'] = 'Card %s skipped: %s.' % (who, rec.get('reason') or '')
|
||||||
detail = rec.get('reason') or rec.get('failed_step') or ''
|
detail = rec.get('reason') or rec.get('failed_step') or ''
|
||||||
|
label = {'already_absent': 'already absent'}.get(result, result)
|
||||||
sys.stderr.write('RAM-BATCH: %s%s%s\n' % (
|
sys.stderr.write('RAM-BATCH: %s%s%s\n' % (
|
||||||
result, (' ' + who) if rec.get('iccid') else '', (' - ' + detail) if detail else ''))
|
label, (' ' + who) if rec.get('iccid') else '', (' - ' + detail) if detail else ''))
|
||||||
|
|
||||||
|
|
||||||
def _ram_batch_failure_fields(result):
|
def _ram_batch_failure_fields(result):
|
||||||
@@ -3044,6 +3047,15 @@ def _ram_batch_handle_card(state, server):
|
|||||||
'module_aid': result.get('module_aid'),
|
'module_aid': result.get('module_aid'),
|
||||||
'nv_delta': result.get('nv_delta')}
|
'nv_delta': result.get('nv_delta')}
|
||||||
rec.update(_ram_batch_steps_fields(result))
|
rec.update(_ram_batch_steps_fields(result))
|
||||||
|
# A delete run whose DELETE answered 'not found' is a success (the
|
||||||
|
# object is gone, the goal is met) but not a deletion: report it as
|
||||||
|
# "already absent" and count it separately (v3.36.0). Only the delete
|
||||||
|
# kind: a reinstall's tolerated pre-delete is not the outcome.
|
||||||
|
if (state.get('plan') or {}).get('kind') == 'delete' and any(
|
||||||
|
s.get('delete_not_found') for s in (result.get('steps') or [])):
|
||||||
|
outcome = 'already_absent'
|
||||||
|
rec['result'] = outcome
|
||||||
|
rec['not_found'] = True
|
||||||
elif result is not None:
|
elif result is not None:
|
||||||
outcome = 'failed'
|
outcome = 'failed'
|
||||||
rec = {'iccid': iccid, 'preset': name, 'result': outcome,
|
rec = {'iccid': iccid, 'preset': name, 'result': outcome,
|
||||||
@@ -3178,7 +3190,7 @@ def _ram_batch_status():
|
|||||||
if _RAM_BATCH is None:
|
if _RAM_BATCH is None:
|
||||||
return {'active': False, 'phase': 'idle', 'attempts': [],
|
return {'active': False, 'phase': 'idle', 'attempts': [],
|
||||||
'attempts_total': 0, 'current': None, 'notice': '',
|
'attempts_total': 0, 'current': None, 'notice': '',
|
||||||
'counts': {'installed': 0, 'failed': 0, 'skipped': 0},
|
'counts': {'installed': 0, 'already_absent': 0, 'failed': 0, 'skipped': 0},
|
||||||
'started': None, 'stopped': None, 'progress': None,
|
'started': None, 'stopped': None, 'progress': None,
|
||||||
'kind': None}
|
'kind': None}
|
||||||
return {'active': bool(_RAM_BATCH.get('active')),
|
return {'active': bool(_RAM_BATCH.get('active')),
|
||||||
|
|||||||
+45
-1
@@ -88,7 +88,8 @@ class BatchStateTests(unittest.TestCase):
|
|||||||
'failed_step': 'LOAD (3/7)', 'por_sw': '6A80'})
|
'failed_step': 'LOAD (3/7)', 'por_sw': '6A80'})
|
||||||
srv._ram_batch_record(st, {'iccid': '8970119000004600114', 'result': 'skipped',
|
srv._ram_batch_record(st, {'iccid': '8970119000004600114', 'result': 'skipped',
|
||||||
'reason': 'no preset matches this ICCID'})
|
'reason': 'no preset matches this ICCID'})
|
||||||
self.assertEqual(st['counts'], {'installed': 1, 'failed': 1, 'skipped': 1})
|
self.assertEqual(st['counts'], {'installed': 1, 'already_absent': 0,
|
||||||
|
'failed': 1, 'skipped': 1})
|
||||||
self.assertEqual(st['attempts_total'], 3)
|
self.assertEqual(st['attempts_total'], 3)
|
||||||
# the notice names the last attempt's outcome
|
# the notice names the last attempt's outcome
|
||||||
self.assertIn('skipped: no preset matches this ICCID', st['notice'])
|
self.assertIn('skipped: no preset matches this ICCID', st['notice'])
|
||||||
@@ -230,6 +231,49 @@ class BatchCardTests(unittest.TestCase):
|
|||||||
self.assertEqual(seen['current']['iccid'], '8970119000004600098')
|
self.assertEqual(seen['current']['iccid'], '8970119000004600098')
|
||||||
self.assertIn('installed', st['notice']) # ...and set for the new outcome
|
self.assertIn('installed', st['notice']) # ...and set for the new outcome
|
||||||
|
|
||||||
|
def test_a_delete_that_finds_nothing_is_reported_already_absent(self):
|
||||||
|
"""A delete run whose DELETE answered 6A88 is a success (the object is
|
||||||
|
gone, the goal is met) but not a deletion: it is counted in its own
|
||||||
|
bucket and the notice says so (v3.36.0)."""
|
||||||
|
st = srv._ram_batch_new_state(_plan('delete'))
|
||||||
|
preset = _preset('8970119000004600098', 'Alfa')
|
||||||
|
|
||||||
|
def fake_run(server, body, plan, scc):
|
||||||
|
return {'success': True, 'final_cntr': '0000000002',
|
||||||
|
'steps': [{'name': 'DELETE (application)', 'sw': '9000',
|
||||||
|
'por_status': 'por_ok', 'por_sw': '6A88',
|
||||||
|
'delete_not_found': True}]}
|
||||||
|
|
||||||
|
with mock.patch.object(srv, '_ram_plan_run', fake_run):
|
||||||
|
srv._ram_batch_handle_card(st, _fake_server('8970119000004600098',
|
||||||
|
_FakeStore([preset])))
|
||||||
|
rec = st['attempts'][0]
|
||||||
|
self.assertEqual(rec['result'], 'already_absent')
|
||||||
|
self.assertTrue(rec['not_found'])
|
||||||
|
self.assertEqual(st['counts']['already_absent'], 1)
|
||||||
|
self.assertEqual(st['counts']['installed'], 0)
|
||||||
|
self.assertIn('already absent', st['notice'])
|
||||||
|
self.assertEqual(st['seen']['8970119000004600098'], 'already_absent')
|
||||||
|
|
||||||
|
def test_a_reinstall_with_a_tolerated_pre_delete_is_still_an_install(self):
|
||||||
|
"""The reinstall's pre-delete may tolerate 'not found', but the
|
||||||
|
outcome is the INSTALL, not 'already absent'."""
|
||||||
|
st = srv._ram_batch_new_state(_plan('reinstall-app'))
|
||||||
|
preset = _preset('8970119000004600098', 'Alfa')
|
||||||
|
|
||||||
|
def fake_run(server, body, plan, scc):
|
||||||
|
return {'success': True,
|
||||||
|
'steps': [{'name': 'DELETE (application)', 'por_sw': '6A88',
|
||||||
|
'delete_not_found': True},
|
||||||
|
{'name': 'INSTALL [for install]', 'por_sw': '9000'}]}
|
||||||
|
|
||||||
|
with mock.patch.object(srv, '_ram_plan_run', fake_run):
|
||||||
|
srv._ram_batch_handle_card(st, _fake_server('8970119000004600098',
|
||||||
|
_FakeStore([preset])))
|
||||||
|
self.assertEqual(st['attempts'][0]['result'], 'installed')
|
||||||
|
self.assertEqual(st['counts']['installed'], 1)
|
||||||
|
self.assertEqual(st['counts']['already_absent'], 0)
|
||||||
|
|
||||||
def test_a_card_already_processed_is_reported_not_reinstalled(self):
|
def test_a_card_already_processed_is_reported_not_reinstalled(self):
|
||||||
st = srv._ram_batch_new_state(_plan())
|
st = srv._ram_batch_new_state(_plan())
|
||||||
preset = _preset('8970119000004600098', 'Alfa')
|
preset = _preset('8970119000004600098', 'Alfa')
|
||||||
|
|||||||
Reference in New Issue
Block a user