@rehearsal-labs/retest 0.1.0 → 0.2.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/README.md +6 -0
- package/dist/agent/session.d.ts +21 -2
- package/dist/agent/session.d.ts.map +1 -1
- package/dist/agent/session.js +73 -11
- package/dist/api/app-page.d.ts +39 -3
- package/dist/api/app-page.d.ts.map +1 -1
- package/dist/api/app-page.js +108 -0
- package/dist/api/dialog.d.ts +38 -0
- package/dist/api/dialog.d.ts.map +1 -0
- package/dist/api/dialog.js +78 -0
- package/dist/api/drag.d.ts +32 -0
- package/dist/api/drag.d.ts.map +1 -0
- package/dist/api/drag.js +26 -0
- package/dist/api/files.d.ts +49 -0
- package/dist/api/files.d.ts.map +1 -0
- package/dist/api/files.js +168 -0
- package/dist/api/inbox.d.ts +97 -0
- package/dist/api/inbox.d.ts.map +1 -0
- package/dist/api/inbox.js +191 -0
- package/dist/api/locator-recipes.d.ts +1 -1
- package/dist/api/locator-recipes.d.ts.map +1 -1
- package/dist/api/locator-recipes.js +4 -3
- package/dist/api/page.d.ts +214 -4
- package/dist/api/page.d.ts.map +1 -1
- package/dist/api/route.d.ts +21 -0
- package/dist/api/route.d.ts.map +1 -0
- package/dist/api/route.js +99 -0
- package/dist/api/test-run.d.ts +127 -1
- package/dist/api/test-run.d.ts.map +1 -1
- package/dist/api/test-run.js +293 -0
- package/dist/assertions/expect.d.ts +91 -2
- package/dist/assertions/expect.d.ts.map +1 -1
- package/dist/assertions/expect.js +98 -0
- package/dist/assertions/look-until.d.ts +5 -0
- package/dist/assertions/look-until.d.ts.map +1 -1
- package/dist/assertions/look-until.js +1 -1
- package/dist/assertions/poll-layout.d.ts +24 -0
- package/dist/assertions/poll-layout.d.ts.map +1 -0
- package/dist/assertions/poll-layout.js +63 -0
- package/dist/assertions/screenshot.d.ts +36 -0
- package/dist/assertions/screenshot.d.ts.map +1 -0
- package/dist/assertions/screenshot.js +125 -0
- package/dist/browser/accessibility.d.ts.map +1 -1
- package/dist/browser/accessibility.js +22 -1
- package/dist/browser/actionability.d.ts +9 -6
- package/dist/browser/actionability.d.ts.map +1 -1
- package/dist/browser/actionability.js +17 -8
- package/dist/browser/browser.d.ts.map +1 -1
- package/dist/browser/browser.js +15 -4
- package/dist/browser/cdp/connection.d.ts +6 -0
- package/dist/browser/cdp/connection.d.ts.map +1 -1
- package/dist/browser/cdp/connection.js +8 -0
- package/dist/browser/checked-state.d.ts +2 -2
- package/dist/browser/checked-state.d.ts.map +1 -1
- package/dist/browser/chromium-process.d.ts +9 -2
- package/dist/browser/chromium-process.d.ts.map +1 -1
- package/dist/browser/chromium-process.js +17 -4
- package/dist/browser/contract.d.ts +119 -5
- package/dist/browser/contract.d.ts.map +1 -1
- package/dist/browser/dialogs.d.ts +157 -0
- package/dist/browser/dialogs.d.ts.map +1 -0
- package/dist/browser/dialogs.js +271 -0
- package/dist/browser/downloads.d.ts +71 -0
- package/dist/browser/downloads.d.ts.map +1 -0
- package/dist/browser/downloads.js +253 -0
- package/dist/browser/drag.d.ts +50 -0
- package/dist/browser/drag.d.ts.map +1 -0
- package/dist/browser/drag.js +584 -0
- package/dist/browser/electron-window-watch.d.ts +32 -0
- package/dist/browser/electron-window-watch.d.ts.map +1 -0
- package/dist/browser/electron-window-watch.js +93 -0
- package/dist/browser/electron.d.ts +13 -2
- package/dist/browser/electron.d.ts.map +1 -1
- package/dist/browser/electron.js +53 -8
- package/dist/browser/element-queries.d.ts +51 -15
- package/dist/browser/element-queries.d.ts.map +1 -1
- package/dist/browser/element-queries.js +92 -13
- package/dist/browser/emulation.d.ts.map +1 -1
- package/dist/browser/emulation.js +7 -5
- package/dist/browser/file-chooser.d.ts +75 -0
- package/dist/browser/file-chooser.d.ts.map +1 -0
- package/dist/browser/file-chooser.js +449 -0
- package/dist/browser/firefox/input.d.ts +5 -0
- package/dist/browser/firefox/input.d.ts.map +1 -1
- package/dist/browser/firefox/input.js +6 -3
- package/dist/browser/firefox/page.d.ts +11 -1
- package/dist/browser/firefox/page.d.ts.map +1 -1
- package/dist/browser/firefox/page.js +66 -7
- package/dist/browser/frames.d.ts +205 -0
- package/dist/browser/frames.d.ts.map +1 -0
- package/dist/browser/frames.js +657 -0
- package/dist/browser/input-guard.d.ts +18 -5
- package/dist/browser/input-guard.d.ts.map +1 -1
- package/dist/browser/input-guard.js +65 -6
- package/dist/browser/isolated-world.d.ts +8 -1
- package/dist/browser/isolated-world.d.ts.map +1 -1
- package/dist/browser/layout.d.ts +120 -0
- package/dist/browser/layout.d.ts.map +1 -0
- package/dist/browser/layout.js +213 -0
- package/dist/browser/locate.d.ts +10 -0
- package/dist/browser/locate.d.ts.map +1 -1
- package/dist/browser/locate.js +24 -6
- package/dist/browser/origin-document.d.ts +4 -0
- package/dist/browser/origin-document.d.ts.map +1 -1
- package/dist/browser/origin-document.js +9 -1
- package/dist/browser/page-scripts.d.ts +48 -0
- package/dist/browser/page-scripts.d.ts.map +1 -1
- package/dist/browser/page-scripts.js +515 -39
- package/dist/browser/page.d.ts +60 -1
- package/dist/browser/page.d.ts.map +1 -1
- package/dist/browser/page.js +275 -32
- package/dist/browser/popups.d.ts +166 -0
- package/dist/browser/popups.d.ts.map +1 -0
- package/dist/browser/popups.js +317 -0
- package/dist/browser/routes.d.ts +97 -0
- package/dist/browser/routes.d.ts.map +1 -0
- package/dist/browser/routes.js +226 -0
- package/dist/browser/screenshot.d.ts +44 -0
- package/dist/browser/screenshot.d.ts.map +1 -0
- package/dist/browser/screenshot.js +51 -0
- package/dist/browser/shadow-candidates.d.ts +20 -0
- package/dist/browser/shadow-candidates.d.ts.map +1 -0
- package/dist/browser/shadow-candidates.js +77 -0
- package/dist/browser/webkit/page.d.ts +11 -1
- package/dist/browser/webkit/page.d.ts.map +1 -1
- package/dist/browser/webkit/page.js +53 -2
- package/dist/browser/webkit/relay.d.ts.map +1 -1
- package/dist/browser/webkit/relay.js +5 -1
- package/dist/cli/commands/run.d.ts.map +1 -1
- package/dist/cli/commands/run.js +2 -0
- package/dist/cli/inspect/test-timeline.d.ts.map +1 -1
- package/dist/cli/inspect/test-timeline.js +38 -4
- package/dist/config/loaded.d.ts +12 -2
- package/dist/config/loaded.d.ts.map +1 -1
- package/dist/config/read-inboxes.d.ts +10 -0
- package/dist/config/read-inboxes.d.ts.map +1 -0
- package/dist/config/read-inboxes.js +78 -0
- package/dist/config/register.d.ts +5 -1
- package/dist/config/register.d.ts.map +1 -1
- package/dist/config/types.d.ts +36 -0
- package/dist/config/types.d.ts.map +1 -1
- package/dist/config/validate.d.ts.map +1 -1
- package/dist/config/validate.js +4 -1
- package/dist/diagnostics/artifact.d.ts.map +1 -1
- package/dist/diagnostics/artifact.js +10 -2
- package/dist/diagnostics/attempt.d.ts +5 -0
- package/dist/diagnostics/attempt.d.ts.map +1 -1
- package/dist/diagnostics/attempt.js +10 -0
- package/dist/diagnostics/chromium-collector.d.ts +9 -1
- package/dist/diagnostics/chromium-collector.d.ts.map +1 -1
- package/dist/diagnostics/chromium-collector.js +19 -4
- package/dist/diagnostics/observations.d.ts +8 -0
- package/dist/diagnostics/observations.d.ts.map +1 -1
- package/dist/diagnostics/session-capture.d.ts.map +1 -1
- package/dist/diagnostics/session-capture.js +8 -0
- package/dist/index.d.ts +9 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/media/policy.d.ts +2 -2
- package/dist/media/policy.d.ts.map +1 -1
- package/dist/media/policy.js +1 -1
- package/dist/playwright/not-yet.d.ts.map +1 -1
- package/dist/playwright/not-yet.js +5 -2
- package/dist/protocol/commands.d.ts +122 -13
- package/dist/protocol/commands.d.ts.map +1 -1
- package/dist/protocol/commands.js +85 -6
- package/dist/protocol/diagnostics.d.ts +10 -0
- package/dist/protocol/diagnostics.d.ts.map +1 -1
- package/dist/protocol/diagnostics.js +5 -0
- package/dist/protocol/dialogs.d.ts +104 -0
- package/dist/protocol/dialogs.d.ts.map +1 -0
- package/dist/protocol/dialogs.js +101 -0
- package/dist/protocol/drag.d.ts +45 -0
- package/dist/protocol/drag.d.ts.map +1 -0
- package/dist/protocol/drag.js +31 -0
- package/dist/protocol/events.d.ts +153 -1
- package/dist/protocol/events.d.ts.map +1 -1
- package/dist/protocol/events.js +97 -0
- package/dist/protocol/files.d.ts +83 -0
- package/dist/protocol/files.d.ts.map +1 -0
- package/dist/protocol/files.js +68 -0
- package/dist/protocol/inbox.d.ts +87 -0
- package/dist/protocol/inbox.d.ts.map +1 -0
- package/dist/protocol/inbox.js +51 -0
- package/dist/protocol/index.d.ts +4 -1
- package/dist/protocol/index.d.ts.map +1 -1
- package/dist/protocol/layout-checks.d.ts +83 -0
- package/dist/protocol/layout-checks.d.ts.map +1 -0
- package/dist/protocol/layout-checks.js +214 -0
- package/dist/protocol/layout.d.ts +135 -0
- package/dist/protocol/layout.d.ts.map +1 -0
- package/dist/protocol/layout.js +124 -0
- package/dist/protocol/locator-checks.d.ts +19 -2
- package/dist/protocol/locator-checks.d.ts.map +1 -1
- package/dist/protocol/locator-checks.js +23 -1
- package/dist/protocol/locator.d.ts +55 -7
- package/dist/protocol/locator.d.ts.map +1 -1
- package/dist/protocol/locator.js +99 -18
- package/dist/protocol/messages.d.ts +95 -0
- package/dist/protocol/messages.d.ts.map +1 -1
- package/dist/protocol/messages.js +63 -0
- package/dist/protocol/popups.d.ts +43 -0
- package/dist/protocol/popups.d.ts.map +1 -0
- package/dist/protocol/popups.js +27 -0
- package/dist/protocol/result.d.ts +5 -0
- package/dist/protocol/result.d.ts.map +1 -1
- package/dist/protocol/result.js +1 -0
- package/dist/protocol/routes.d.ts +136 -0
- package/dist/protocol/routes.d.ts.map +1 -0
- package/dist/protocol/routes.js +191 -0
- package/dist/protocol/screenshot.d.ts +219 -0
- package/dist/protocol/screenshot.d.ts.map +1 -0
- package/dist/protocol/screenshot.js +335 -0
- package/dist/reporters/actions.d.ts +6 -4
- package/dist/reporters/actions.d.ts.map +1 -1
- package/dist/reporters/actions.js +24 -6
- package/dist/reporters/agent.d.ts.map +1 -1
- package/dist/reporters/agent.js +4 -2
- package/dist/reporters/dialogs-popups.d.ts +22 -0
- package/dist/reporters/dialogs-popups.d.ts.map +1 -0
- package/dist/reporters/dialogs-popups.js +30 -0
- package/dist/reporters/failure-card.d.ts +10 -1
- package/dist/reporters/failure-card.d.ts.map +1 -1
- package/dist/reporters/failure-card.js +5 -1
- package/dist/reporters/format.d.ts +33 -0
- package/dist/reporters/format.d.ts.map +1 -1
- package/dist/reporters/format.js +42 -0
- package/dist/reporters/html/comparison-view.d.ts +12 -0
- package/dist/reporters/html/comparison-view.d.ts.map +1 -0
- package/dist/reporters/html/comparison-view.js +21 -0
- package/dist/reporters/html/diagnostics-view.js +8 -0
- package/dist/reporters/html/downloads-view.d.ts +10 -0
- package/dist/reporters/html/downloads-view.d.ts.map +1 -0
- package/dist/reporters/html/downloads-view.js +29 -0
- package/dist/reporters/html/style.d.ts.map +1 -1
- package/dist/reporters/html/style.js +3 -0
- package/dist/reporters/html/summary-view.d.ts.map +1 -1
- package/dist/reporters/html/summary-view.js +3 -2
- package/dist/reporters/html/test-view.d.ts.map +1 -1
- package/dist/reporters/html/test-view.js +6 -3
- package/dist/reporters/html/timeline-view.d.ts.map +1 -1
- package/dist/reporters/html/timeline-view.js +26 -2
- package/dist/reporters/human-card.js +10 -0
- package/dist/reporters/human.d.ts.map +1 -1
- package/dist/reporters/human.js +3 -1
- package/dist/reporters/routes.d.ts +9 -0
- package/dist/reporters/routes.d.ts.map +1 -0
- package/dist/reporters/routes.js +17 -0
- package/dist/runner/child-program.js +10 -0
- package/dist/runner/contract.d.ts +5 -0
- package/dist/runner/contract.d.ts.map +1 -1
- package/dist/runner/dialogs.d.ts +71 -0
- package/dist/runner/dialogs.d.ts.map +1 -0
- package/dist/runner/dialogs.js +189 -0
- package/dist/runner/files.d.ts +79 -0
- package/dist/runner/files.d.ts.map +1 -0
- package/dist/runner/files.js +312 -0
- package/dist/runner/inbox-links.d.ts +19 -0
- package/dist/runner/inbox-links.d.ts.map +1 -0
- package/dist/runner/inbox-links.js +63 -0
- package/dist/runner/inbox.d.ts +87 -0
- package/dist/runner/inbox.d.ts.map +1 -0
- package/dist/runner/inbox.js +340 -0
- package/dist/runner/index.d.ts +2 -1
- package/dist/runner/index.d.ts.map +1 -1
- package/dist/runner/native-pool.d.ts.map +1 -1
- package/dist/runner/native-pool.js +2 -0
- package/dist/runner/observations.d.ts +15 -2
- package/dist/runner/observations.d.ts.map +1 -1
- package/dist/runner/observations.js +109 -6
- package/dist/runner/popups.d.ts +82 -0
- package/dist/runner/popups.d.ts.map +1 -0
- package/dist/runner/popups.js +236 -0
- package/dist/runner/redactor.d.ts.map +1 -1
- package/dist/runner/redactor.js +10 -6
- package/dist/runner/routes.d.ts +23 -0
- package/dist/runner/routes.d.ts.map +1 -0
- package/dist/runner/routes.js +128 -0
- package/dist/runner/run-media.d.ts +6 -0
- package/dist/runner/run-media.d.ts.map +1 -1
- package/dist/runner/run-media.js +21 -0
- package/dist/runner/run-session.d.ts.map +1 -1
- package/dist/runner/run-session.js +93 -8
- package/dist/runner/running-test.d.ts +35 -0
- package/dist/runner/running-test.d.ts.map +1 -1
- package/dist/runner/running-test.js +333 -22
- package/dist/runner/screenshot-baselines.d.ts +159 -0
- package/dist/runner/screenshot-baselines.d.ts.map +1 -0
- package/dist/runner/screenshot-baselines.js +357 -0
- package/dist/runner/secrets.d.ts +13 -0
- package/dist/runner/secrets.d.ts.map +1 -1
- package/dist/runner/secrets.js +6 -4
- package/dist/runner/test-pages.d.ts +3 -1
- package/dist/runner/test-pages.d.ts.map +1 -1
- package/dist/runner/test-pages.js +4 -3
- package/dist/schemas/event-v1.schema.json +118873 -6311
- package/dist/schemas/result-v1.schema.json +4 -0
- package/dist/shared/image-diff.d.ts +36 -0
- package/dist/shared/image-diff.d.ts.map +1 -0
- package/dist/shared/image-diff.js +242 -0
- package/dist/shared/metadata-process.d.ts +4 -1
- package/dist/shared/metadata-process.d.ts.map +1 -1
- package/dist/shared/metadata-process.js +51 -9
- package/dist/shared/png.d.ts +35 -0
- package/dist/shared/png.d.ts.map +1 -0
- package/dist/shared/png.js +251 -0
- package/dist/shared/process-ownership.d.ts +15 -4
- package/dist/shared/process-ownership.d.ts.map +1 -1
- package/dist/shared/process-ownership.js +123 -34
- package/dist/store/artifacts.d.ts +23 -2
- package/dist/store/artifacts.d.ts.map +1 -1
- package/dist/store/artifacts.js +40 -1
- package/dist/store/rebuild-result.d.ts.map +1 -1
- package/dist/store/rebuild-result.js +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { RgbaImage } from './png.ts';
|
|
2
|
+
/** The colour distance, from 0 to 1, above which a pixel counts as different, as Playwright's `toHaveScreenshot` sets it. */
|
|
3
|
+
export declare const defaultThreshold = 0.2;
|
|
4
|
+
/** How two pictures of one size differ: the pixels counted as different, and the pixels left out as anti-aliasing. */
|
|
5
|
+
export type PixelDifference = {
|
|
6
|
+
readonly differentPixels: number;
|
|
7
|
+
readonly antiAliasedPixels: number;
|
|
8
|
+
};
|
|
9
|
+
/** The picture `differencePicture` draws, and the count it drew. */
|
|
10
|
+
export type DrawnDifference = PixelDifference & {
|
|
11
|
+
readonly picture: RgbaImage;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Whether two pictures are the same size and the same in every byte of every pixel.
|
|
15
|
+
*
|
|
16
|
+
* @example samePixels(first, second) // true
|
|
17
|
+
*/
|
|
18
|
+
export declare function samePixels(first: RgbaImage, second: RgbaImage): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Counts the pixels of two pictures of one size whose colours are further apart than `threshold`, from 0 to 1, leaving
|
|
21
|
+
* out those that are anti-aliasing in either picture. Throws `RangeError` for pictures of different sizes or a
|
|
22
|
+
* threshold outside 0 to 1.
|
|
23
|
+
*
|
|
24
|
+
* @example comparePixels(baseline, picture, 0.2).differentPixels // 0
|
|
25
|
+
*/
|
|
26
|
+
export declare function comparePixels(expected: RgbaImage, actual: RgbaImage, threshold?: number): PixelDifference;
|
|
27
|
+
/**
|
|
28
|
+
* Draws where two pictures differ, on a canvas as large as the larger of the two each way: the actual picture faded
|
|
29
|
+
* to grey, and each differing pixel in red. A pixel that only one picture covers is red. Anti-aliased pixels stay
|
|
30
|
+
* faded. The counts are those of `comparePixels` when the sizes match, and count the uncovered pixels as different
|
|
31
|
+
* when they do not.
|
|
32
|
+
*
|
|
33
|
+
* @example differencePicture(baseline, picture, 0.2).picture.width // 1280
|
|
34
|
+
*/
|
|
35
|
+
export declare function differencePicture(expected: RgbaImage, actual: RgbaImage, threshold?: number): DrawnDifference;
|
|
36
|
+
//# sourceMappingURL=image-diff.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"image-diff.d.ts","sourceRoot":"","sources":["../../src/shared/image-diff.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAA;AA0BzC,6HAA6H;AAC7H,eAAO,MAAM,gBAAgB,MAAM,CAAA;AAEnC,sHAAsH;AACtH,MAAM,MAAM,eAAe,GAAG;IAAE,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAA;CAAE,CAAA;AAEtG,oEAAoE;AACpE,MAAM,MAAM,eAAe,GAAG,eAAe,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,SAAS,CAAA;CAAE,CAAA;AAQ/E;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,GAAG,OAAO,CAGvE;AAED;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAE,MAAyB,GAAG,eAAe,CAG3H;AAED;;;;;;;GAOG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,GAAE,MAAyB,GAAG,eAAe,CAsB/H"}
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// How two pictures differ, pixel by pixel, for a screenshot comparison. A pixel differs when the distance between its
|
|
2
|
+
// two colours, in the YIQ space that weighs brightness the way eyes do, is above a threshold; a pixel that differs only
|
|
3
|
+
// because an edge is anti-aliased a little differently is not counted.
|
|
4
|
+
//
|
|
5
|
+
// The colour distance and the anti-aliasing detector are adapted from pixelmatch 7.2.0 by Mapbox, read as published on
|
|
6
|
+
// npm, and match the version Playwright 1.63.0 vendors, whose `toHaveScreenshot` counts pixels this way, so a threshold
|
|
7
|
+
// means here what it means there. The distance follows Kotsarenko and Ramos, "Measuring perceived color difference using
|
|
8
|
+
// YIQ NTSC transmission color space in mobile applications" (2010); the detector follows Vysniauskas, "Anti-aliased
|
|
9
|
+
// pixel and intensity slope detector" (2009). Semi-transparent pixels are blended on white, as Playwright's copy does.
|
|
10
|
+
// pixelmatch's licence:
|
|
11
|
+
//
|
|
12
|
+
// ISC License
|
|
13
|
+
//
|
|
14
|
+
// Copyright (c) 2025, Mapbox
|
|
15
|
+
//
|
|
16
|
+
// Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby
|
|
17
|
+
// granted, provided that the above copyright notice and this permission notice appear in all copies.
|
|
18
|
+
//
|
|
19
|
+
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING
|
|
20
|
+
// ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL,
|
|
21
|
+
// DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
|
22
|
+
// WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE
|
|
23
|
+
// USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
24
|
+
/** The colour distance, from 0 to 1, above which a pixel counts as different, as Playwright's `toHaveScreenshot` sets it. */
|
|
25
|
+
export const defaultThreshold = 0.2;
|
|
26
|
+
// The largest squared distance two colours can have in YIQ, so a threshold of 1 lets every colour through.
|
|
27
|
+
const maxSquaredDistance = 35215;
|
|
28
|
+
// How much of the actual picture's brightness shows in the difference picture: faded, so the red stands out.
|
|
29
|
+
const fade = 0.1;
|
|
30
|
+
const differenceColour = [255, 0, 0];
|
|
31
|
+
/**
|
|
32
|
+
* Whether two pictures are the same size and the same in every byte of every pixel.
|
|
33
|
+
*
|
|
34
|
+
* @example samePixels(first, second) // true
|
|
35
|
+
*/
|
|
36
|
+
export function samePixels(first, second) {
|
|
37
|
+
if (first.width !== second.width || first.height !== second.height || first.data.length !== second.data.length)
|
|
38
|
+
return false;
|
|
39
|
+
return Buffer.from(first.data.buffer, first.data.byteOffset, first.data.byteLength).equals(Buffer.from(second.data.buffer, second.data.byteOffset, second.data.byteLength));
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Counts the pixels of two pictures of one size whose colours are further apart than `threshold`, from 0 to 1, leaving
|
|
43
|
+
* out those that are anti-aliasing in either picture. Throws `RangeError` for pictures of different sizes or a
|
|
44
|
+
* threshold outside 0 to 1.
|
|
45
|
+
*
|
|
46
|
+
* @example comparePixels(baseline, picture, 0.2).differentPixels // 0
|
|
47
|
+
*/
|
|
48
|
+
export function comparePixels(expected, actual, threshold = defaultThreshold) {
|
|
49
|
+
checkSizes(expected, actual);
|
|
50
|
+
return scan(expected, actual, threshold, undefined);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Draws where two pictures differ, on a canvas as large as the larger of the two each way: the actual picture faded
|
|
54
|
+
* to grey, and each differing pixel in red. A pixel that only one picture covers is red. Anti-aliased pixels stay
|
|
55
|
+
* faded. The counts are those of `comparePixels` when the sizes match, and count the uncovered pixels as different
|
|
56
|
+
* when they do not.
|
|
57
|
+
*
|
|
58
|
+
* @example differencePicture(baseline, picture, 0.2).picture.width // 1280
|
|
59
|
+
*/
|
|
60
|
+
export function differencePicture(expected, actual, threshold = defaultThreshold) {
|
|
61
|
+
checkThreshold(threshold);
|
|
62
|
+
const width = Math.max(expected.width, actual.width);
|
|
63
|
+
const height = Math.max(expected.height, actual.height);
|
|
64
|
+
const data = new Uint8Array(width * height * 4);
|
|
65
|
+
const canvas = { width, height, data };
|
|
66
|
+
if (expected.width === actual.width && expected.height === actual.height) {
|
|
67
|
+
const counted = scan(expected, actual, threshold, (index, different) => (different ? paint(data, index * 4) : fadeInto(data, index * 4, actual.data, index * 4)));
|
|
68
|
+
return { ...counted, picture: canvas };
|
|
69
|
+
}
|
|
70
|
+
let differentPixels = 0;
|
|
71
|
+
for (let y = 0; y < height; y++) {
|
|
72
|
+
for (let x = 0; x < width; x++) {
|
|
73
|
+
const at = (y * width + x) * 4;
|
|
74
|
+
if (x < actual.width && y < actual.height && x < expected.width && y < expected.height)
|
|
75
|
+
fadeInto(data, at, actual.data, (y * actual.width + x) * 4);
|
|
76
|
+
else {
|
|
77
|
+
paint(data, at);
|
|
78
|
+
differentPixels++;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return { differentPixels, antiAliasedPixels: 0, picture: canvas };
|
|
83
|
+
}
|
|
84
|
+
function checkSizes(expected, actual) {
|
|
85
|
+
if (expected.width !== actual.width || expected.height !== actual.height) {
|
|
86
|
+
throw new RangeError(`Pixels compare only between pictures of one size, not ${expected.width} by ${expected.height} and ${actual.width} by ${actual.height}.`);
|
|
87
|
+
}
|
|
88
|
+
for (const image of [expected, actual])
|
|
89
|
+
if (image.data.length !== image.width * image.height * 4)
|
|
90
|
+
throw new RangeError(`${image.width} by ${image.height} RGBA pixels take ${image.width * image.height * 4} bytes, not ${image.data.length}.`);
|
|
91
|
+
}
|
|
92
|
+
function checkThreshold(threshold) {
|
|
93
|
+
if (!Number.isFinite(threshold) || threshold < 0 || threshold > 1)
|
|
94
|
+
throw new RangeError(`A threshold is a colour distance from 0 to 1, not ${threshold}.`);
|
|
95
|
+
}
|
|
96
|
+
// Visits every pixel of two pictures of one size, counting those that differ, and tells `mark` of each, different or not.
|
|
97
|
+
function scan(expected, actual, threshold, mark) {
|
|
98
|
+
checkThreshold(threshold);
|
|
99
|
+
const { width, height } = expected;
|
|
100
|
+
const first = expected.data;
|
|
101
|
+
const second = actual.data;
|
|
102
|
+
const limit = maxSquaredDistance * threshold * threshold;
|
|
103
|
+
let differentPixels = 0;
|
|
104
|
+
let antiAliasedPixels = 0;
|
|
105
|
+
for (let y = 0; y < height; y++) {
|
|
106
|
+
for (let x = 0; x < width; x++) {
|
|
107
|
+
const index = y * width + x;
|
|
108
|
+
const delta = colourDelta(first, second, index * 4, index * 4, false);
|
|
109
|
+
if (Math.abs(delta) <= limit) {
|
|
110
|
+
mark?.(index, false);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (antiAliased(first, second, x, y, width, height) || antiAliased(second, first, x, y, width, height)) {
|
|
114
|
+
antiAliasedPixels++;
|
|
115
|
+
mark?.(index, false);
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
differentPixels++;
|
|
119
|
+
mark?.(index, true);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return { differentPixels, antiAliasedPixels };
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Whether the pixel at `x`, `y` of `picture` looks like part of an anti-aliased edge: among its eight neighbours it has
|
|
126
|
+
* no more than two of its own brightness, at least one darker and one brighter, and the darkest or the brightest of
|
|
127
|
+
* them sits in a flat run of at least three equal neighbours in both pictures.
|
|
128
|
+
*/
|
|
129
|
+
function antiAliased(picture, other, x, y, width, height) {
|
|
130
|
+
const left = Math.max(x - 1, 0);
|
|
131
|
+
const top = Math.max(y - 1, 0);
|
|
132
|
+
const right = Math.min(x + 1, width - 1);
|
|
133
|
+
const bottom = Math.min(y + 1, height - 1);
|
|
134
|
+
const at = (y * width + x) * 4;
|
|
135
|
+
let equal = x === left || x === right || y === top || y === bottom ? 1 : 0;
|
|
136
|
+
let darkest = 0;
|
|
137
|
+
let brightest = 0;
|
|
138
|
+
let darkestAt;
|
|
139
|
+
let brightestAt;
|
|
140
|
+
for (let column = left; column <= right; column++) {
|
|
141
|
+
for (let row = top; row <= bottom; row++) {
|
|
142
|
+
if (column === x && row === y)
|
|
143
|
+
continue;
|
|
144
|
+
const delta = colourDelta(picture, picture, at, (row * width + column) * 4, true);
|
|
145
|
+
if (delta === 0) {
|
|
146
|
+
equal++;
|
|
147
|
+
if (equal > 2)
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
else if (delta < darkest) {
|
|
151
|
+
darkest = delta;
|
|
152
|
+
darkestAt = [column, row];
|
|
153
|
+
}
|
|
154
|
+
else if (delta > brightest) {
|
|
155
|
+
brightest = delta;
|
|
156
|
+
brightestAt = [column, row];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (darkestAt === undefined || brightestAt === undefined)
|
|
161
|
+
return false;
|
|
162
|
+
const flat = ([column, row]) => hasManySiblings(picture, column, row, width, height) && hasManySiblings(other, column, row, width, height);
|
|
163
|
+
return flat(darkestAt) || flat(brightestAt);
|
|
164
|
+
}
|
|
165
|
+
// Whether a pixel has at least three neighbours exactly like it, a pixel on the picture's edge counting the edge as one.
|
|
166
|
+
function hasManySiblings(picture, x, y, width, height) {
|
|
167
|
+
const left = Math.max(x - 1, 0);
|
|
168
|
+
const top = Math.max(y - 1, 0);
|
|
169
|
+
const right = Math.min(x + 1, width - 1);
|
|
170
|
+
const bottom = Math.min(y + 1, height - 1);
|
|
171
|
+
const at = (y * width + x) * 4;
|
|
172
|
+
let equal = x === left || x === right || y === top || y === bottom ? 1 : 0;
|
|
173
|
+
for (let column = left; column <= right; column++) {
|
|
174
|
+
for (let row = top; row <= bottom; row++) {
|
|
175
|
+
if (column === x && row === y)
|
|
176
|
+
continue;
|
|
177
|
+
const other = (row * width + column) * 4;
|
|
178
|
+
if (picture[at] === picture[other] && picture[at + 1] === picture[other + 1] && picture[at + 2] === picture[other + 2] && picture[at + 3] === picture[other + 3])
|
|
179
|
+
equal++;
|
|
180
|
+
if (equal > 2)
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
// The squared YIQ distance between two pixels, negative when the first is the brighter; with `brightnessOnly`, the
|
|
187
|
+
// difference in brightness alone, first less second.
|
|
188
|
+
function colourDelta(first, second, at, other, brightnessOnly) {
|
|
189
|
+
const r1 = first[at] ?? 0;
|
|
190
|
+
const g1 = first[at + 1] ?? 0;
|
|
191
|
+
const b1 = first[at + 2] ?? 0;
|
|
192
|
+
const a1 = first[at + 3] ?? 0;
|
|
193
|
+
const r2 = second[other] ?? 0;
|
|
194
|
+
const g2 = second[other + 1] ?? 0;
|
|
195
|
+
const b2 = second[other + 2] ?? 0;
|
|
196
|
+
const a2 = second[other + 3] ?? 0;
|
|
197
|
+
if (r1 === r2 && g1 === g2 && b1 === b2 && a1 === a2)
|
|
198
|
+
return 0;
|
|
199
|
+
const [red1, green1, blue1] = onWhite(r1, g1, b1, a1);
|
|
200
|
+
const [red2, green2, blue2] = onWhite(r2, g2, b2, a2);
|
|
201
|
+
const brightness1 = luma(red1, green1, blue1);
|
|
202
|
+
const brightness2 = luma(red2, green2, blue2);
|
|
203
|
+
const y = brightness1 - brightness2;
|
|
204
|
+
if (brightnessOnly)
|
|
205
|
+
return y;
|
|
206
|
+
const i = inPhase(red1, green1, blue1) - inPhase(red2, green2, blue2);
|
|
207
|
+
const q = quadrature(red1, green1, blue1) - quadrature(red2, green2, blue2);
|
|
208
|
+
const delta = 0.5053 * y * y + 0.299 * i * i + 0.1957 * q * q;
|
|
209
|
+
return brightness1 > brightness2 ? -delta : delta;
|
|
210
|
+
}
|
|
211
|
+
function onWhite(red, green, blue, alpha) {
|
|
212
|
+
if (alpha === 255)
|
|
213
|
+
return [red, green, blue];
|
|
214
|
+
const opacity = alpha / 255;
|
|
215
|
+
return [blend(red, opacity), blend(green, opacity), blend(blue, opacity)];
|
|
216
|
+
}
|
|
217
|
+
function blend(channel, opacity) {
|
|
218
|
+
return 255 + (channel - 255) * opacity;
|
|
219
|
+
}
|
|
220
|
+
function luma(red, green, blue) {
|
|
221
|
+
return red * 0.29889531 + green * 0.58662247 + blue * 0.11448223;
|
|
222
|
+
}
|
|
223
|
+
function inPhase(red, green, blue) {
|
|
224
|
+
return red * 0.59597799 - green * 0.2741761 - blue * 0.32180189;
|
|
225
|
+
}
|
|
226
|
+
function quadrature(red, green, blue) {
|
|
227
|
+
return red * 0.21147017 - green * 0.52261711 + blue * 0.31114694;
|
|
228
|
+
}
|
|
229
|
+
function paint(data, at) {
|
|
230
|
+
data[at] = differenceColour[0];
|
|
231
|
+
data[at + 1] = differenceColour[1];
|
|
232
|
+
data[at + 2] = differenceColour[2];
|
|
233
|
+
data[at + 3] = 255;
|
|
234
|
+
}
|
|
235
|
+
function fadeInto(data, at, source, from) {
|
|
236
|
+
const opacity = (fade * (source[from + 3] ?? 0)) / 255;
|
|
237
|
+
const grey = blend(luma(source[from] ?? 0, source[from + 1] ?? 0, source[from + 2] ?? 0), opacity);
|
|
238
|
+
data[at] = grey;
|
|
239
|
+
data[at + 1] = grey;
|
|
240
|
+
data[at + 2] = grey;
|
|
241
|
+
data[at + 3] = 255;
|
|
242
|
+
}
|
|
@@ -36,13 +36,16 @@ export declare const metadataPending: number;
|
|
|
36
36
|
export declare const metadataRunning: number;
|
|
37
37
|
export declare const metadataComplete: number;
|
|
38
38
|
export declare const metadataAbandoned: number;
|
|
39
|
+
/** The worker was ended for another query before it answered this one, which is asked again of a fresh worker. */
|
|
40
|
+
export declare const metadataWorkerEnded: number;
|
|
39
41
|
export declare const metadataSuccess: number;
|
|
40
42
|
export declare const metadataFailure: number;
|
|
41
43
|
/**
|
|
42
44
|
* A synchronous bridge for fresh ownership readings, including exit hooks. The worker uses an asynchronous child
|
|
43
45
|
* and bounds its output and time itself, so Node's synchronous-child timeout never kills an unrecorded process.
|
|
44
46
|
* A query the worker failed to answer fails alone: that worker is ended and the next query starts a fresh one, so one
|
|
45
|
-
* stall under load never leaves a long-lived host without readings, and so without the right to signal anything.
|
|
47
|
+
* stall under load never leaves a long-lived host without readings, and so without the right to signal anything. A
|
|
48
|
+
* query the ended worker still held is asked again of the fresh one, within its own caller's deadline.
|
|
46
49
|
*/
|
|
47
50
|
export declare function readMetadataProcess(options: MetadataProcessOptions): string;
|
|
48
51
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metadata-process.d.ts","sourceRoot":"","sources":["../../src/shared/metadata-process.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAIvD,sFAAsF;AACtF,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACtD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,sGAAsG;IACtG,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAA;IACxC;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAC9B,CAAA;AAED,yFAAyF;AACzF,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACtD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,sGAAsG;IACtG,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAA;IACnC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAA;CACnC,CAAA;AAED,eAAO,MAAM,mBAAmB,EAAE,MAAwB,CAAA;AAC1D;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,EAAE,MAAyB,CAAA;AAC/D,eAAO,MAAM,eAAe,EAAE,MAAU,CAAA;AACxC,eAAO,MAAM,eAAe,EAAE,MAAU,CAAA;AACxC,eAAO,MAAM,gBAAgB,EAAE,MAAU,CAAA;AACzC,eAAO,MAAM,iBAAiB,EAAE,MAAU,CAAA;AAC1C,eAAO,MAAM,eAAe,EAAE,MAAU,CAAA;AACxC,eAAO,MAAM,eAAe,EAAE,MAAU,CAAA;
|
|
1
|
+
{"version":3,"file":"metadata-process.d.ts","sourceRoot":"","sources":["../../src/shared/metadata-process.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,yBAAyB,CAAA;AAIvD,sFAAsF;AACtF,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACtD,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAA;IAC3B,sGAAsG;IACtG,QAAQ,CAAC,QAAQ,CAAC,EAAE,QAAQ,GAAG,SAAS,CAAA;IACxC;;;OAGG;IACH,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAC9B,CAAA;AAED,yFAAyF;AACzF,MAAM,MAAM,sBAAsB,GAAG;IACnC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAA;IACxB,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAA;IAChC,QAAQ,CAAC,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;IACtD,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAA;IAC1B,sGAAsG;IACtG,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;IAC1B,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAA;IACnC,QAAQ,CAAC,MAAM,EAAE,iBAAiB,CAAA;CACnC,CAAA;AAED,eAAO,MAAM,mBAAmB,EAAE,MAAwB,CAAA;AAC1D;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,EAAE,MAAyB,CAAA;AAC/D,eAAO,MAAM,eAAe,EAAE,MAAU,CAAA;AACxC,eAAO,MAAM,eAAe,EAAE,MAAU,CAAA;AACxC,eAAO,MAAM,gBAAgB,EAAE,MAAU,CAAA;AACzC,eAAO,MAAM,iBAAiB,EAAE,MAAU,CAAA;AAC1C,kHAAkH;AAClH,eAAO,MAAM,mBAAmB,EAAE,MAAU,CAAA;AAC5C,eAAO,MAAM,eAAe,EAAE,MAAU,CAAA;AACxC,eAAO,MAAM,eAAe,EAAE,MAAU,CAAA;AAaxC;;;;;;GAMG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,sBAAsB,GAAG,MAAM,CAsB3E;AAED;;;;;;GAMG;AACH,wBAAsB,wBAAwB,CAAC,OAAO,EAAE,sBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,CA8B/F;AAuFD;;;;;GAKG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,GAAG,GAAG,MAAM,IAAI,CAQ7D"}
|
|
@@ -12,6 +12,8 @@ export const metadataPending = 0;
|
|
|
12
12
|
export const metadataRunning = 1;
|
|
13
13
|
export const metadataComplete = 2;
|
|
14
14
|
export const metadataAbandoned = 3;
|
|
15
|
+
/** The worker was ended for another query before it answered this one, which is asked again of a fresh worker. */
|
|
16
|
+
export const metadataWorkerEnded = 4;
|
|
15
17
|
export const metadataSuccess = 0;
|
|
16
18
|
export const metadataFailure = 1;
|
|
17
19
|
const queryLimitMs = 5000;
|
|
@@ -19,22 +21,31 @@ const workerAllowanceMs = 1000;
|
|
|
19
21
|
// One spare of each production capacity. Only a completed query can return its buffer: an abandoned worker may
|
|
20
22
|
// still be writing, and a concurrent query must hold a different buffer until its completion signal is consumed.
|
|
21
23
|
const spareOutputs = new Map();
|
|
24
|
+
// Queries posted and not yet settled. A worker ended for one query fails the others it holds at once, rather than
|
|
25
|
+
// leaving each to wait out its whole bound for an answer that cannot come.
|
|
26
|
+
const inFlight = new Set();
|
|
22
27
|
let bridge;
|
|
23
28
|
let workerModule = new URL(import.meta.url.endsWith('.ts') ? './metadata-process-worker.ts' : './metadata-process-worker.js', import.meta.url);
|
|
24
29
|
/**
|
|
25
30
|
* A synchronous bridge for fresh ownership readings, including exit hooks. The worker uses an asynchronous child
|
|
26
31
|
* and bounds its output and time itself, so Node's synchronous-child timeout never kills an unrecorded process.
|
|
27
32
|
* A query the worker failed to answer fails alone: that worker is ended and the next query starts a fresh one, so one
|
|
28
|
-
* stall under load never leaves a long-lived host without readings, and so without the right to signal anything.
|
|
33
|
+
* stall under load never leaves a long-lived host without readings, and so without the right to signal anything. A
|
|
34
|
+
* query the ended worker still held is asked again of the fresh one, within its own caller's deadline.
|
|
29
35
|
*/
|
|
30
36
|
export function readMetadataProcess(options) {
|
|
31
|
-
|
|
37
|
+
let query = submit(options);
|
|
32
38
|
try {
|
|
33
39
|
for (;;) {
|
|
34
40
|
checkCaller(query, options.deadline);
|
|
35
41
|
const state = Atomics.load(query.control, 0);
|
|
36
42
|
if (state === metadataComplete)
|
|
37
43
|
return answer(query);
|
|
44
|
+
if (state === metadataWorkerEnded) {
|
|
45
|
+
settle(query);
|
|
46
|
+
query = submit(options);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
38
49
|
const remaining = query.deadline - performance.now();
|
|
39
50
|
if (remaining <= 0) {
|
|
40
51
|
if (abandoned(query))
|
|
@@ -45,7 +56,7 @@ export function readMetadataProcess(options) {
|
|
|
45
56
|
}
|
|
46
57
|
}
|
|
47
58
|
finally {
|
|
48
|
-
|
|
59
|
+
settle(query);
|
|
49
60
|
}
|
|
50
61
|
}
|
|
51
62
|
/**
|
|
@@ -56,7 +67,7 @@ export function readMetadataProcess(options) {
|
|
|
56
67
|
* @example const text = await readMetadataProcessAsync({ command: '/bin/ps', args: ['-axo', 'pid='], environment: { PATH: '/usr/bin:/bin' } })
|
|
57
68
|
*/
|
|
58
69
|
export async function readMetadataProcessAsync(options) {
|
|
59
|
-
|
|
70
|
+
let query = submit(options);
|
|
60
71
|
const wake = () => { Atomics.notify(query.control, 0); };
|
|
61
72
|
// Neither the unreferenced worker nor an Atomics wait keeps Node alive. Keep this bounded query awaited even when
|
|
62
73
|
// it is the host's last work, including when a failed worker can no longer notify it.
|
|
@@ -68,6 +79,11 @@ export async function readMetadataProcessAsync(options) {
|
|
|
68
79
|
const state = Atomics.load(query.control, 0);
|
|
69
80
|
if (state === metadataComplete)
|
|
70
81
|
return answer(query);
|
|
82
|
+
if (state === metadataWorkerEnded) {
|
|
83
|
+
settle(query);
|
|
84
|
+
query = submit(options);
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
71
87
|
const remaining = query.deadline - performance.now();
|
|
72
88
|
if (remaining <= 0) {
|
|
73
89
|
if (abandoned(query))
|
|
@@ -80,7 +96,7 @@ export async function readMetadataProcessAsync(options) {
|
|
|
80
96
|
}
|
|
81
97
|
}
|
|
82
98
|
finally {
|
|
83
|
-
|
|
99
|
+
settle(query);
|
|
84
100
|
clearInterval(pending);
|
|
85
101
|
options.deadline?.signal?.removeEventListener('abort', wake);
|
|
86
102
|
}
|
|
@@ -118,7 +134,25 @@ function submit(options) {
|
|
|
118
134
|
discardWorker(worker);
|
|
119
135
|
throw new Error('The metadata worker could not receive the query.');
|
|
120
136
|
}
|
|
121
|
-
|
|
137
|
+
const query = { worker, control, output, deadline: Math.min(performance.now() + timeoutMs + workerAllowanceMs, request.deadline === undefined ? Infinity : request.deadline - performance.timeOrigin) };
|
|
138
|
+
inFlight.add(query);
|
|
139
|
+
return query;
|
|
140
|
+
}
|
|
141
|
+
function settle(query) {
|
|
142
|
+
inFlight.delete(query);
|
|
143
|
+
releaseOutput(query);
|
|
144
|
+
}
|
|
145
|
+
// Fails a query its worker can no longer answer, unless the answer came first, and wakes whoever waits on it.
|
|
146
|
+
function endedWorker(query) {
|
|
147
|
+
for (;;) {
|
|
148
|
+
const latest = Atomics.load(query.control, 0);
|
|
149
|
+
if (latest === metadataComplete || latest === metadataAbandoned || latest === metadataWorkerEnded)
|
|
150
|
+
return;
|
|
151
|
+
if (Atomics.compareExchange(query.control, 0, latest, metadataWorkerEnded) !== latest)
|
|
152
|
+
continue;
|
|
153
|
+
Atomics.notify(query.control, 0);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
122
156
|
}
|
|
123
157
|
// A reply completing at the deadline may be consumed; an abandoned queued request must never start later. True once the
|
|
124
158
|
// query is abandoned and its worker ended, false when the reply came first.
|
|
@@ -189,16 +223,24 @@ function metadataWorker() {
|
|
|
189
223
|
throw new Error('The metadata worker could not start.');
|
|
190
224
|
}
|
|
191
225
|
worker.on('error', () => { discardWorker(worker); });
|
|
192
|
-
worker.on('exit', () => {
|
|
193
|
-
bridge
|
|
226
|
+
worker.on('exit', () => {
|
|
227
|
+
if (bridge === worker)
|
|
228
|
+
bridge = undefined;
|
|
229
|
+
for (const query of inFlight)
|
|
230
|
+
if (query.worker === worker)
|
|
231
|
+
endedWorker(query);
|
|
232
|
+
});
|
|
194
233
|
worker.unref();
|
|
195
234
|
bridge = worker;
|
|
196
235
|
return worker;
|
|
197
236
|
}
|
|
198
|
-
// The query that met the failure reports it; the next one starts a fresh worker.
|
|
237
|
+
// The query that met the failure reports it; the next one starts a fresh worker, and the others it held are asked again.
|
|
199
238
|
function discardWorker(worker) {
|
|
200
239
|
if (bridge === worker)
|
|
201
240
|
bridge = undefined;
|
|
241
|
+
for (const query of inFlight)
|
|
242
|
+
if (query.worker === worker)
|
|
243
|
+
endedWorker(query);
|
|
202
244
|
// Ending a failed worker thread closes its pipes; its detached child is never sent a process signal.
|
|
203
245
|
void worker.terminate().catch(() => { });
|
|
204
246
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** A picture as Retest compares it: `width` by `height` pixels, four bytes each, red, green, blue and alpha, row by row. */
|
|
2
|
+
export type RgbaImage = {
|
|
3
|
+
readonly width: number;
|
|
4
|
+
readonly height: number;
|
|
5
|
+
readonly data: Uint8Array;
|
|
6
|
+
};
|
|
7
|
+
/** A PNG as `readPng` reads it: its pixels as RGBA, and the text of its `tEXt` chunks by keyword. */
|
|
8
|
+
export type PngPicture = RgbaImage & {
|
|
9
|
+
readonly text: Readonly<Record<string, string>>;
|
|
10
|
+
};
|
|
11
|
+
/** What `writePng` adds to the pixels: `tEXt` chunks, by keyword, each keyword 1 to 79 Latin-1 letters. */
|
|
12
|
+
export type PngWriteOptions = {
|
|
13
|
+
readonly text?: Readonly<Record<string, string>>;
|
|
14
|
+
};
|
|
15
|
+
/** Thrown for bytes that are not a PNG Retest reads. The message says what the bytes were. */
|
|
16
|
+
export declare class PngError extends Error {
|
|
17
|
+
readonly name = "PngError";
|
|
18
|
+
}
|
|
19
|
+
/** The most bytes a decoded picture may take, filter bytes included, so a damaged file cannot ask for all memory. */
|
|
20
|
+
export declare const maxPictureBytes: number;
|
|
21
|
+
/**
|
|
22
|
+
* Decodes a PNG into RGBA pixels: 8-bit RGB or RGBA, not interlaced, its rows filtered with any of the five filters.
|
|
23
|
+
* Every chunk's checksum is checked. RGB pixels get an alpha of 255. Throws `PngError` for anything else.
|
|
24
|
+
*
|
|
25
|
+
* @example readPng(bytes).width // 1280
|
|
26
|
+
*/
|
|
27
|
+
export declare function readPng(bytes: Uint8Array): PngPicture;
|
|
28
|
+
/**
|
|
29
|
+
* Encodes RGBA pixels as a PNG: RGB when every pixel is opaque, RGBA otherwise, each row with the filter that leaves the
|
|
30
|
+
* smallest sum, as libpng chooses, and any `tEXt` chunks the options name.
|
|
31
|
+
*
|
|
32
|
+
* @example writePng({ width: 1, height: 1, data: Uint8Array.of(255, 0, 0, 255) }, { text: { 'retest:scale': '2' } })
|
|
33
|
+
*/
|
|
34
|
+
export declare function writePng(image: RgbaImage, options?: PngWriteOptions): Uint8Array;
|
|
35
|
+
//# sourceMappingURL=png.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"png.d.ts","sourceRoot":"","sources":["../../src/shared/png.ts"],"names":[],"mappings":"AAOA,4HAA4H;AAC5H,MAAM,MAAM,SAAS,GAAG;IAAE,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAA;CAAE,CAAA;AAEtG,qGAAqG;AACrG,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG;IAAE,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CAAE,CAAA;AAExF,2GAA2G;AAC3G,MAAM,MAAM,eAAe,GAAG;IAAE,QAAQ,CAAC,IAAI,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAA;CAAE,CAAA;AAElF,8FAA8F;AAC9F,qBAAa,QAAS,SAAQ,KAAK;IACjC,SAAkB,IAAI,cAAa;CACpC;AAED,qHAAqH;AACrH,eAAO,MAAM,eAAe,EAAE,MAA0B,CAAA;AAQxD;;;;;GAKG;AACH,wBAAgB,OAAO,CAAC,KAAK,EAAE,UAAU,GAAG,UAAU,CA0CrD;AAED;;;;;GAKG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,SAAS,EAAE,OAAO,GAAE,eAAoB,GAAG,UAAU,CAepF"}
|