mirror of
https://github.com/snapotter-hq/SnapOtter.git
synced 2026-08-03 07:46:42 +02:00
test: split the three oversized format-matrix specs (#651)
Vitest shards by file and runs a file's tests serially in one fork, so a single spec set the floor for the whole Integration job no matter how many shards or forks it got. Cost-aware sharding (#650) balanced the shards but could not get under that floor. Split the three specs that exceeded it: format-matrix-comprehensive (1365s) into 4 by describe, format-matrix (1130s) into 4 with Cross-format matrix striped over FORMAT_SAMPLES, and format-matrix-generated (779s) into 3 striped over TOOLS. Largest spec is now 370s. Each preamble moved verbatim into a sibling .shared.ts exposing setupMatrixApp(). Integration shards went from 20m59s/17m55s/16m33s/9m19s to 11m44s/12m31s/10m37s/11m1s. Coverage checked, not assumed: the set of test names collected by vitest list is byte-identical across the split, 2151 before and 2151 after. Per-shard totals matched the baseline exactly at 9903 tests, 9435 passed, 468 skipped.
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
/**
|
||||
* Comprehensive cross-format parameterized integration tests, part 4 of 4.
|
||||
*
|
||||
* Split from format-matrix-comprehensive.test.ts so the parts shard and run in
|
||||
* parallel; shared setup and helpers live in
|
||||
* ./format-matrix-comprehensive.shared.ts.
|
||||
*/
|
||||
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
assertDownloadResponse,
|
||||
CORE_FORMATS,
|
||||
callTool,
|
||||
type FormatDef,
|
||||
getTimeout,
|
||||
isAsyncFallback,
|
||||
PRIMARY_FORMATS,
|
||||
setupMatrixApp,
|
||||
} from "./format-matrix-comprehensive.shared.js";
|
||||
|
||||
vi.setConfig({ testTimeout: 60_000 });
|
||||
|
||||
setupMatrixApp();
|
||||
|
||||
describe("Crop across all 16 primary formats", () => {
|
||||
const CROP_CONFIGS = [
|
||||
{ label: "10x10 px at origin", settings: { width: 10, height: 10, left: 0, top: 0 } },
|
||||
{ label: "50x50 px at 5,5", settings: { width: 50, height: 50, left: 5, top: 5 } },
|
||||
] as const;
|
||||
|
||||
for (const cfg of CROP_CONFIGS) {
|
||||
describe(`crop ${cfg.label}`, () => {
|
||||
for (const fmt of PRIMARY_FORMATS) {
|
||||
it(
|
||||
`${fmt.name}`,
|
||||
async () => {
|
||||
const res = await callTool("crop", fmt, { ...cfg.settings });
|
||||
if (!res) return;
|
||||
assertDownloadResponse(res, fmt);
|
||||
},
|
||||
getTimeout(fmt),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// 3. ROTATE -- 16 formats x 4 angles + flip combos
|
||||
// =========================================================================
|
||||
|
||||
describe("Rotate across all 16 primary formats", () => {
|
||||
const ROTATE_CONFIGS = [
|
||||
{ label: "90 degrees", settings: { angle: 90 } },
|
||||
{ label: "180 degrees", settings: { angle: 180 } },
|
||||
{ label: "270 degrees", settings: { angle: 270 } },
|
||||
{ label: "horizontal flip", settings: { angle: 0, horizontal: true } },
|
||||
] as const;
|
||||
|
||||
for (const cfg of ROTATE_CONFIGS) {
|
||||
describe(`rotate ${cfg.label}`, () => {
|
||||
for (const fmt of PRIMARY_FORMATS) {
|
||||
it(
|
||||
`${fmt.name}`,
|
||||
async () => {
|
||||
const res = await callTool("rotate", fmt, { ...cfg.settings });
|
||||
if (!res) return;
|
||||
assertDownloadResponse(res, fmt);
|
||||
},
|
||||
getTimeout(fmt),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// 4. CONVERT -- each of the 16 formats -> JPEG, PNG, WebP
|
||||
// =========================================================================
|
||||
|
||||
describe("Sharpening across all 16 primary formats", () => {
|
||||
const SHARPEN_CONFIGS = [
|
||||
{ label: "adaptive method", settings: { method: "adaptive" } },
|
||||
{ label: "unsharp-mask method", settings: { method: "unsharp-mask" } },
|
||||
] as const;
|
||||
|
||||
for (const cfg of SHARPEN_CONFIGS) {
|
||||
describe(`sharpening ${cfg.label}`, () => {
|
||||
for (const fmt of PRIMARY_FORMATS) {
|
||||
it(
|
||||
`${fmt.name}`,
|
||||
async () => {
|
||||
const res = await callTool("sharpening", fmt, { ...cfg.settings });
|
||||
if (!res) return;
|
||||
assertDownloadResponse(res, fmt);
|
||||
},
|
||||
getTimeout(fmt),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// 8. STRIP-METADATA -- 16 formats + verify stripped output has fewer bytes
|
||||
// =========================================================================
|
||||
|
||||
describe("Border across all 16 primary formats", () => {
|
||||
const BORDER_CONFIGS = [
|
||||
{ label: "5px red", settings: { borderWidth: 5, borderColor: "#FF0000" } },
|
||||
{ label: "10px blue", settings: { borderWidth: 10, borderColor: "#0000FF" } },
|
||||
] as const;
|
||||
|
||||
for (const cfg of BORDER_CONFIGS) {
|
||||
describe(`border ${cfg.label}`, () => {
|
||||
for (const fmt of PRIMARY_FORMATS) {
|
||||
it(
|
||||
`${fmt.name}`,
|
||||
async () => {
|
||||
const res = await callTool("border", fmt, { ...cfg.settings });
|
||||
if (!res) return;
|
||||
assertDownloadResponse(res, fmt);
|
||||
},
|
||||
getTimeout(fmt),
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// 13. CHAINED OPERATIONS: core formats through resize then compress
|
||||
// =========================================================================
|
||||
|
||||
describe("Extended conversion targets (core formats)", () => {
|
||||
const EXTENDED_TARGETS = [
|
||||
{ format: "avif", ext: ".avif" },
|
||||
{ format: "tiff", ext: ".tiff" },
|
||||
{ format: "gif", ext: ".gif" },
|
||||
] as const;
|
||||
|
||||
for (const target of EXTENDED_TARGETS) {
|
||||
describe(`convert to ${target.format}`, () => {
|
||||
for (const fmt of CORE_FORMATS) {
|
||||
// Skip identity conversions
|
||||
if (fmt.name.toLowerCase() === target.format) continue;
|
||||
if (fmt.name === "AVIF" && target.format === "avif") continue;
|
||||
|
||||
const testTimeout = target.format === "avif" || target.format === "gif" ? 120_000 : 120_000;
|
||||
it(`${fmt.name} -> ${target.format}`, { timeout: testTimeout }, async () => {
|
||||
const res = await callTool("convert", fmt, { format: target.format });
|
||||
if (!res) return;
|
||||
if (isAsyncFallback(res)) return;
|
||||
expect(res.statusCode).toBe(200);
|
||||
|
||||
const body = JSON.parse(res.body);
|
||||
expect(body.downloadUrl).toBeDefined();
|
||||
expect(body.downloadUrl).toContain(target.ext);
|
||||
expect(body.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// 17. INFO CONSISTENCY: dimensions are consistent with resize output
|
||||
//
|
||||
// For core formats: get info, then resize to specific dimensions and
|
||||
// verify the resize succeeded (demonstrates info output is meaningful).
|
||||
// =========================================================================
|
||||
|
||||
describe("Info consistency check (core formats)", () => {
|
||||
for (const fmt of CORE_FORMATS) {
|
||||
it(`${fmt.name}: info dimensions are positive`, async () => {
|
||||
const res = await callTool("info", fmt, {});
|
||||
if (!res) return;
|
||||
expect(res.statusCode).toBe(200);
|
||||
|
||||
const body = JSON.parse(res.body);
|
||||
expect(body.width).toBeGreaterThan(0);
|
||||
expect(body.height).toBeGreaterThan(0);
|
||||
|
||||
// Verify we can resize to half the original dimensions
|
||||
const halfW = Math.max(1, Math.floor(body.width / 2));
|
||||
const halfH = Math.max(1, Math.floor(body.height / 2));
|
||||
|
||||
const resizeRes = await callTool("resize", fmt, { width: halfW, height: halfH });
|
||||
if (!resizeRes) return;
|
||||
expect(resizeRes.statusCode).toBe(200);
|
||||
|
||||
const resizeBody = JSON.parse(resizeRes.body);
|
||||
expect(resizeBody.processedSize).toBeGreaterThan(0);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// =========================================================================
|
||||
// 18. EXOTIC FORMAT ERROR SHAPE VERIFICATION
|
||||
//
|
||||
// Exotic formats that fail should return structured errors, not raw
|
||||
// stack traces or HTML error pages. This is a deeper check than the
|
||||
// no-crash matrix.
|
||||
// =========================================================================
|
||||
|
||||
describe("ICO (multi-size format) through tools", () => {
|
||||
const icoFmt: FormatDef = {
|
||||
name: "ICO",
|
||||
file: "sample.ico",
|
||||
mime: "image/x-icon",
|
||||
needsCliDecoder: true,
|
||||
needsHeifDecoder: false,
|
||||
mayFailValidation: false,
|
||||
};
|
||||
|
||||
const ICO_TOOLS = [
|
||||
{ id: "resize", settings: { width: 32, height: 32 } },
|
||||
{ id: "convert", settings: { format: "png" } },
|
||||
{ id: "info", settings: {} },
|
||||
{ id: "border", settings: { borderWidth: 2, borderColor: "#000000" } },
|
||||
{ id: "image-enhancement", settings: { mode: "auto", intensity: 50 } },
|
||||
];
|
||||
|
||||
for (const tool of ICO_TOOLS) {
|
||||
it(`${tool.id}: handles ICO input (may need ImageMagick)`, async () => {
|
||||
const res = await callTool(tool.id, icoFmt, tool.settings);
|
||||
if (!res) return;
|
||||
|
||||
// ICO requires CLI decoder; accept success or clean error
|
||||
expect(res.statusCode).not.toBe(500);
|
||||
expect([200, 202, 400, 422]).toContain(res.statusCode);
|
||||
|
||||
const body = JSON.parse(res.body);
|
||||
if (res.statusCode >= 400) {
|
||||
expect(body.error).toBeDefined();
|
||||
expect(typeof body.error).toBe("string");
|
||||
}
|
||||
}, 180_000);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user