fix(firmware): retain independent failures at each extraction owner
This commit is contained in:
1 parent
115a78fe24
commit
7298bd08d1
6 files changed
+186
-47
No files matched your search
@@ -108,7 +108,7 @@ const chunk = z.strictObject({
|
||||
handler: z.string().nullable(),
|
||||
encrypted: z.boolean().nullable(),
|
||||
root_file_range: range.nullable(),
|
||||
/** The producer reported extraction problems for this chunk. */
|
||||
/** The producer reported an extraction error or missing dependency for this chunk. */
|
||||
extraction_failed: z.boolean(),
|
||||
});
|
||||
/** One unextracted region or failing task/stage, with a supported continuation
|
||||
|
||||
@@ -178,7 +178,12 @@ export const publishFirmwareExtraction = async (context: {
|
||||
unpublished.length > 0 ? "partial" : context.normalized.coverage,
|
||||
files,
|
||||
unpublished_entries: unpublished,
|
||||
chunks: context.normalized.chunks,
|
||||
chunks: context.normalized.chunks.map(
|
||||
({ extraction_failure_kinds, ...chunk }) => ({
|
||||
...chunk,
|
||||
extraction_failed: extraction_failure_kinds.length > 0,
|
||||
}),
|
||||
),
|
||||
derivations: context.normalized.derivations,
|
||||
depth_limited_paths: context.normalized.depth_limited_paths,
|
||||
diagnostics: context.normalized.diagnostics,
|
||||
@@ -390,13 +395,6 @@ const extractionGaps = (
|
||||
? nestedContinuationId(chunk.input_path)
|
||||
: continuation(GAP_CONTINUATIONS[kind]),
|
||||
}));
|
||||
// Chunks whose failure a dependency row already covers stay out of the
|
||||
// generic extractor-failure emission.
|
||||
const dependencyChunkKeys = new Set(
|
||||
normalized.chunk_missing_dependencies.map(({ input_path, provider_id }) =>
|
||||
JSON.stringify([input_path, provider_id]),
|
||||
),
|
||||
);
|
||||
// A depth ceiling can only be re-rooted through a published child that a
|
||||
// fresh firmware input could actually accept; the index is only needed
|
||||
// when a depth row will actually consult it.
|
||||
@@ -451,17 +449,8 @@ const extractionGaps = (
|
||||
})),
|
||||
...chunkGaps("unknown-chunk-handler", (chunk) => chunk.handler === null),
|
||||
...chunkGaps("encrypted-chunk", (chunk) => chunk.encrypted === true),
|
||||
...chunkGaps(
|
||||
"extractor-failure",
|
||||
(chunk) =>
|
||||
chunk.extraction_failed === true &&
|
||||
chunk.handler !== null &&
|
||||
chunk.encrypted !== true &&
|
||||
// The chunk's own dependency row already carries its recovery; a
|
||||
// task-wide dependency names no region and replaces nothing.
|
||||
!dependencyChunkKeys.has(
|
||||
JSON.stringify([chunk.input_path, chunk.provider_id]),
|
||||
),
|
||||
...chunkGaps("extractor-failure", (chunk) =>
|
||||
chunk.extraction_failure_kinds.includes("extractor-failure"),
|
||||
),
|
||||
...normalized.depth_limited_paths.map((input_path): ExtractionGap => ({
|
||||
kind: "depth-limited",
|
||||
|
||||
@@ -376,13 +376,96 @@ describe("firmware producer chunk failures", () => {
|
||||
expect(normalized.chunks).toHaveLength(1);
|
||||
expect(normalized.chunks[0]).toMatchObject({
|
||||
handler: "zip",
|
||||
extraction_failed: true,
|
||||
extraction_failure_kinds: ["extractor-failure"],
|
||||
});
|
||||
expect(normalized.coverage).toBe("partial");
|
||||
});
|
||||
});
|
||||
|
||||
describe("firmware producer error severity", () => {
|
||||
it.each(["chunk", "multi-file"] as const)(
|
||||
"retains independent failure kinds at one %s owner",
|
||||
(owner) => {
|
||||
const record = root();
|
||||
const extraction_reports = [
|
||||
{
|
||||
__typename__: "ExtractorDependencyNotFoundReport",
|
||||
severity: "ERROR",
|
||||
dependencies: ["sasquatch"],
|
||||
},
|
||||
{
|
||||
__typename__: "UnknownError",
|
||||
severity: "ERROR",
|
||||
exception: "independent failure",
|
||||
},
|
||||
];
|
||||
const observation =
|
||||
owner === "chunk"
|
||||
? {
|
||||
__typename__: "ChunkReport",
|
||||
id: "stage-id",
|
||||
handler_name: "gzip",
|
||||
start_offset: 0,
|
||||
end_offset: 20,
|
||||
size: 20,
|
||||
is_encrypted: false,
|
||||
extraction_reports,
|
||||
}
|
||||
: {
|
||||
__typename__: "MultiFileReport",
|
||||
id: "stage-id",
|
||||
handler_name: "gzip",
|
||||
name: "rootfs",
|
||||
paths: [],
|
||||
extraction_reports,
|
||||
};
|
||||
const normalized = normalizeUnblobReport(
|
||||
[
|
||||
{
|
||||
...record,
|
||||
reports: [
|
||||
...record.reports.filter(
|
||||
(item) => item.__typename__ !== "UnknownChunkReport",
|
||||
),
|
||||
observation,
|
||||
],
|
||||
},
|
||||
],
|
||||
context,
|
||||
);
|
||||
if (owner === "chunk") {
|
||||
expect(normalized.chunks[0]?.extraction_failure_kinds).toEqual([
|
||||
"missing-extractor-dependency",
|
||||
"extractor-failure",
|
||||
]);
|
||||
expect(normalized.chunk_missing_dependencies).toEqual([
|
||||
{
|
||||
input_path: "$input",
|
||||
provider_id: "stage-id",
|
||||
dependencies: ["sasquatch"],
|
||||
},
|
||||
]);
|
||||
} else {
|
||||
expect(normalized.failed_extraction_stages).toEqual([
|
||||
{
|
||||
input_path: "$input",
|
||||
provider_id: "stage-id",
|
||||
handler: "gzip",
|
||||
dependencies: ["sasquatch"],
|
||||
kind: "missing-extractor-dependency",
|
||||
},
|
||||
{
|
||||
input_path: "$input",
|
||||
provider_id: "stage-id",
|
||||
handler: "gzip",
|
||||
dependencies: [],
|
||||
kind: "extractor-failure",
|
||||
},
|
||||
]);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const outcomes = [
|
||||
{
|
||||
name: "task exception",
|
||||
@@ -470,7 +553,9 @@ describe("firmware producer error severity", () => {
|
||||
report: owner === "multi-file" ? observation : report,
|
||||
});
|
||||
if (owner === "chunk") {
|
||||
expect(normalized.chunks[0]?.extraction_failed).toBe(failed);
|
||||
expect(normalized.chunks[0]?.extraction_failure_kinds).toEqual(
|
||||
failed ? ["extractor-failure"] : [],
|
||||
);
|
||||
expect(normalized.failed_extraction_stages).toEqual([]);
|
||||
} else {
|
||||
expect(normalized.failed_extraction_stages).toEqual(
|
||||
|
||||
@@ -65,8 +65,21 @@ const missingDependency = z.object({
|
||||
// The declared severity owns failure; a diagnostic's presence or class name
|
||||
// alone does not establish that extraction failed.
|
||||
const extractionError = z.object({ severity: z.literal("ERROR") });
|
||||
const hasExtractionError = (reports: readonly unknown[]): boolean =>
|
||||
reports.some((item) => extractionError.safeParse(item).success);
|
||||
type ExtractionFailureKind =
|
||||
| "missing-extractor-dependency"
|
||||
| "extractor-failure";
|
||||
const extractionFailureKinds = (
|
||||
reports: readonly unknown[],
|
||||
): ExtractionFailureKind[] => {
|
||||
const kinds = new Set<ExtractionFailureKind>();
|
||||
for (const item of reports) {
|
||||
if (missingDependency.safeParse(item).success)
|
||||
kinds.add("missing-extractor-dependency");
|
||||
else if (extractionError.safeParse(item).success)
|
||||
kinds.add("extractor-failure");
|
||||
}
|
||||
return [...kinds];
|
||||
};
|
||||
const region = z.object({
|
||||
id: z.string(),
|
||||
start_offset: integer,
|
||||
@@ -240,7 +253,7 @@ export const normalizeUnblobReport = (
|
||||
handler: string | null;
|
||||
encrypted: boolean | null;
|
||||
root_file_range: { offset: number; length: number } | null;
|
||||
extraction_failed: boolean;
|
||||
extraction_failure_kinds: ExtractionFailureKind[];
|
||||
}[] = [];
|
||||
const seen = new Set<string>();
|
||||
const taskFailures = new Set<string>();
|
||||
@@ -365,8 +378,7 @@ export const normalizeUnblobReport = (
|
||||
);
|
||||
noteMissingDependencies(path, [item]);
|
||||
if (
|
||||
hasExtractionError([item]) &&
|
||||
!missingDependency.safeParse(item).success &&
|
||||
extractionFailureKinds([item]).includes("extractor-failure") &&
|
||||
!taskFailures.has(path)
|
||||
) {
|
||||
taskFailures.add(path);
|
||||
@@ -402,8 +414,9 @@ export const normalizeUnblobReport = (
|
||||
})
|
||||
.parse(item)
|
||||
: null;
|
||||
const extractionFailed =
|
||||
detail !== null && hasExtractionError(detail.extraction_reports);
|
||||
const failures = extractionFailureKinds(
|
||||
detail?.extraction_reports ?? [],
|
||||
);
|
||||
if (detail !== null)
|
||||
for (const problem of detail.extraction_reports) {
|
||||
diagnostics.push(
|
||||
@@ -426,7 +439,7 @@ export const normalizeUnblobReport = (
|
||||
range: { offset: parsed.start_offset, length: parsed.size },
|
||||
handler: detail?.handler_name ?? null,
|
||||
encrypted: detail?.is_encrypted ?? null,
|
||||
extraction_failed: extractionFailed,
|
||||
extraction_failure_kinds: failures,
|
||||
root_file_range:
|
||||
path === "$input"
|
||||
? {
|
||||
@@ -453,28 +466,26 @@ export const normalizeUnblobReport = (
|
||||
// The report owns its failure and dependencies. A produced subtask
|
||||
// is a derivation, not a new owner for the parent's report.
|
||||
const dependencies = dependencyNames(parsed.extraction_reports);
|
||||
if (
|
||||
dependencies === null &&
|
||||
!hasExtractionError(parsed.extraction_reports)
|
||||
)
|
||||
continue;
|
||||
const rows =
|
||||
dependencies === null
|
||||
? [{ dependencies: [] as readonly string[] }]
|
||||
: dependencyRows(dependencies);
|
||||
const rows = [
|
||||
...(dependencies === null
|
||||
? []
|
||||
: dependencyRows(dependencies).map((row) => ({
|
||||
...row,
|
||||
kind: "missing-extractor-dependency" as const,
|
||||
}))),
|
||||
...(extractionFailureKinds(parsed.extraction_reports).includes(
|
||||
"extractor-failure",
|
||||
)
|
||||
? [{ dependencies: [], kind: "extractor-failure" as const }]
|
||||
: []),
|
||||
];
|
||||
for (const row of rows)
|
||||
failedExtractionStages.push({
|
||||
input_path: path,
|
||||
provider_id: parsed.id,
|
||||
handler: parsed.handler_name,
|
||||
dependencies:
|
||||
dependencies === null
|
||||
? []
|
||||
: uniqueSorted([...row.dependencies]),
|
||||
kind:
|
||||
dependencies === null
|
||||
? "extractor-failure"
|
||||
: "missing-extractor-dependency",
|
||||
dependencies: row.dependencies,
|
||||
kind: row.kind,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -567,6 +567,35 @@ it.each(["task-error", "task-warning"])(
|
||||
},
|
||||
);
|
||||
|
||||
it("retains independent failures beside dependencies at each extraction owner", async () => {
|
||||
const fixture = await firmwareFixture("mixed-failures");
|
||||
const result = await fixture.service.execute("extract_firmware", {
|
||||
path: fixture.path,
|
||||
output_directory: fixture.output,
|
||||
});
|
||||
if (!result.ok) throw result.error;
|
||||
const normalized = firmwareResultSchemas.extract_firmware.parse(
|
||||
result.value.normalized_result,
|
||||
);
|
||||
const gaps = gapsWithContinuations(normalized);
|
||||
expect(gaps).toHaveLength(4);
|
||||
for (const provider_id of ["gzip-id", "mixed-id"]) {
|
||||
const owned = gaps.filter((gap) => gap.provider_id === provider_id);
|
||||
expect(owned.map((gap) => gap.kind).sort()).toEqual([
|
||||
"extractor-failure",
|
||||
"missing-extractor-dependency",
|
||||
]);
|
||||
for (const gap of owned) {
|
||||
expect(gap.input_path).toBe("$input");
|
||||
expect(gap.range).toEqual(
|
||||
provider_id === "mixed-id" ? null : normalized.chunks[0]?.range,
|
||||
);
|
||||
}
|
||||
}
|
||||
expect(normalized.chunks[0]?.extraction_failed).toBe(true);
|
||||
await assertFirmwareCleanup(fixture.launches);
|
||||
});
|
||||
|
||||
it("carries the owning multi-file stage on a task-level dependency gap", async () => {
|
||||
const fixture = await firmwareFixture("multi-file-dependency");
|
||||
const result = await fixture.service.execute("extract_firmware", {
|
||||
|
||||
Vendored
+25
@@ -359,6 +359,31 @@ if (args.includes("--version")) {
|
||||
subtasks: [childTask],
|
||||
},
|
||||
];
|
||||
if (mode === "mixed-failures") {
|
||||
const reports = [
|
||||
{
|
||||
__typename__: "ExtractorDependencyNotFoundReport",
|
||||
severity: "ERROR",
|
||||
dependencies: ["sasquatch"],
|
||||
},
|
||||
{
|
||||
__typename__: "UnknownError",
|
||||
severity: "ERROR",
|
||||
exception: "independent extraction failure",
|
||||
},
|
||||
];
|
||||
records[1].reports.find(
|
||||
(report) => report.__typename__ === "ChunkReport",
|
||||
).extraction_reports = reports;
|
||||
records[1].reports.push({
|
||||
__typename__: "MultiFileReport",
|
||||
id: "mixed-id",
|
||||
handler_name: "tar",
|
||||
name: "rootfs.tar",
|
||||
paths: [],
|
||||
extraction_reports: reports,
|
||||
});
|
||||
}
|
||||
if (mode === "many-unknown") {
|
||||
records[1].reports = [
|
||||
...records[1].reports,
|
||||
|
||||
Reference in new issue
Block a user