@hominis/fireforge 0.53.0 → 0.54.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -1
- package/dist/build-info.json +4 -4
- package/dist/src/commands/patch/staged-dependency-remove.d.ts +26 -4
- package/dist/src/commands/patch/staged-dependency-remove.js +60 -23
- package/dist/src/commands/patch/staged-dependency.js +25 -18
- package/dist/src/commands/re-export-options.js +8 -2
- package/dist/src/commands/run.js +11 -4
- package/dist/src/commands/test-orchestrate.js +4 -1
- package/dist/src/commands/test-prelock.d.ts +11 -0
- package/dist/src/commands/test-prelock.js +27 -2
- package/dist/src/commands/test-register.js +2 -1
- package/dist/src/commands/test.js +4 -2
- package/dist/src/core/negative-control-diff.js +31 -6
- package/dist/src/core/test-stale-check.js +1 -1
- package/dist/src/types/commands/options.d.ts +12 -0
- package/docs/configuration.md +7 -0
- package/docs/patch-workflow.md +13 -7
- package/docs/testing.md +9 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 0.
|
|
3
|
+
## 0.54.0
|
|
4
|
+
|
|
5
|
+
### Test harness
|
|
6
|
+
|
|
7
|
+
- The browser and port checks that follow work a `fireforge test` already did under the lock wait out `--wait-browser` and `--wait-port` again, instead of probing once: the checks after a `--build` run's pre-test build, and those of every later run of a `--repeat` or `--negative-control` sequence. The pre-lock pass spent the budgets and the in-lock checks only probed, so a peer's browser that started during the build ended the run `reason=preflight note=browser-busy` (`control=inconclusive` for a control) and threw the build away.
|
|
8
|
+
- A negative-control diff that names one file in more than one section (a generator that writes one section per edit) is applied as one section with its hunks in file order, instead of being refused. Sections whose hunks overlap are still refused, naming both hunks and the one-section remedy.
|
|
9
|
+
- Windows: the `stale-compiled` refusal names the linked binary with forward slashes (`engine/obj-x/dist/bin/libxul.so`), not with mixed separators.
|
|
10
|
+
|
|
11
|
+
### Patch queue
|
|
12
|
+
|
|
13
|
+
- `patch staged-dependency --remove` without `--kind` takes the kind from its matcher: `--specifier` selects imports and `--line` registrations. A selection with neither, such as `--creates` alone, searches both kinds, as the flag's help says. It used to search imports only, so removing a stale registration by `--creates` was refused until `--kind registration` was added. A miss lists the patch's declarations of every kind it searched.
|
|
14
|
+
- `re-export --expect-removed` combines with `--scan`, so one call can retire a widget's files from one patch while `--scan-file` adopts a new module into another. The retired path leaves its owner's file list before the scan, which cannot adopt it back. `--files`, `--all` and an implicit selection are still refused.
|
|
15
|
+
|
|
16
|
+
### Run
|
|
17
|
+
|
|
18
|
+
- `fireforge run` passes the arguments after `--` to the browser, after the profile ones: `fireforge run --profile <dir> -- -marionette -remote-allow-system-access` drives a headed session on a kept profile, with `-purgecaches`, from chrome-context Marionette.
|
|
19
|
+
|
|
20
|
+
### Other
|
|
21
|
+
|
|
22
|
+
- Tests: the `git diff --no-index` negative-control test no longer fails when the temp root's random suffix ends in `b`, which its fixture rewrite matched inside the old side's absolute path. The `application.ini` path test compares native paths on Windows.
|
|
4
23
|
|
|
5
24
|
### Test harness
|
|
6
25
|
|
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"version": "0.
|
|
4
|
-
"commit": "
|
|
5
|
-
"shortCommit": "
|
|
3
|
+
"version": "0.54.0",
|
|
4
|
+
"commit": "2bf7356bab0d77849cb1ab136f0160995dabf41e",
|
|
5
|
+
"shortCommit": "2bf7356bab0d",
|
|
6
6
|
"dirty": false,
|
|
7
7
|
"dirtyHash": null,
|
|
8
|
-
"buildTime": "2026-10-
|
|
8
|
+
"buildTime": "2026-10-24T18:17:09.937Z"
|
|
9
9
|
}
|
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
*
|
|
5
5
|
* A removal names its entries by any of `--file`, `--specifier`/`--line`,
|
|
6
6
|
* `--creates` and `--owner`, and must give at least `--creates` or the
|
|
7
|
-
* `--file` + matcher pair.
|
|
7
|
+
* `--file` + matcher pair. Without `--kind` the matcher picks the kind
|
|
8
|
+
* (`--specifier` an import, `--line` a registration), and a selection with
|
|
9
|
+
* neither searches both kinds, so `--creates` alone removes every
|
|
10
|
+
* declaration for that path. Registration lines compare whitespace-
|
|
8
11
|
* insensitively (runs of whitespace collapse, ends trimmed), the way the
|
|
9
12
|
* lint compares them, so a line copied from a patch hunk matches the stored
|
|
10
13
|
* one whatever its padding. A selection that matches nothing is refused
|
|
@@ -12,6 +15,24 @@
|
|
|
12
15
|
*/
|
|
13
16
|
import type { PatchStagedDependencies, PatchStagedDependencyOptions } from '../../types/commands/index.js';
|
|
14
17
|
import { type StagedDependencyKind, type StagedEntryView } from './staged-dependency-entries.js';
|
|
18
|
+
/** One stored declaration together with its kind. */
|
|
19
|
+
export interface KindedEntryView {
|
|
20
|
+
kind: StagedDependencyKind;
|
|
21
|
+
view: StagedEntryView;
|
|
22
|
+
}
|
|
23
|
+
/** Singular noun for a declaration of the given kinds. */
|
|
24
|
+
export declare function declarationNoun(kinds: readonly StagedDependencyKind[]): string;
|
|
25
|
+
/**
|
|
26
|
+
* The kinds a `--remove` searches. `--kind` wins when given (and is
|
|
27
|
+
* validated by the caller); otherwise `--specifier` selects imports,
|
|
28
|
+
* `--line` registrations, and a selection with neither matcher searches
|
|
29
|
+
* both kinds.
|
|
30
|
+
*
|
|
31
|
+
* @param options - Command options
|
|
32
|
+
* @returns The kinds to search, or undefined when `--kind` was given
|
|
33
|
+
* @throws InvalidArgumentError when `--specifier` and `--line` are both given without `--kind`
|
|
34
|
+
*/
|
|
35
|
+
export declare function inferRemovalKinds(options: PatchStagedDependencyOptions): readonly StagedDependencyKind[] | undefined;
|
|
15
36
|
/**
|
|
16
37
|
* Resolves the staged entries a `--remove` invocation names. The returned
|
|
17
38
|
* views are the stored entries themselves, so the removal matches them
|
|
@@ -20,12 +41,13 @@ import { type StagedDependencyKind, type StagedEntryView } from './staged-depend
|
|
|
20
41
|
* - `--file` + matcher identifying one entry infers `--creates`.
|
|
21
42
|
* - `--file` + matcher identifying several entries without `--creates`
|
|
22
43
|
* refuses with the candidate list.
|
|
23
|
-
* - `--creates` alone selects every declaration for that created path
|
|
44
|
+
* - `--creates` alone selects every declaration for that created path, of
|
|
45
|
+
* every kind searched.
|
|
24
46
|
*
|
|
25
|
-
* @param
|
|
47
|
+
* @param kinds - Declaration kinds to search
|
|
26
48
|
* @param options - Command options
|
|
27
49
|
* @param staged - The target patch's current staged dependencies
|
|
28
50
|
* @returns The matching stored entries (never empty)
|
|
29
51
|
* @throws InvalidArgumentError when the selectors are insufficient or match nothing
|
|
30
52
|
*/
|
|
31
|
-
export declare function selectRemovalEntries(
|
|
53
|
+
export declare function selectRemovalEntries(kinds: readonly StagedDependencyKind[], options: PatchStagedDependencyOptions, staged: PatchStagedDependencies | undefined): KindedEntryView[];
|
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
*
|
|
6
6
|
* A removal names its entries by any of `--file`, `--specifier`/`--line`,
|
|
7
7
|
* `--creates` and `--owner`, and must give at least `--creates` or the
|
|
8
|
-
* `--file` + matcher pair.
|
|
8
|
+
* `--file` + matcher pair. Without `--kind` the matcher picks the kind
|
|
9
|
+
* (`--specifier` an import, `--line` a registration), and a selection with
|
|
10
|
+
* neither searches both kinds, so `--creates` alone removes every
|
|
11
|
+
* declaration for that path. Registration lines compare whitespace-
|
|
9
12
|
* insensitively (runs of whitespace collapse, ends trimmed), the way the
|
|
10
13
|
* lint compares them, so a line copied from a patch hunk matches the stored
|
|
11
14
|
* one whatever its padding. A selection that matches nothing is refused
|
|
@@ -18,10 +21,38 @@ import { dependencyLabel, importView, registrationView, } from './staged-depende
|
|
|
18
21
|
function normalizeRegistrationLine(line) {
|
|
19
22
|
return line.trim().replace(/\s+/g, ' ');
|
|
20
23
|
}
|
|
21
|
-
function stagedViews(
|
|
22
|
-
return kind === 'import'
|
|
23
|
-
? (staged?.forwardImports ?? []).map(importView)
|
|
24
|
-
: (staged?.registrations ?? []).map(registrationView);
|
|
24
|
+
function stagedViews(kinds, staged) {
|
|
25
|
+
return kinds.flatMap((kind) => kind === 'import'
|
|
26
|
+
? (staged?.forwardImports ?? []).map((entry) => ({ kind, view: importView(entry) }))
|
|
27
|
+
: (staged?.registrations ?? []).map((entry) => ({ kind, view: registrationView(entry) })));
|
|
28
|
+
}
|
|
29
|
+
/** Singular noun for a declaration of the given kinds. */
|
|
30
|
+
export function declarationNoun(kinds) {
|
|
31
|
+
if (kinds.length !== 1)
|
|
32
|
+
return 'staged declaration';
|
|
33
|
+
return kinds[0] === 'import' ? 'staged forward-import' : 'staged registration';
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The kinds a `--remove` searches. `--kind` wins when given (and is
|
|
37
|
+
* validated by the caller); otherwise `--specifier` selects imports,
|
|
38
|
+
* `--line` registrations, and a selection with neither matcher searches
|
|
39
|
+
* both kinds.
|
|
40
|
+
*
|
|
41
|
+
* @param options - Command options
|
|
42
|
+
* @returns The kinds to search, or undefined when `--kind` was given
|
|
43
|
+
* @throws InvalidArgumentError when `--specifier` and `--line` are both given without `--kind`
|
|
44
|
+
*/
|
|
45
|
+
export function inferRemovalKinds(options) {
|
|
46
|
+
if (options.kind !== undefined)
|
|
47
|
+
return undefined;
|
|
48
|
+
if (options.specifier !== undefined && options.line !== undefined) {
|
|
49
|
+
throw new InvalidArgumentError('--specifier names an import and --line a registration; pass one of them, or --kind.', '--specifier/--line');
|
|
50
|
+
}
|
|
51
|
+
if (options.specifier !== undefined)
|
|
52
|
+
return ['import'];
|
|
53
|
+
if (options.line !== undefined)
|
|
54
|
+
return ['registration'];
|
|
55
|
+
return ['import', 'registration'];
|
|
25
56
|
}
|
|
26
57
|
/**
|
|
27
58
|
* Resolves the staged entries a `--remove` invocation names. The returned
|
|
@@ -31,47 +62,53 @@ function stagedViews(kind, staged) {
|
|
|
31
62
|
* - `--file` + matcher identifying one entry infers `--creates`.
|
|
32
63
|
* - `--file` + matcher identifying several entries without `--creates`
|
|
33
64
|
* refuses with the candidate list.
|
|
34
|
-
* - `--creates` alone selects every declaration for that created path
|
|
65
|
+
* - `--creates` alone selects every declaration for that created path, of
|
|
66
|
+
* every kind searched.
|
|
35
67
|
*
|
|
36
|
-
* @param
|
|
68
|
+
* @param kinds - Declaration kinds to search
|
|
37
69
|
* @param options - Command options
|
|
38
70
|
* @param staged - The target patch's current staged dependencies
|
|
39
71
|
* @returns The matching stored entries (never empty)
|
|
40
72
|
* @throws InvalidArgumentError when the selectors are insufficient or match nothing
|
|
41
73
|
*/
|
|
42
|
-
export function selectRemovalEntries(
|
|
43
|
-
const
|
|
44
|
-
const
|
|
74
|
+
export function selectRemovalEntries(kinds, options, staged) {
|
|
75
|
+
const single = kinds.length === 1 ? kinds[0] : undefined;
|
|
76
|
+
const matcherFlag = single === 'import'
|
|
77
|
+
? '--specifier'
|
|
78
|
+
: single === 'registration'
|
|
79
|
+
? '--line'
|
|
80
|
+
: '--specifier or --line';
|
|
81
|
+
const matcher = single === 'import' ? options.specifier : single === 'registration' ? options.line : undefined;
|
|
45
82
|
const hasPair = Boolean(options.file) && Boolean(matcher);
|
|
46
83
|
if (!options.creates && !hasPair) {
|
|
47
|
-
throw new InvalidArgumentError(`--remove${kind === 'registration' ? ' --kind registration' : ''} requires --creates, ` +
|
|
84
|
+
throw new InvalidArgumentError(`--remove${options.kind === 'registration' ? ' --kind registration' : ''} requires --creates, ` +
|
|
48
85
|
`or --file with ${matcherFlag}; --creates alone removes every declaration for that path.`, matcher && !options.file
|
|
49
86
|
? '--file'
|
|
50
87
|
: options.file && !matcher
|
|
51
88
|
? matcherFlag
|
|
52
89
|
: `--creates or --file + ${matcherFlag}`);
|
|
53
90
|
}
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
91
|
+
const views = stagedViews(kinds, staged);
|
|
92
|
+
const candidates = views.filter(({ kind, view }) => (!options.file || view.file === options.file) &&
|
|
93
|
+
(matcher === undefined ||
|
|
94
|
+
comparableMatcher(kind, view.matcher) === comparableMatcher(kind, matcher)) &&
|
|
58
95
|
(!options.creates || view.creates === options.creates) &&
|
|
59
96
|
(options.owner === undefined || view.owner === options.owner));
|
|
97
|
+
const label = ({ kind, view }) => ` - ${dependencyLabel(kind, view)}`;
|
|
60
98
|
if (candidates.length === 0) {
|
|
61
|
-
const noun =
|
|
99
|
+
const noun = declarationNoun(kinds);
|
|
62
100
|
const existing = views.length === 0
|
|
63
101
|
? `This patch has no ${noun}s.`
|
|
64
|
-
: `This patch's ${noun}s:\n${views.map(
|
|
102
|
+
: `This patch's ${noun}s:\n${views.map(label).join('\n')}`;
|
|
65
103
|
throw new InvalidArgumentError(`--remove matched no ${noun}. ${existing}`, '--remove');
|
|
66
104
|
}
|
|
67
105
|
if (candidates.length > 1 && !options.creates) {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
throw new GeneralError(`--remove matches ${candidates.length} ${noun} on this patch; pass --creates to pick one:\n${list}`);
|
|
106
|
+
throw new GeneralError(`--remove matches ${candidates.length} ${declarationNoun(kinds)}s on this patch; ` +
|
|
107
|
+
`pass --creates to pick one:\n${candidates.map(label).join('\n')}`);
|
|
71
108
|
}
|
|
72
|
-
const
|
|
73
|
-
if (
|
|
74
|
-
info(`Inferred --creates ${
|
|
109
|
+
const only = candidates.length === 1 ? candidates[0] : undefined;
|
|
110
|
+
if (only !== undefined && !options.creates) {
|
|
111
|
+
info(`Inferred --creates ${only.view.creates} from the single matching staged entry.`);
|
|
75
112
|
}
|
|
76
113
|
return candidates;
|
|
77
114
|
}
|
|
@@ -13,7 +13,7 @@ import { requirePatchQueue, requirePatchTarget } from './patch-context.js';
|
|
|
13
13
|
import { stagedDependencyDelta } from './staged-dependency-batch.js';
|
|
14
14
|
import { applyMode, countReasonUpdates, countStagedEntries, dependencyLabel, importView, registrationView, } from './staged-dependency-entries.js';
|
|
15
15
|
import { runBatchAdd } from './staged-dependency-from.js';
|
|
16
|
-
import { selectRemovalEntries } from './staged-dependency-remove.js';
|
|
16
|
+
import { declarationNoun, inferRemovalKinds, selectRemovalEntries, } from './staged-dependency-remove.js';
|
|
17
17
|
import { validateStagedDependencyAdd } from './staged-dependency-validate.js';
|
|
18
18
|
/** Joins the names of the missing flags for error attribution. */
|
|
19
19
|
function missingFlagList(pairs) {
|
|
@@ -89,16 +89,17 @@ function requireRegistrationOptions(options) {
|
|
|
89
89
|
/**
|
|
90
90
|
* Renders a one-line summary of a staged-dependency metadata change.
|
|
91
91
|
*/
|
|
92
|
-
function describeStagedDependencyChange(before, after, mode,
|
|
92
|
+
function describeStagedDependencyChange(before, after, mode, kinds, targets) {
|
|
93
93
|
const beforeCount = countStagedEntries(before);
|
|
94
94
|
const afterCount = countStagedEntries(after);
|
|
95
|
-
const
|
|
95
|
+
const targetKinds = [...new Set(targets.map((target) => target.kind))];
|
|
96
|
+
const noun = declarationNoun(targetKinds.length > 0 ? targetKinds : kinds);
|
|
96
97
|
if (mode === 'clear') {
|
|
97
98
|
return beforeCount === 0
|
|
98
99
|
? 'stagedDependencies was already empty — no change'
|
|
99
100
|
: `cleared ${beforeCount} staged dependency declaration(s)`;
|
|
100
101
|
}
|
|
101
|
-
const label = targets.map((
|
|
102
|
+
const label = targets.map(({ kind, view }) => dependencyLabel(kind, view)).join('; ');
|
|
102
103
|
if (targets.length === 0)
|
|
103
104
|
return 'stagedDependencies unchanged';
|
|
104
105
|
if (mode === 'add') {
|
|
@@ -127,7 +128,11 @@ export async function patchStagedDependencyCommand(projectRoot, identifier, opti
|
|
|
127
128
|
await runBatchAdd(projectRoot, identifier, mode, options);
|
|
128
129
|
return;
|
|
129
130
|
}
|
|
130
|
-
|
|
131
|
+
// Without --kind, --remove searches the kind its matcher names, or both.
|
|
132
|
+
const kinds = (mode === 'remove' ? inferRemovalKinds(options) : undefined) ?? [
|
|
133
|
+
kindFromOptions(options),
|
|
134
|
+
];
|
|
135
|
+
const kind = kinds.length === 1 ? kinds[0] : undefined;
|
|
131
136
|
// The patch queue loads before the declaration fields resolve so
|
|
132
137
|
// `--remove` can infer `--creates` from the target patch's staged
|
|
133
138
|
// entries.
|
|
@@ -143,24 +148,25 @@ export async function patchStagedDependencyCommand(projectRoot, identifier, opti
|
|
|
143
148
|
validateStagedDependencyAdd(added, manifest.patches);
|
|
144
149
|
}
|
|
145
150
|
const targets = mode === 'remove'
|
|
146
|
-
? selectRemovalEntries(
|
|
151
|
+
? selectRemovalEntries(kinds, options, targetPatch.stagedDependencies)
|
|
147
152
|
: importDependency !== undefined
|
|
148
|
-
? [importView(importDependency)]
|
|
153
|
+
? [{ kind: 'import', view: importView(importDependency) }]
|
|
149
154
|
: registrationDependency !== undefined
|
|
150
|
-
? [registrationView(registrationDependency)]
|
|
155
|
+
? [{ kind: 'registration', view: registrationView(registrationDependency) }]
|
|
151
156
|
: [];
|
|
152
|
-
const applyKind = (entries,
|
|
153
|
-
if (!
|
|
157
|
+
const applyKind = (entries, entryKind, toView, addition) => {
|
|
158
|
+
if (!kinds.includes(entryKind))
|
|
154
159
|
return [...entries];
|
|
160
|
+
const own = targets.filter((target) => target.kind === entryKind).map(({ view }) => view);
|
|
155
161
|
if (mode !== 'remove')
|
|
156
|
-
return applyMode(entries, mode,
|
|
157
|
-
return
|
|
162
|
+
return applyMode(entries, mode, own[0], toView, addition);
|
|
163
|
+
return own.reduce((remaining, target) => applyMode(remaining, 'remove', target, toView, undefined), [...entries]);
|
|
158
164
|
};
|
|
159
165
|
const applyToStaged = (staged) => {
|
|
160
166
|
if (mode === 'clear')
|
|
161
167
|
return {};
|
|
162
|
-
const forwardImports = applyKind(staged?.forwardImports ?? [],
|
|
163
|
-
const registrations = applyKind(staged?.registrations ?? [],
|
|
168
|
+
const forwardImports = applyKind(staged?.forwardImports ?? [], 'import', importView, importDependency);
|
|
169
|
+
const registrations = applyKind(staged?.registrations ?? [], 'registration', registrationView, registrationDependency);
|
|
164
170
|
return {
|
|
165
171
|
...(forwardImports.length > 0 ? { forwardImports } : {}),
|
|
166
172
|
...(registrations.length > 0 ? { registrations } : {}),
|
|
@@ -168,7 +174,7 @@ export async function patchStagedDependencyCommand(projectRoot, identifier, opti
|
|
|
168
174
|
};
|
|
169
175
|
const existing = targetPatch.stagedDependencies;
|
|
170
176
|
const projected = applyToStaged(existing);
|
|
171
|
-
const summary = describeStagedDependencyChange(existing, projected, mode,
|
|
177
|
+
const summary = describeStagedDependencyChange(existing, projected, mode, kinds, targets);
|
|
172
178
|
if (isDryRun) {
|
|
173
179
|
info(`[dry-run] ${targetPatch.filename}: ${summary}.`);
|
|
174
180
|
outro('Dry run complete — no changes made');
|
|
@@ -188,7 +194,7 @@ export async function patchStagedDependencyCommand(projectRoot, identifier, opti
|
|
|
188
194
|
}
|
|
189
195
|
const before = result.before.stagedDependencies;
|
|
190
196
|
const after = result.after.stagedDependencies;
|
|
191
|
-
info(`${targetPatch.filename}: ${describeStagedDependencyChange(before, after, mode,
|
|
197
|
+
info(`${targetPatch.filename}: ${describeStagedDependencyChange(before, after, mode, kinds, targets)}.`);
|
|
192
198
|
if (mode === 'remove' && countStagedEntries(after) === countStagedEntries(before)) {
|
|
193
199
|
// The selection matched when the queue was read; a peer removed the
|
|
194
200
|
// entries before the locked write. Still a miss, so still a refusal.
|
|
@@ -201,7 +207,8 @@ export async function patchStagedDependencyCommand(projectRoot, identifier, opti
|
|
|
201
207
|
args: {
|
|
202
208
|
filename: targetPatch.filename,
|
|
203
209
|
mode,
|
|
204
|
-
kind
|
|
210
|
+
// A kind-less --remove searched both kinds; the delta names which.
|
|
211
|
+
...(kind !== undefined ? { kind } : {}),
|
|
205
212
|
...stagedDependencyDelta(before, after),
|
|
206
213
|
},
|
|
207
214
|
...(options.yes === true ? { yes: true } : {}),
|
|
@@ -223,7 +230,7 @@ export function registerPatchStagedDependency(parent, context) {
|
|
|
223
230
|
.option('--add', 'Add a staged dependency declaration')
|
|
224
231
|
.option('--remove', 'Remove matching staged dependency declaration(s); a selection that matches nothing exits 8')
|
|
225
232
|
.option('--clear', 'Drop the stagedDependencies field entirely')
|
|
226
|
-
.option('--kind <kind>', 'Declaration shape: "import" (forward import, the default) or "registration" (jar.mn packaging line, customElements or actor registration)')
|
|
233
|
+
.option('--kind <kind>', 'Declaration shape: "import" (forward import, the default) or "registration" (jar.mn packaging line, customElements or actor registration); --remove without it takes the kind --specifier or --line names, or both kinds')
|
|
227
234
|
.option('--file <path>', 'Declaring file path relative to engine/')
|
|
228
235
|
.option('--specifier <specifier>', 'Exact import specifier as it appears in source (--kind import)')
|
|
229
236
|
.option('--line <text>', 'Registration/packaging line as the patch adds it, compared whitespace-insensitively (--kind registration)')
|
|
@@ -97,10 +97,16 @@ function validatePathFlagValues(options) {
|
|
|
97
97
|
}
|
|
98
98
|
}
|
|
99
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* A retirement names its owner among explicitly selected patches. It
|
|
102
|
+
* combines with `--scan`: the retired path leaves the scan's base first,
|
|
103
|
+
* and the scan cannot adopt it back, since a retired created file is absent
|
|
104
|
+
* and a retired upstream file is clean at HEAD.
|
|
105
|
+
*/
|
|
100
106
|
function validateExpectedRemovalUsage(patches, options) {
|
|
101
107
|
if (options.expectRemoved?.length &&
|
|
102
|
-
(options.
|
|
103
|
-
throw new InvalidArgumentError('--expect-removed requires explicit selected patches and
|
|
108
|
+
(options.all || options.files !== undefined || patches.length === 0)) {
|
|
109
|
+
throw new InvalidArgumentError('--expect-removed requires explicit selected patches and cannot combine with --files or --all.', '--expect-removed');
|
|
104
110
|
}
|
|
105
111
|
}
|
|
106
112
|
/**
|
package/dist/src/commands/run.js
CHANGED
|
@@ -130,6 +130,7 @@ async function runInteractive(engineDir, options, profile) {
|
|
|
130
130
|
const exitCode = await run(engineDir, [
|
|
131
131
|
...(options.headless ? ['--headless'] : []),
|
|
132
132
|
...profile.args,
|
|
133
|
+
...(options.args ?? []),
|
|
133
134
|
]);
|
|
134
135
|
// Exit-code whitelist:
|
|
135
136
|
// 0: clean shutdown
|
|
@@ -234,7 +235,12 @@ async function runSmokeExit(engineDir, options, profile) {
|
|
|
234
235
|
const startedAt = Date.now();
|
|
235
236
|
let result;
|
|
236
237
|
try {
|
|
237
|
-
const machArgs = [
|
|
238
|
+
const machArgs = [
|
|
239
|
+
'run',
|
|
240
|
+
...(options.headless ? ['--headless'] : []),
|
|
241
|
+
...profile.args,
|
|
242
|
+
...(options.args ?? []),
|
|
243
|
+
];
|
|
238
244
|
result = await runMachSmoke(machArgs, engineDir, {
|
|
239
245
|
smokeTimeoutMs,
|
|
240
246
|
onStdoutLine: (line) => {
|
|
@@ -394,6 +400,7 @@ export function registerRun(program, { getProjectRoot, withErrorHandling }) {
|
|
|
394
400
|
program
|
|
395
401
|
.command('run')
|
|
396
402
|
.description('Launch the built browser')
|
|
403
|
+
.argument('[args...]', 'Arguments passed to the browser after the profile ones, given after "--" (e.g. -- -marionette -remote-allow-system-access)')
|
|
397
404
|
.option('--smoke-exit <seconds>', 'Smoke-run mode (POSIX only): launch, capture console, SIGTERM the process group after <seconds>. Exit 0 on a clean window, 12 on unallowed errors, 13 on launch failure.', commanderArgParser((value) => {
|
|
398
405
|
const parsed = Number.parseInt(value, 10);
|
|
399
406
|
if (!Number.isFinite(parsed) || parsed < 1 || String(parsed) !== value.trim()) {
|
|
@@ -410,8 +417,8 @@ export function registerRun(program, { getProjectRoot, withErrorHandling }) {
|
|
|
410
417
|
.option('--temp-profile', 'Launch with a fresh temporary profile that is removed when the run ends. The default for --smoke-exit.')
|
|
411
418
|
.action(withErrorHandling(
|
|
412
419
|
// `args` is the variadic positional, not a Commander flag, so it
|
|
413
|
-
//
|
|
414
|
-
async (options) => {
|
|
415
|
-
await runCommand(getProjectRoot(), pickDefined(options));
|
|
420
|
+
// arrives apart from the options object.
|
|
421
|
+
async (args, options) => {
|
|
422
|
+
await runCommand(getProjectRoot(), pickDefined({ ...options, ...(args.length > 0 ? { args } : {}) }));
|
|
416
423
|
}));
|
|
417
424
|
}
|
|
@@ -27,6 +27,7 @@ import { stripEnginePrefix } from '../utils/paths.js';
|
|
|
27
27
|
import { controlBuildRecorded, runControlRestoreBuild } from './test-control-restore.js';
|
|
28
28
|
import { classifyTestHarnesses } from './test-modes.js';
|
|
29
29
|
import { deriveControlOutcome, describeControlOutcome, formatAggregateBody, formatRunLine, parseCapturedVerdict, worstRun, } from './test-multi-verdict.js';
|
|
30
|
+
import { rearmSpentWaits } from './test-prelock.js';
|
|
30
31
|
import { emitAggregateVerdict, emitFailVerdict, setVerdictCapture, setVerdictRunAttribute, verdictEmitted, } from './test-verdict.js';
|
|
31
32
|
function assertMultiRunUsage(testPaths, options) {
|
|
32
33
|
const flag = options.negativeControl !== undefined ? '--negative-control' : '--repeat';
|
|
@@ -58,7 +59,9 @@ async function splitHarnesses(engineDir, testPaths, options) {
|
|
|
58
59
|
}
|
|
59
60
|
/** Options for every run after the first: the build and doctor run once. */
|
|
60
61
|
function laterRunOptions(options) {
|
|
61
|
-
|
|
62
|
+
// A later run follows the earlier runs' work under the same lock hold, so
|
|
63
|
+
// its browser and port checks wait out the budgets again.
|
|
64
|
+
const later = { ...rearmSpentWaits(options) };
|
|
62
65
|
delete later.build;
|
|
63
66
|
delete later.extendCoverage;
|
|
64
67
|
delete later.refuseUnexportedDrift;
|
|
@@ -48,3 +48,14 @@ export declare const TEST_BUILD_PREFLIGHT: {
|
|
|
48
48
|
* @param options - Parsed test options
|
|
49
49
|
*/
|
|
50
50
|
export declare function runTestPreLockPreflight(projectRoot: string, testPaths: string[], options: TestOptions): Promise<TestOptions>;
|
|
51
|
+
/**
|
|
52
|
+
* `options` with the wait budgets the pre-lock pass spent put back, for a
|
|
53
|
+
* browser or port check that follows work this invocation already did
|
|
54
|
+
* under the lock: its pre-test build, or an earlier run of a sequence. A
|
|
55
|
+
* busy browser there is a peer that started after the lock was taken, and
|
|
56
|
+
* refusing would throw the build away, so it is waited out like the first
|
|
57
|
+
* check's.
|
|
58
|
+
*
|
|
59
|
+
* @param options - In-lock run options
|
|
60
|
+
*/
|
|
61
|
+
export declare function rearmSpentWaits(options: TestOptions): TestOptions;
|
|
@@ -219,15 +219,40 @@ async function refuseStalePackagingBeforeLock(projectRoot, engineDir, options, r
|
|
|
219
219
|
'against a fresh build of the requested paths.', error.note);
|
|
220
220
|
}
|
|
221
221
|
}
|
|
222
|
-
/**
|
|
222
|
+
/**
|
|
223
|
+
* `options` without one wait budget, which this pass already spent. The
|
|
224
|
+
* budget moves to `spentWaits`, for {@link rearmSpentWaits}.
|
|
225
|
+
*/
|
|
223
226
|
function withoutOption(options, key) {
|
|
224
|
-
const rest = { ...options };
|
|
227
|
+
const rest = { ...options, spentWaits: { ...options.spentWaits, [key]: options[key] } };
|
|
225
228
|
if (key === 'waitBrowser')
|
|
226
229
|
delete rest.waitBrowser;
|
|
227
230
|
else
|
|
228
231
|
delete rest.waitPort;
|
|
229
232
|
return rest;
|
|
230
233
|
}
|
|
234
|
+
/**
|
|
235
|
+
* `options` with the wait budgets the pre-lock pass spent put back, for a
|
|
236
|
+
* browser or port check that follows work this invocation already did
|
|
237
|
+
* under the lock: its pre-test build, or an earlier run of a sequence. A
|
|
238
|
+
* busy browser there is a peer that started after the lock was taken, and
|
|
239
|
+
* refusing would throw the build away, so it is waited out like the first
|
|
240
|
+
* check's.
|
|
241
|
+
*
|
|
242
|
+
* @param options - In-lock run options
|
|
243
|
+
*/
|
|
244
|
+
export function rearmSpentWaits(options) {
|
|
245
|
+
const spent = options.spentWaits;
|
|
246
|
+
if (spent === undefined)
|
|
247
|
+
return options;
|
|
248
|
+
const rearmed = { ...options };
|
|
249
|
+
if (spent.waitBrowser !== undefined)
|
|
250
|
+
rearmed.waitBrowser = spent.waitBrowser;
|
|
251
|
+
if (spent.waitPort !== undefined)
|
|
252
|
+
rearmed.waitPort = spent.waitPort;
|
|
253
|
+
delete rearmed.spentWaits;
|
|
254
|
+
return rearmed;
|
|
255
|
+
}
|
|
231
256
|
/**
|
|
232
257
|
* Spends `--wait-browser` and `--wait-port` before the lock, mirroring
|
|
233
258
|
* which in-lock checks would apply to this run, and strips each budget it
|
|
@@ -86,7 +86,8 @@ const TEST_HELP_TEXT = [
|
|
|
86
86
|
'launchable binary (with the build that left it missing, when one is',
|
|
87
87
|
'recorded), the named-file checkJs pass of --build, and the',
|
|
88
88
|
'--wait-browser/--wait-port budgets. Each is re-checked once under the',
|
|
89
|
-
'lock
|
|
89
|
+
'lock; the checks after a pre-test build or an earlier run of the',
|
|
90
|
+
'sequence wait the budgets out again.',
|
|
90
91
|
].join('\n');
|
|
91
92
|
function registerTestMultiRunOptions(test) {
|
|
92
93
|
return test
|
|
@@ -25,7 +25,7 @@ import { buildPerfSampleEnv, mergeHarnessEnv, resolveShuffleSeed } from './test-
|
|
|
25
25
|
import { removePgidFile, setActivePgidFile } from './test-harness-teardown.js';
|
|
26
26
|
import { assertPathlessTestMode, assertTestModeCombinations, canaryTimeoutSeconds, classifyBeforeDispatch, resolveCanaryPath, restartBetweenTestsArgs, } from './test-modes.js';
|
|
27
27
|
import { orchestrateTestRuns } from './test-orchestrate.js';
|
|
28
|
-
import { resolveLaunchablePathForTests, runTestPreLockPreflight, TEST_BUILD_PREFLIGHT, } from './test-prelock.js';
|
|
28
|
+
import { rearmSpentWaits, resolveLaunchablePathForTests, runTestPreLockPreflight, TEST_BUILD_PREFLIGHT, } from './test-prelock.js';
|
|
29
29
|
import { resetTestResults, withRawLogDir, writeTestResults } from './test-results.js';
|
|
30
30
|
import { DEFAULT_HARNESS_RETRIES } from './test-run.js';
|
|
31
31
|
import { emitFailVerdict, resetVerdictEmission, setVerdictRunAttribute, verdictEmitted, } from './test-verdict.js';
|
|
@@ -362,7 +362,9 @@ async function runTestCommandBody(projectRoot, testPaths, options = {}, profileE
|
|
|
362
362
|
if (buildId !== undefined)
|
|
363
363
|
setVerdictRunAttribute('build-id', buildId);
|
|
364
364
|
}
|
|
365
|
-
|
|
365
|
+
// After a pre-test build, a busy browser or port is a peer that started
|
|
366
|
+
// during it; waiting it out keeps the build this run just paid for.
|
|
367
|
+
const { forwardedPort, effectivePort } = await ensureTestBrowserEnvironment(paths.engine, launchablePath, xpcshellOnly, projectConfig, options.build === true ? rearmSpentWaits(options) : options, buildCheck.objDir);
|
|
366
368
|
if (options.doctor) {
|
|
367
369
|
const doctorOutcome = await runDoctorPreflight({
|
|
368
370
|
engineDir: paths.engine,
|
|
@@ -70,8 +70,8 @@ export function parseControlDiff(content, options = {}) {
|
|
|
70
70
|
refuse('no diff sections found. Write the control with git, e.g. ' +
|
|
71
71
|
'`git -C engine diff -- <file> > control.diff` after making the edit, then revert the edit.');
|
|
72
72
|
}
|
|
73
|
-
const
|
|
74
|
-
|
|
73
|
+
const files = new Map();
|
|
74
|
+
for (const section of sections) {
|
|
75
75
|
let path = normalizePathSlashes(section.targetPath);
|
|
76
76
|
if (section.isBinary)
|
|
77
77
|
refuse(`${path} is a binary section.`);
|
|
@@ -90,11 +90,36 @@ export function parseControlDiff(content, options = {}) {
|
|
|
90
90
|
}
|
|
91
91
|
if (section.hunks.length === 0)
|
|
92
92
|
refuse(`${path} has no hunks.`);
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
const existing = files.get(path);
|
|
94
|
+
if (existing === undefined) {
|
|
95
|
+
files.set(path, { path, hunks: [...section.hunks], sections: 1 });
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
existing.hunks.push(...section.hunks);
|
|
99
|
+
existing.sections += 1;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return [...files.values()].map(({ path, hunks, sections: count }) => ({
|
|
103
|
+
path,
|
|
104
|
+
hunks: count === 1 ? hunks : mergeSectionHunks(path, hunks),
|
|
105
|
+
}));
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Orders the hunks of a file that several sections name (a generator that
|
|
109
|
+
* writes one section per edit) as one section's would be. Hunks that
|
|
110
|
+
* overlap cannot be applied one after another, so they are refused.
|
|
111
|
+
*/
|
|
112
|
+
function mergeSectionHunks(path, hunks) {
|
|
113
|
+
const ordered = [...hunks].sort((left, right) => left.oldStart - right.oldStart);
|
|
114
|
+
ordered.forEach((hunk, index) => {
|
|
115
|
+
const previous = ordered[index - 1];
|
|
116
|
+
if (previous !== undefined && previous.oldStart + previous.oldCount > hunk.oldStart) {
|
|
117
|
+
refuse(`${path} appears in more than one section, and hunks from them overlap ` +
|
|
118
|
+
`(@@ -${previous.oldStart},${previous.oldCount} @@ and @@ -${hunk.oldStart},${hunk.oldCount} @@). ` +
|
|
119
|
+
'Make every edit to the file, then write one section for it with `git -C engine diff -- <file>`.');
|
|
120
|
+
}
|
|
97
121
|
});
|
|
122
|
+
return ordered;
|
|
98
123
|
}
|
|
99
124
|
function hunkSides(hunk, label, index) {
|
|
100
125
|
const oldLines = [];
|
|
@@ -139,7 +139,7 @@ export async function findCompiledSourcesNewerThanBinary(engineDir, baseline, la
|
|
|
139
139
|
}
|
|
140
140
|
return {
|
|
141
141
|
paths: paths.sort(),
|
|
142
|
-
linkedPath: relative(engineDir, linked.path),
|
|
142
|
+
linkedPath: normalizePathSlashes(relative(engineDir, linked.path)),
|
|
143
143
|
linkedAt: new Date(linked.mtimeMs),
|
|
144
144
|
};
|
|
145
145
|
}
|
|
@@ -648,6 +648,18 @@ export interface TestOptions extends EngineLockWaitable {
|
|
|
648
648
|
waitBrowser?: number | boolean;
|
|
649
649
|
/** Queue behind the mochitest/Marionette port holder. */
|
|
650
650
|
waitPort?: number | boolean;
|
|
651
|
+
/**
|
|
652
|
+
* The `--wait-browser`/`--wait-port` budgets the pre-lock pass already
|
|
653
|
+
* spent (and stripped from `waitBrowser`/`waitPort`). The first in-lock
|
|
654
|
+
* check probes once; a check that follows work this invocation did under
|
|
655
|
+
* the lock (its pre-test build, or an earlier run of a `--repeat` or
|
|
656
|
+
* `--negative-control` sequence) waits them out again, because refusing
|
|
657
|
+
* there throws that work away.
|
|
658
|
+
*/
|
|
659
|
+
spentWaits?: {
|
|
660
|
+
waitBrowser?: number | boolean;
|
|
661
|
+
waitPort?: number | boolean;
|
|
662
|
+
};
|
|
651
663
|
/** Also write the run log here, outside `.fireforge/logs/` retention. */
|
|
652
664
|
keepLog?: string;
|
|
653
665
|
/** Forward mochitest's `--restart-between-tests` (mochitest dispatch only). */
|
package/docs/configuration.md
CHANGED
|
@@ -656,6 +656,13 @@ owning process is gone.
|
|
|
656
656
|
every run, and a temporary profile starts without one.
|
|
657
657
|
- `--temp-profile` gives a plain `run` the same fresh, removed-afterwards
|
|
658
658
|
profile.
|
|
659
|
+
- Arguments after `--` go to the browser, after the profile ones. A headed
|
|
660
|
+
session on a kept profile that a chrome-context Marionette client drives
|
|
661
|
+
is `fireforge run --profile <dir> -- -marionette -remote-allow-system-access`,
|
|
662
|
+
with `marionette.port` set in the profile's `user.js` so peers' harnesses
|
|
663
|
+
keep 2828. The objdir app launched directly gets neither `-purgecaches`
|
|
664
|
+
nor the startup-cache cleanup, so it can serve a widget script an earlier
|
|
665
|
+
launch cached.
|
|
659
666
|
|
|
660
667
|
A smoke run whose browser exits with a clean status before the window
|
|
661
668
|
elapses, without printing any console output, fails with exit 13.
|
package/docs/patch-workflow.md
CHANGED
|
@@ -169,7 +169,7 @@ list rather than the subset left after filtering, so nothing dropped upstream
|
|
|
169
169
|
can pass as a silent success.
|
|
170
170
|
|
|
171
171
|
A file can be retired from a selected patch with repeatable
|
|
172
|
-
`--expect-removed <path
|
|
172
|
+
`--expect-removed <path>`. This works for an absent
|
|
173
173
|
file the patch created, and for an upstream file the patch modified or
|
|
174
174
|
deleted whose engine copy is back at HEAD:
|
|
175
175
|
|
|
@@ -190,7 +190,10 @@ validated before writes, in dry-run too. Other paths keep
|
|
|
190
190
|
both refusal checks; retirement grants no unmanaged or foreign-drift
|
|
191
191
|
exception. Removing every file would empty the patch and requires `patch
|
|
192
192
|
delete` (or `patch retire`, which keeps the order number) instead.
|
|
193
|
-
`--expect-removed` cannot combine with `--
|
|
193
|
+
`--expect-removed` cannot combine with `--files` or `--all`. It combines
|
|
194
|
+
with `--scan`, so one call can retire a widget's files from one patch while
|
|
195
|
+
`--scan-file` adopts a new module into another: the retired path leaves the
|
|
196
|
+
owner's file list before its scan, which cannot adopt it back.
|
|
194
197
|
|
|
195
198
|
The same run drops every staged declaration, registration or forward
|
|
196
199
|
import, in any patch of the queue, whose `creates` path it retired and no
|
|
@@ -386,11 +389,14 @@ hash-object` and `git update-index`. A collision (two rows at one order) or
|
|
|
386
389
|
copy and the index do not differ on key order alone.
|
|
387
390
|
- `patch staged-dependency <name> --remove` names the entries to remove by
|
|
388
391
|
any of `--file`, `--specifier`/`--line`, `--creates` and `--owner`. It needs
|
|
389
|
-
`--creates`, or `--file` with the matcher. `--
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
392
|
+
`--creates`, or `--file` with the matcher. Without `--kind`, `--specifier`
|
|
393
|
+
selects imports, `--line` selects registrations, and a selection with
|
|
394
|
+
neither searches both kinds, so `--creates` alone removes every
|
|
395
|
+
declaration for that created path, imports and registrations alike, which
|
|
396
|
+
is the cleanup after a merge deletes it. Registration lines compare
|
|
397
|
+
whitespace-insensitively, as the lint does. A selection that matches
|
|
398
|
+
nothing is refused with exit 8, on a dry run too, and the refusal lists
|
|
399
|
+
the patch's declarations of every kind it searched.
|
|
394
400
|
- `patch staged-dependency <name> --add --from <json-file>` adds many
|
|
395
401
|
declarations in one call under one lock hold. The file is a `lint
|
|
396
402
|
--per-patch --report` document (its findings' `stagedDeclarations` for
|
package/docs/testing.md
CHANGED
|
@@ -83,7 +83,10 @@ Furnace deployed it to, since that is what the run reads; the component
|
|
|
83
83
|
source itself is never touched. Only modifications of existing text files
|
|
84
84
|
are accepted: a created, deleted, renamed or binary file, or a symlinked
|
|
85
85
|
target, is refused. Hunks must match exactly; the only tolerance is a line
|
|
86
|
-
offset, as in `git apply`, never fuzz.
|
|
86
|
+
offset, as in `git apply`, never fuzz. A file named by more than one
|
|
87
|
+
section (a generator that writes one section per edit) is applied as one
|
|
88
|
+
section with every hunk in file order; sections whose hunks overlap are
|
|
89
|
+
refused, with the advice to write one section for the file.
|
|
87
90
|
|
|
88
91
|
The verdict says how the control came out with `control=`:
|
|
89
92
|
|
|
@@ -329,7 +332,11 @@ engine session lock is requested, so a run waiting out a browser or a port
|
|
|
329
332
|
does not hold every peer's build, test and re-export behind it. The run then
|
|
330
333
|
joins the lock queue and re-checks once under the lock; the normal preflight
|
|
331
334
|
refusal and its remedy apply if the resource is busy again (or still busy)
|
|
332
|
-
there.
|
|
335
|
+
there. A check that follows work the run already did under the lock waits
|
|
336
|
+
the budgets out again instead: the browser and port checks after a
|
|
337
|
+
`--build` run's pre-test build, and those of every later run of a `--repeat`
|
|
338
|
+
or `--negative-control` sequence, so a peer's browser that started during
|
|
339
|
+
the build does not throw the build away. With `--kill-stale-marionette` a reapable stale browser counts as
|
|
333
340
|
clear before the lock, and the port waits stay under the lock, where the
|
|
334
341
|
stale listener can be reaped.
|
|
335
342
|
|