Bug 1881621 - Add colors/color_canvas.html tests to dom/canvas/test/reftest. r=bradwerth
[gecko.git] / dom / canvas / test / reftest / colors / generate_color_canvas_reftests.py
blob8c1e5f378844caf1807c3e03bf68c483cff64f65
1 #! python3
3 # Typecheck:
4 # `pip -m mypy generate_color_canvas_reftests.py`
6 # Run:
7 # `./generate_color_canvas_reftests.py [--write]`
9 import functools
10 import json
11 import math
12 import pathlib
13 import re
14 import sys
15 from typing import Iterable, NamedTuple, TypeVar
17 ARGS = sys.argv[1:]
18 DEST = pathlib.Path(__file__).parent / "_generated_reftest.list"
20 COL_DELIM = " "
21 COL_ALIGNMENT = 4
23 # -
25 T = TypeVar("T")
26 U = TypeVar("U")
28 # -
31 # crossCombine([{a:false},{a:5}], [{},{b:5}])
32 # [{a:false}, {a:true}, {a:false,b:5}, {a:true,b:5}]
33 def cross_combine(*args_tup: list[dict]) -> list[dict]:
34 args = list(args_tup)
35 for i, a in enumerate(args):
36 assert type(a) == list, f"Arg{i} is {type(a)}, expected {list}."
38 def cross_combine2(listA, listB):
39 listC = []
40 for a in listA:
41 for b in listB:
42 c = dict()
43 c.update(a)
44 c.update(b)
45 listC.append(c)
46 return listC
48 res: list[dict] = [dict()]
49 while True:
50 try:
51 next = args.pop(0)
52 except IndexError:
53 break
54 res = cross_combine2(res, next)
55 return res
58 # keyed_alternatives('count', [1,2,3]) -> [{count: 1}, {count: 2}, {count: 3}]
59 def keyed_alternatives(key: T, vals: Iterable[U]) -> list[dict[T, U]]:
60 """
61 res = []
62 for v in vals:
63 d = dict()
64 d[key] = v
65 res.append(d)
66 return res
67 """
68 # return [dict([[key,v]]) for v in vals]
69 return [{key: v} for v in vals]
72 # -
75 def eprint(*args, **kwargs):
76 sys.stdout.flush()
77 print(*args, file=sys.stderr, **kwargs)
78 sys.stderr.flush()
81 # -
83 # color_canvas.html?e_width=100&e_height=100&e_context=css&e_options={}&e_cspace=&e_webgl_format=&e_color=rgb(127,0,0)
85 CSPACE_LIST = ["srgb", "display-p3"]
86 CANVAS_CSPACES = keyed_alternatives("e_cspace", CSPACE_LIST)
88 RGB_LIST = [
89 "0.000 0.000 0.000",
90 "0.200 0.200 0.200", # 0.2*255 = 51
91 "0.200 0.000 0.000",
92 "0.000 0.200 0.000",
93 "0.000 0.000 0.200",
94 "0.502 0.502 0.502", # 0.502*255 = 128.01
95 "0.502 0.000 0.000",
96 "0.000 0.502 0.000",
97 "0.000 0.000 0.502",
98 "1.000 1.000 1.000",
99 #'1.000 0.000 0.000', # These will hit gamut clipping on most displays.
100 #'0.000 1.000 0.000',
101 #'0.000 0.000 1.000',
104 WEBGL_COLORS = keyed_alternatives("e_color", [f"color(srgb {rgb})" for rgb in RGB_LIST])
106 C2D_COLORS = []
107 for cspace in CSPACE_LIST:
108 C2D_COLORS += keyed_alternatives(
109 "e_color", [f"color({cspace} {rgb})" for rgb in RGB_LIST]
114 WEBGL_FORMATS = keyed_alternatives(
115 "e_webgl_format",
117 "RGBA8",
118 # Bug 1883748: (webgl.drawingbufferStorage)
119 #'SRGB8_ALPHA8',
120 #'RGBA16F',
123 WEBGL = cross_combine(
124 [{"e_context": "webgl"}], WEBGL_FORMATS, CANVAS_CSPACES, WEBGL_COLORS
129 C2D_OPTIONS_COMBOS = cross_combine(
130 keyed_alternatives("willReadFrequently", ["true", "false"]), # E.g. D2D vs Skia
131 # keyed_alternatives('alpha', ['true','false'])
133 C2D_OPTIONS = [
134 json.dumps(config, separators=(",", ":")) for config in C2D_OPTIONS_COMBOS
137 C2D = cross_combine(
138 [{"e_context": "2d"}],
139 keyed_alternatives("e_options", C2D_OPTIONS),
140 CANVAS_CSPACES,
141 C2D_COLORS,
146 COMBOS: list[dict[str, str]] = cross_combine(WEBGL + C2D)
148 eprint(f"{len(COMBOS)} combinations...")
152 Config = dict[str, str]
155 class CssColor(NamedTuple):
156 cspace: str
157 rgb: str
159 def rgb_vals(self) -> tuple[float, float, float]:
160 (r, g, b) = [float(z) for z in self.rgb.split(" ")]
161 return (r, g, b)
163 def is_same_color(x, y) -> bool:
164 if x == y:
165 return True
166 (r, g, b) = x.rgb_vals()
167 if x.rgb == y.rgb and r == g and g == b:
168 return True
169 return False
172 class Reftest(NamedTuple):
173 notes: list[str]
174 op: str
175 test_config: Config
176 ref_config: Config
179 def make_ref_config(color: CssColor) -> Config:
180 return {
181 "e_context": "css",
182 "e_color": f"color({color.cspace} {color.rgb})",
186 class ColorReftest(NamedTuple):
187 notes: list[str]
188 test_config: Config
189 ref_color: CssColor
191 def to_reftest(self):
192 ref_config = make_ref_config(self.ref_color)
193 return Reftest(self.notes.copy(), "==", self.test_config.copy(), ref_config)
196 class Expectation(NamedTuple):
197 notes: list[str]
198 test_config: Config
199 color: CssColor
202 def parse_css_color(s: str) -> CssColor:
203 m = re.match("color[(]([^)]+)[)]", s)
204 assert m, s
205 (cspace, rgb) = m.group(1).split(" ", 1)
206 return CssColor(cspace, rgb)
209 def correct_color_from_test_config(test_config: Config) -> CssColor:
210 canvas_cspace = test_config["e_cspace"]
211 if not canvas_cspace:
212 canvas_cspace = "srgb"
214 correct_color = parse_css_color(test_config["e_color"])
215 if test_config["e_context"] == "webgl":
216 # Webgl ignores the color's cspace, because webgl has no concept of
217 # source colorspace for clears/draws to the backbuffer.
218 # This (correct) behavior is as if the color's cspace were overwritten by the
219 # cspace of the canvas. (so expect that)
220 correct_color = CssColor(canvas_cspace, correct_color.rgb)
222 return correct_color
225 # -------------------------------------
226 # -------------------------------------
227 # -------------------------------------
228 # Choose (multiple?) reference configs given a test config.
231 def reftests_from_config(test_config: Config) -> Iterable[ColorReftest]:
232 correct_color = correct_color_from_test_config(test_config)
234 if test_config["e_context"] == "2d":
235 # Canvas2d generally has the same behavior as css, so expect all passing.
236 yield ColorReftest([], test_config, correct_color)
237 return
239 assert test_config["e_context"] == "webgl", test_config["e_context"]
243 def reftests_from_expected_color(
244 notes: list[str], expected_color: CssColor
245 ) -> Iterable[ColorReftest]:
246 # If expecting failure, generate two tests, both expecting failure:
247 # 1. expected-fail test == correct_color
248 # 2. expected-pass test == (incorrect) expected_color
249 # If we fix an error, we'll see one unexpected-pass and one unexpected-fail.
250 # If we get a new wrong answer, we'll see one unexpected-fail.
252 if not expected_color.is_same_color(correct_color):
253 yield ColorReftest(notes + ["fails"], test_config, correct_color)
254 yield ColorReftest(notes, test_config, expected_color)
255 else:
256 yield ColorReftest(notes, test_config, correct_color)
259 # On Mac, (with the pref) we do tag the IOSurface with the right cspace.
260 # On other platforms, Webgl always outputs in the display color profile
261 # right now. This is the same as "srgb".
263 expected_color_srgb = CssColor("srgb", correct_color.rgb)
264 # Mac
265 yield from reftests_from_expected_color(["skip-if(!cocoaWidget)"], correct_color)
266 # Win, Lin, Android
267 yield from reftests_from_expected_color(
268 ["skip-if(cocoaWidget) "], expected_color_srgb
275 def amended_notes_from_reftest(reftest: ColorReftest) -> list[str]:
276 notes = reftest.notes[:]
278 ref_rgb_vals = reftest.ref_color.rgb_vals()
279 is_green_only = ref_rgb_vals == (0, ref_rgb_vals[1], 0)
280 if (
281 "fails" in reftest.notes
282 and reftest.test_config["e_context"] == "webgl"
283 and reftest.test_config["e_cspace"] == "display-p3"
284 and is_green_only
286 # Android's display bitdepth rounds srgb green and p3 green to the same thing.
287 notes[notes.index("fails")] = "fails-if(!Android)"
289 return notes
292 # -------------------------------------
293 # -------------------------------------
294 # -------------------------------------
295 # Ok, back to implementation.
298 def encode_url_v(k, v):
299 if k == "e_color":
300 # reftest harness can't deal with spaces in urls, and 'color(srgb%201%200%200)' is hard to read.
301 v = v.replace(" ", ",")
303 assert " " not in v, (k, v)
304 return v
307 # Cool:
308 assert encode_url_v("e_color", "color(srgb 0 0 0)") == "color(srgb,0,0,0)"
309 # Unfortunate, but tolerable:
310 assert encode_url_v("e_color", "color(srgb 0 0 0)") == "color(srgb,,,0,,,0,,,0)"
315 def url_from_config(kvs: Config) -> str:
316 parts = [f"{k}={encode_url_v(k,v)}" for k, v in kvs.items()]
317 url = "color_canvas.html?" + "&".join(parts)
318 return url
323 color_reftests: list[ColorReftest] = []
324 for c in COMBOS:
325 color_reftests += reftests_from_config(c)
326 color_reftests = [
327 ColorReftest(amended_notes_from_reftest(r), r.test_config, r.ref_color)
328 for r in color_reftests
330 reftests = [r.to_reftest() for r in color_reftests]
334 HEADINGS = ["# annotations", "op", "test", "reference"]
335 table: list[list[str]] = [HEADINGS]
336 table = [
338 " ".join(r.notes),
339 r.op,
340 url_from_config(r.test_config),
341 url_from_config(r.ref_config),
343 for r in reftests
349 def round_to(a, b: int) -> int:
350 return int(math.ceil(a / b) * b)
353 def aligned_lines_from_table(
354 rows: list[list[str]], col_delim=" ", col_alignment=4
355 ) -> Iterable[str]:
356 max_col_len = functools.reduce(
357 lambda accum, input: [max(r, len(c)) for r, c in zip(accum, input)],
358 rows,
359 [0 for _ in rows[0]],
361 max_col_len = [round_to(x, col_alignment) for x in max_col_len]
363 for i, row in enumerate(rows):
364 parts = [s + (" " * (col_len - len(s))) for s, col_len in zip(row, max_col_len)]
365 line = col_delim.join(parts)
366 yield line
371 GENERATED_FILE_LINE = "### Generated, do not edit. ###"
373 lines = list(aligned_lines_from_table(table, COL_DELIM, COL_ALIGNMENT))
374 WARN_EVERY_N_LINES = 5
375 i = WARN_EVERY_N_LINES - 1
376 while i < len(lines):
377 lines.insert(i, " " + GENERATED_FILE_LINE)
378 i += WARN_EVERY_N_LINES
382 GENERATED_BY_ARGS = [f"./{pathlib.Path(__file__).name}"] + ARGS
384 REFTEST_LIST_PREAMBLE = f"""\
385 {GENERATED_FILE_LINE}
386 {GENERATED_FILE_LINE}
387 {GENERATED_FILE_LINE}
389 # Generated by `{' '.join(GENERATED_BY_ARGS)}`.
392 defaults pref(webgl.colorspaces.prototype,true)
394 {GENERATED_FILE_LINE}
397 # Ensure not white-screening:
398 != {url_from_config({})+'='} about:blank
399 # Ensure differing results with different args:
400 != {url_from_config({'e_color':'color(srgb 1 0 0)'})} {url_from_config({'e_color':'color(srgb 0 1 0)'})}
402 {GENERATED_FILE_LINE}
406 lines.insert(0, REFTEST_LIST_PREAMBLE)
407 lines.append("")
411 for line in lines:
412 print(line)
414 if "--write" not in ARGS:
415 eprint("Use --write to write. Exiting...")
416 sys.exit(0)
420 eprint("Concatenating...")
421 file_str = "\n".join([line.rstrip() for line in lines])
423 eprint(f"Writing to {DEST}...")
424 DEST.write_bytes(file_str.encode())
425 eprint("Done!")
427 sys.exit(0)