xref: /aosp_15_r20/external/skia/modules/canvaskit/tests/util.js (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
1// The size of the golden images (DMs)
2const CANVAS_WIDTH = 600;
3const CANVAS_HEIGHT = 600;
4
5const SHOULD_SKIP = 'should_skip';
6
7const _commonGM = (it, pause, name, callback, assetsToFetchOrPromisesToWaitOn) => {
8    if (name.includes(' ')) {
9        throw name + " cannot contain spaces";
10    }
11    const fetchPromises = [];
12    for (const assetOrPromise of assetsToFetchOrPromisesToWaitOn) {
13        // https://stackoverflow.com/a/9436948
14        if (typeof assetOrPromise === 'string' || assetOrPromise instanceof String) {
15            const newPromise = fetchWithRetries(assetOrPromise)
16                .then((response) => response.arrayBuffer())
17                .catch((err) => {
18                    console.error(err);
19                    throw err;
20                });
21            fetchPromises.push(newPromise);
22        } else if (typeof assetOrPromise.then === 'function') {
23            fetchPromises.push(assetOrPromise);
24        } else {
25            throw 'Neither a string nor a promise ' + assetOrPromise;
26        }
27    }
28    it('draws gm '+name, (done) => {
29        const surface = CanvasKit.MakeCanvasSurface('test');
30        expect(surface).toBeTruthy('Could not make surface');
31        if (!surface) {
32            done();
33            return;
34        }
35        // if fetchPromises is empty, the returned promise will
36        // resolve right away and just call the callback.
37        Promise.all(fetchPromises).then((values) => {
38            try {
39                // If callback returns a promise, the chained .then
40                // will wait for it. Otherwise, we'll pass the return value on,
41                // which could indicate to skip this test and not report it to Gold.
42                surface.getCanvas().clear(CanvasKit.WHITE);
43                return callback(surface.getCanvas(), values, surface);
44            } catch (e) {
45                console.log(`gm ${name} failed with error`, e);
46                expect(e).toBeFalsy();
47                debugger;
48                done();
49            }
50        }).then((shouldSkip) => {
51            surface.flush();
52            if (shouldSkip === SHOULD_SKIP) {
53                surface.delete();
54                done();
55                console.log(`skipped gm ${name}`);
56                return;
57            }
58            if (pause) {
59                reportSurface(surface, name, null);
60                console.error('pausing due to pause_gm being invoked');
61            } else {
62                reportSurface(surface, name, done);
63            }
64        }).catch((e) => {
65            console.log(`could not load assets for gm ${name}`, e);
66            debugger;
67            done();
68        });
69    })
70};
71
72const fetchWithRetries = (url) => {
73    const MAX_ATTEMPTS = 3;
74    const DELAY_AFTER_FAILURE = 1000;
75
76    return new Promise((resolve, reject) => {
77        let attempts = 0;
78        const attemptFetch = () => {
79            attempts++;
80            fetch(url).then((resp) => resolve(resp))
81                .catch((err) => {
82                    if (attempts < MAX_ATTEMPTS) {
83                        console.warn(`got error in fetching ${url}, retrying`, err);
84                        retryAfterDelay();
85                    } else {
86                        console.error(`got error in fetching ${url} even after ${attempts} attempts`, err);
87                        reject(err);
88                    }
89                });
90        };
91        const retryAfterDelay = () => {
92            setTimeout(() => {
93                attemptFetch();
94            }, DELAY_AFTER_FAILURE);
95        }
96        attemptFetch();
97    });
98
99}
100
101/**
102 * Takes a name, a callback, and any number of assets or promises. It executes the
103 * callback (presumably, the test) and reports the resulting surface to Gold.
104 * @param name {string}
105 * @param callback {Function}, has two params, the first is a CanvasKit.Canvas
106 *    and the second is an array of results from the passed in assets or promises.
107 *    If a given assetOrPromise was a string, the result will be an ArrayBuffer.
108 * @param assetsToFetchOrPromisesToWaitOn {string|Promise}. If a string, it will
109 *    be treated as a url to fetch and return an ArrayBuffer with the contents as
110 *    a result in the callback. Otherwise, the promise will be waited on and its
111 *    result will be whatever the promise resolves to.
112 */
113const gm = (name, callback, ...assetsToFetchOrPromisesToWaitOn) => {
114    _commonGM(it, false, name, callback, assetsToFetchOrPromisesToWaitOn);
115};
116
117/**
118 *  fgm is like gm, except only tests declared with fgm, force_gm, or fit will be
119 *  executed. This mimics the behavior of Jasmine.js.
120 */
121const fgm = (name, callback, ...assetsToFetchOrPromisesToWaitOn) => {
122    _commonGM(fit, false, name, callback, assetsToFetchOrPromisesToWaitOn);
123};
124
125/**
126 *  force_gm is like gm, except only tests declared with fgm, force_gm, or fit will be
127 *  executed. This mimics the behavior of Jasmine.js.
128 */
129const force_gm = (name, callback, ...assetsToFetchOrPromisesToWaitOn) => {
130    fgm(name, callback, assetsToFetchOrPromisesToWaitOn);
131};
132
133/**
134 *  skip_gm does nothing. It is a convenient way to skip a test temporarily.
135 */
136const skip_gm = (name, callback, ...assetsToFetchOrPromisesToWaitOn) => {
137    console.log(`Skipping gm ${name}`);
138    // do nothing, skip the test for now
139};
140
141/**
142 *  pause_gm is like fgm, except the test will not finish right away and clear,
143 *  making it ideal for a human to manually inspect the results.
144 */
145const pause_gm = (name, callback, ...assetsToFetchOrPromisesToWaitOn) => {
146    _commonGM(fit, true, name, callback, assetsToFetchOrPromisesToWaitOn);
147};
148
149const _commonMultipleCanvasGM = (it, pause, name, callback) => {
150    if (name.includes(' ')) {
151        throw name + " cannot contain spaces";
152    }
153    it(`draws gm ${name} on both CanvasKit and using Canvas2D`, (done) => {
154        const skcanvas = CanvasKit.MakeCanvas(CANVAS_WIDTH, CANVAS_HEIGHT);
155        skcanvas._config = 'software_canvas';
156        const realCanvas = document.getElementById('test');
157        realCanvas._config = 'html_canvas';
158        realCanvas.width = CANVAS_WIDTH;
159        realCanvas.height = CANVAS_HEIGHT;
160
161        if (pause) {
162            console.log('debugging canvaskit version');
163            callback(realCanvas);
164            callback(skcanvas);
165            const png = skcanvas.toDataURL();
166            const img = document.createElement('img');
167            document.body.appendChild(img);
168            img.src = png;
169            debugger;
170            return;
171        }
172
173        const promises = [];
174
175        for (const canvas of [skcanvas, realCanvas]) {
176            callback(canvas);
177            // canvas has .toDataURL (even though skcanvas is not a real Canvas)
178            // so this will work.
179            promises.push(reportCanvas(canvas, name, canvas._config));
180        }
181        Promise.all(promises).then(() => {
182            skcanvas.dispose();
183            done();
184        }).catch(reportError(done));
185    });
186};
187
188/**
189 * Takes a name and a callback. It executes the callback (presumably, the test)
190 * for both a CanvasKit.Canvas and a native Canvas2D. The result of both will be
191 * uploaded to Gold.
192 * @param name {string}
193 * @param callback {Function}, has one param, either a CanvasKit.Canvas or a native
194 *    Canvas2D object.
195 */
196const multipleCanvasGM = (name, callback) => {
197    _commonMultipleCanvasGM(it, false, name, callback);
198};
199
200/**
201 *  fmultipleCanvasGM is like multipleCanvasGM, except only tests declared with
202 *  fmultipleCanvasGM, force_multipleCanvasGM, or fit will be executed. This
203 *  mimics the behavior of Jasmine.js.
204 */
205const fmultipleCanvasGM = (name, callback) => {
206    _commonMultipleCanvasGM(fit, false, name, callback);
207};
208
209/**
210 *  force_multipleCanvasGM is like multipleCanvasGM, except only tests declared
211 *  with fmultipleCanvasGM, force_multipleCanvasGM, or fit will be executed. This
212 *  mimics the behavior of Jasmine.js.
213 */
214const force_multipleCanvasGM = (name, callback) => {
215    fmultipleCanvasGM(name, callback);
216};
217
218/**
219 *  pause_multipleCanvasGM is like fmultipleCanvasGM, except the test will not
220 *  finish right away and clear, making it ideal for a human to manually inspect the results.
221 */
222const pause_multipleCanvasGM = (name, callback) => {
223    _commonMultipleCanvasGM(fit, true, name, callback);
224};
225
226/**
227 *  skip_multipleCanvasGM does nothing. It is a convenient way to skip a test temporarily.
228 */
229const skip_multipleCanvasGM = (name, callback) => {
230    console.log(`Skipping multiple canvas gm ${name}`);
231};
232
233
234function reportSurface(surface, testname, done) {
235    // Sometimes, the webgl canvas is blank, but the surface has the pixel
236    // data. So, we copy it out and draw it to a normal canvas to take a picture.
237    // To be consistent across CPU and GPU, we just do it for all configurations
238    // (even though the CPU canvas shows up after flush just fine).
239    let pixels = surface.getCanvas().readPixels(0, 0, {
240        width: CANVAS_WIDTH,
241        height: CANVAS_HEIGHT,
242        colorType: CanvasKit.ColorType.RGBA_8888,
243        alphaType: CanvasKit.AlphaType.Unpremul,
244        colorSpace: CanvasKit.ColorSpace.SRGB,
245    });
246    if (!pixels) {
247        throw 'Could not get pixels for test '+testname;
248    }
249    pixels = new Uint8ClampedArray(pixels.buffer);
250    const imageData = new ImageData(pixels, CANVAS_WIDTH, CANVAS_HEIGHT);
251
252    const reportingCanvas = document.getElementById('report');
253    if (!reportingCanvas) {
254        throw 'Reporting canvas not found';
255    }
256    reportingCanvas.getContext('2d').putImageData(imageData, 0, 0);
257    if (!done) {
258        return;
259    }
260    reportCanvas(reportingCanvas, testname).then(() => {
261        surface.delete();
262        done();
263    }).catch(reportError(done));
264}
265
266
267function starPath(CanvasKit, X=128, Y=128, R=116) {
268    const p = new CanvasKit.Path();
269    p.moveTo(X + R, Y);
270    for (let i = 1; i < 8; i++) {
271      let a = 2.6927937 * i;
272      p.lineTo(X + R * Math.cos(a), Y + R * Math.sin(a));
273    }
274    p.close();
275    return p;
276}
277