summaryrefslogtreecommitdiffstats
path: root/dom/canvas/test/reftest/colors/generate_color_canvas_reftests.py
diff options
context:
space:
mode:
Diffstat (limited to 'dom/canvas/test/reftest/colors/generate_color_canvas_reftests.py')
-rw-r--r--dom/canvas/test/reftest/colors/generate_color_canvas_reftests.py427
1 files changed, 427 insertions, 0 deletions
diff --git a/dom/canvas/test/reftest/colors/generate_color_canvas_reftests.py b/dom/canvas/test/reftest/colors/generate_color_canvas_reftests.py
new file mode 100644
index 0000000000..8c1e5f3788
--- /dev/null
+++ b/dom/canvas/test/reftest/colors/generate_color_canvas_reftests.py
@@ -0,0 +1,427 @@
+#! python3
+
+# Typecheck:
+# `pip -m mypy generate_color_canvas_reftests.py`
+#
+# Run:
+# `./generate_color_canvas_reftests.py [--write]`
+
+import functools
+import json
+import math
+import pathlib
+import re
+import sys
+from typing import Iterable, NamedTuple, TypeVar
+
+ARGS = sys.argv[1:]
+DEST = pathlib.Path(__file__).parent / "_generated_reftest.list"
+
+COL_DELIM = " "
+COL_ALIGNMENT = 4
+
+# -
+
+T = TypeVar("T")
+U = TypeVar("U")
+
+# -
+
+
+# crossCombine([{a:false},{a:5}], [{},{b:5}])
+# [{a:false}, {a:true}, {a:false,b:5}, {a:true,b:5}]
+def cross_combine(*args_tup: list[dict]) -> list[dict]:
+ args = list(args_tup)
+ for i, a in enumerate(args):
+ assert type(a) == list, f"Arg{i} is {type(a)}, expected {list}."
+
+ def cross_combine2(listA, listB):
+ listC = []
+ for a in listA:
+ for b in listB:
+ c = dict()
+ c.update(a)
+ c.update(b)
+ listC.append(c)
+ return listC
+
+ res: list[dict] = [dict()]
+ while True:
+ try:
+ next = args.pop(0)
+ except IndexError:
+ break
+ res = cross_combine2(res, next)
+ return res
+
+
+# keyed_alternatives('count', [1,2,3]) -> [{count: 1}, {count: 2}, {count: 3}]
+def keyed_alternatives(key: T, vals: Iterable[U]) -> list[dict[T, U]]:
+ """
+ res = []
+ for v in vals:
+ d = dict()
+ d[key] = v
+ res.append(d)
+ return res
+ """
+ # return [dict([[key,v]]) for v in vals]
+ return [{key: v} for v in vals]
+
+
+# -
+
+
+def eprint(*args, **kwargs):
+ sys.stdout.flush()
+ print(*args, file=sys.stderr, **kwargs)
+ sys.stderr.flush()
+
+
+# -
+
+# 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)
+
+CSPACE_LIST = ["srgb", "display-p3"]
+CANVAS_CSPACES = keyed_alternatives("e_cspace", CSPACE_LIST)
+
+RGB_LIST = [
+ "0.000 0.000 0.000",
+ "0.200 0.200 0.200", # 0.2*255 = 51
+ "0.200 0.000 0.000",
+ "0.000 0.200 0.000",
+ "0.000 0.000 0.200",
+ "0.502 0.502 0.502", # 0.502*255 = 128.01
+ "0.502 0.000 0.000",
+ "0.000 0.502 0.000",
+ "0.000 0.000 0.502",
+ "1.000 1.000 1.000",
+ #'1.000 0.000 0.000', # These will hit gamut clipping on most displays.
+ #'0.000 1.000 0.000',
+ #'0.000 0.000 1.000',
+]
+
+WEBGL_COLORS = keyed_alternatives("e_color", [f"color(srgb {rgb})" for rgb in RGB_LIST])
+
+C2D_COLORS = []
+for cspace in CSPACE_LIST:
+ C2D_COLORS += keyed_alternatives(
+ "e_color", [f"color({cspace} {rgb})" for rgb in RGB_LIST]
+ )
+
+# -
+
+WEBGL_FORMATS = keyed_alternatives(
+ "e_webgl_format",
+ [
+ "RGBA8",
+ # Bug 1883748: (webgl.drawingbufferStorage)
+ #'SRGB8_ALPHA8',
+ #'RGBA16F',
+ ],
+)
+WEBGL = cross_combine(
+ [{"e_context": "webgl"}], WEBGL_FORMATS, CANVAS_CSPACES, WEBGL_COLORS
+)
+
+# -
+
+C2D_OPTIONS_COMBOS = cross_combine(
+ keyed_alternatives("willReadFrequently", ["true", "false"]), # E.g. D2D vs Skia
+ # keyed_alternatives('alpha', ['true','false'])
+)
+C2D_OPTIONS = [
+ json.dumps(config, separators=(",", ":")) for config in C2D_OPTIONS_COMBOS
+]
+
+C2D = cross_combine(
+ [{"e_context": "2d"}],
+ keyed_alternatives("e_options", C2D_OPTIONS),
+ CANVAS_CSPACES,
+ C2D_COLORS,
+)
+
+# -
+
+COMBOS: list[dict[str, str]] = cross_combine(WEBGL + C2D)
+
+eprint(f"{len(COMBOS)} combinations...")
+
+# -
+
+Config = dict[str, str]
+
+
+class CssColor(NamedTuple):
+ cspace: str
+ rgb: str
+
+ def rgb_vals(self) -> tuple[float, float, float]:
+ (r, g, b) = [float(z) for z in self.rgb.split(" ")]
+ return (r, g, b)
+
+ def is_same_color(x, y) -> bool:
+ if x == y:
+ return True
+ (r, g, b) = x.rgb_vals()
+ if x.rgb == y.rgb and r == g and g == b:
+ return True
+ return False
+
+
+class Reftest(NamedTuple):
+ notes: list[str]
+ op: str
+ test_config: Config
+ ref_config: Config
+
+
+def make_ref_config(color: CssColor) -> Config:
+ return {
+ "e_context": "css",
+ "e_color": f"color({color.cspace} {color.rgb})",
+ }
+
+
+class ColorReftest(NamedTuple):
+ notes: list[str]
+ test_config: Config
+ ref_color: CssColor
+
+ def to_reftest(self):
+ ref_config = make_ref_config(self.ref_color)
+ return Reftest(self.notes.copy(), "==", self.test_config.copy(), ref_config)
+
+
+class Expectation(NamedTuple):
+ notes: list[str]
+ test_config: Config
+ color: CssColor
+
+
+def parse_css_color(s: str) -> CssColor:
+ m = re.match("color[(]([^)]+)[)]", s)
+ assert m, s
+ (cspace, rgb) = m.group(1).split(" ", 1)
+ return CssColor(cspace, rgb)
+
+
+def correct_color_from_test_config(test_config: Config) -> CssColor:
+ canvas_cspace = test_config["e_cspace"]
+ if not canvas_cspace:
+ canvas_cspace = "srgb"
+
+ correct_color = parse_css_color(test_config["e_color"])
+ if test_config["e_context"] == "webgl":
+ # Webgl ignores the color's cspace, because webgl has no concept of
+ # source colorspace for clears/draws to the backbuffer.
+ # This (correct) behavior is as if the color's cspace were overwritten by the
+ # cspace of the canvas. (so expect that)
+ correct_color = CssColor(canvas_cspace, correct_color.rgb)
+
+ return correct_color
+
+
+# -------------------------------------
+# -------------------------------------
+# -------------------------------------
+# Choose (multiple?) reference configs given a test config.
+
+
+def reftests_from_config(test_config: Config) -> Iterable[ColorReftest]:
+ correct_color = correct_color_from_test_config(test_config)
+
+ if test_config["e_context"] == "2d":
+ # Canvas2d generally has the same behavior as css, so expect all passing.
+ yield ColorReftest([], test_config, correct_color)
+ return
+
+ assert test_config["e_context"] == "webgl", test_config["e_context"]
+
+ # -
+
+ def reftests_from_expected_color(
+ notes: list[str], expected_color: CssColor
+ ) -> Iterable[ColorReftest]:
+ # If expecting failure, generate two tests, both expecting failure:
+ # 1. expected-fail test == correct_color
+ # 2. expected-pass test == (incorrect) expected_color
+ # If we fix an error, we'll see one unexpected-pass and one unexpected-fail.
+ # If we get a new wrong answer, we'll see one unexpected-fail.
+
+ if not expected_color.is_same_color(correct_color):
+ yield ColorReftest(notes + ["fails"], test_config, correct_color)
+ yield ColorReftest(notes, test_config, expected_color)
+ else:
+ yield ColorReftest(notes, test_config, correct_color)
+
+ # -
+ # On Mac, (with the pref) we do tag the IOSurface with the right cspace.
+ # On other platforms, Webgl always outputs in the display color profile
+ # right now. This is the same as "srgb".
+
+ expected_color_srgb = CssColor("srgb", correct_color.rgb)
+ # Mac
+ yield from reftests_from_expected_color(["skip-if(!cocoaWidget)"], correct_color)
+ # Win, Lin, Android
+ yield from reftests_from_expected_color(
+ ["skip-if(cocoaWidget) "], expected_color_srgb
+ )
+
+
+# -
+
+
+def amended_notes_from_reftest(reftest: ColorReftest) -> list[str]:
+ notes = reftest.notes[:]
+
+ ref_rgb_vals = reftest.ref_color.rgb_vals()
+ is_green_only = ref_rgb_vals == (0, ref_rgb_vals[1], 0)
+ if (
+ "fails" in reftest.notes
+ and reftest.test_config["e_context"] == "webgl"
+ and reftest.test_config["e_cspace"] == "display-p3"
+ and is_green_only
+ ):
+ # Android's display bitdepth rounds srgb green and p3 green to the same thing.
+ notes[notes.index("fails")] = "fails-if(!Android)"
+
+ return notes
+
+
+# -------------------------------------
+# -------------------------------------
+# -------------------------------------
+# Ok, back to implementation.
+
+
+def encode_url_v(k, v):
+ if k == "e_color":
+ # reftest harness can't deal with spaces in urls, and 'color(srgb%201%200%200)' is hard to read.
+ v = v.replace(" ", ",")
+
+ assert " " not in v, (k, v)
+ return v
+
+
+# Cool:
+assert encode_url_v("e_color", "color(srgb 0 0 0)") == "color(srgb,0,0,0)"
+# Unfortunate, but tolerable:
+assert encode_url_v("e_color", "color(srgb 0 0 0)") == "color(srgb,,,0,,,0,,,0)"
+
+# -
+
+
+def url_from_config(kvs: Config) -> str:
+ parts = [f"{k}={encode_url_v(k,v)}" for k, v in kvs.items()]
+ url = "color_canvas.html?" + "&".join(parts)
+ return url
+
+
+# -
+
+color_reftests: list[ColorReftest] = []
+for c in COMBOS:
+ color_reftests += reftests_from_config(c)
+color_reftests = [
+ ColorReftest(amended_notes_from_reftest(r), r.test_config, r.ref_color)
+ for r in color_reftests
+]
+reftests = [r.to_reftest() for r in color_reftests]
+
+# -
+
+HEADINGS = ["# annotations", "op", "test", "reference"]
+table: list[list[str]] = [HEADINGS]
+table = [
+ [
+ " ".join(r.notes),
+ r.op,
+ url_from_config(r.test_config),
+ url_from_config(r.ref_config),
+ ]
+ for r in reftests
+]
+
+# -
+
+
+def round_to(a, b: int) -> int:
+ return int(math.ceil(a / b) * b)
+
+
+def aligned_lines_from_table(
+ rows: list[list[str]], col_delim=" ", col_alignment=4
+) -> Iterable[str]:
+ max_col_len = functools.reduce(
+ lambda accum, input: [max(r, len(c)) for r, c in zip(accum, input)],
+ rows,
+ [0 for _ in rows[0]],
+ )
+ max_col_len = [round_to(x, col_alignment) for x in max_col_len]
+
+ for i, row in enumerate(rows):
+ parts = [s + (" " * (col_len - len(s))) for s, col_len in zip(row, max_col_len)]
+ line = col_delim.join(parts)
+ yield line
+
+
+# -
+
+GENERATED_FILE_LINE = "### Generated, do not edit. ###"
+
+lines = list(aligned_lines_from_table(table, COL_DELIM, COL_ALIGNMENT))
+WARN_EVERY_N_LINES = 5
+i = WARN_EVERY_N_LINES - 1
+while i < len(lines):
+ lines.insert(i, " " + GENERATED_FILE_LINE)
+ i += WARN_EVERY_N_LINES
+
+# -
+
+GENERATED_BY_ARGS = [f"./{pathlib.Path(__file__).name}"] + ARGS
+
+REFTEST_LIST_PREAMBLE = f"""\
+{GENERATED_FILE_LINE}
+ {GENERATED_FILE_LINE}
+ {GENERATED_FILE_LINE}
+
+# Generated by `{' '.join(GENERATED_BY_ARGS)}`.
+# -
+
+defaults pref(webgl.colorspaces.prototype,true)
+
+{GENERATED_FILE_LINE}
+# -
+
+# Ensure not white-screening:
+!= {url_from_config({})+'='} about:blank
+# Ensure differing results with different args:
+!= {url_from_config({'e_color':'color(srgb 1 0 0)'})} {url_from_config({'e_color':'color(srgb 0 1 0)'})}
+
+{GENERATED_FILE_LINE}
+# -
+"""
+
+lines.insert(0, REFTEST_LIST_PREAMBLE)
+lines.append("")
+
+# -
+
+for line in lines:
+ print(line)
+
+if "--write" not in ARGS:
+ eprint("Use --write to write. Exiting...")
+ sys.exit(0)
+
+# -
+
+eprint("Concatenating...")
+file_str = "\n".join([line.rstrip() for line in lines])
+
+eprint(f"Writing to {DEST}...")
+DEST.write_bytes(file_str.encode())
+eprint("Done!")
+
+sys.exit(0)