4 # `pip -m mypy generate_color_canvas_reftests.py`
7 # `./generate_color_canvas_reftests.py [--write]`
15 from typing
import Iterable
, NamedTuple
, TypeVar
18 DEST
= pathlib
.Path(__file__
).parent
/ "_generated_reftest.list"
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]:
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
):
48 res
: list[dict] = [dict()]
54 res
= cross_combine2(res
, next
)
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
]]:
68 # return [dict([[key,v]]) for v in vals]
69 return [{key
: v
} for v
in vals
]
75 def eprint(*args
, **kwargs
):
77 print(*args
, file=sys
.stderr
, **kwargs
)
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
)
90 "0.200 0.200 0.200", # 0.2*255 = 51
94 "0.502 0.502 0.502", # 0.502*255 = 128.01
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
])
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(
118 # Bug 1883748: (webgl.drawingbufferStorage)
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'])
134 json
.dumps(config
, separators
=(",", ":")) for config
in C2D_OPTIONS_COMBOS
138 [{"e_context": "2d"}],
139 keyed_alternatives("e_options", C2D_OPTIONS
),
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
):
159 def rgb_vals(self
) -> tuple[float, float, float]:
160 (r
, g
, b
) = [float(z
) for z
in self
.rgb
.split(" ")]
163 def is_same_color(x
, y
) -> bool:
166 (r
, g
, b
) = x
.rgb_vals()
167 if x
.rgb
== y
.rgb
and r
== g
and g
== b
:
172 class Reftest(NamedTuple
):
179 def make_ref_config(color
: CssColor
) -> Config
:
182 "e_color": f
"color({color.cspace} {color.rgb})",
186 class ColorReftest(NamedTuple
):
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
):
202 def parse_css_color(s
: str) -> CssColor
:
203 m
= re
.match("color[(]([^)]+)[)]", 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
)
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
)
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
)
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
)
265 yield from reftests_from_expected_color(["skip-if(!cocoaWidget)"], correct_color
)
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)
281 "fails" in reftest
.notes
282 and reftest
.test_config
["e_context"] == "webgl"
283 and reftest
.test_config
["e_cspace"] == "display-p3"
286 # Android's display bitdepth rounds srgb green and p3 green to the same thing.
287 notes
[notes
.index("fails")] = "fails-if(!Android)"
292 # -------------------------------------
293 # -------------------------------------
294 # -------------------------------------
295 # Ok, back to implementation.
298 def encode_url_v(k
, v
):
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
)
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
)
323 color_reftests
: list[ColorReftest
] = []
325 color_reftests
+= reftests_from_config(c
)
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
]
340 url_from_config(r
.test_config
),
341 url_from_config(r
.ref_config
),
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
356 max_col_len
= functools
.reduce(
357 lambda accum
, input: [max(r
, len(c
)) for r
, c
in zip(accum
, input)],
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
)
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
)
414 if "--write" not in ARGS
:
415 eprint("Use --write to write. Exiting...")
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())