From cdafbec95bd598d0bd109e1210053dfe664e6901 Mon Sep 17 00:00:00 2001 From: Thomas Broyer Date: Tue, 6 Feb 2024 14:22:53 +0100 Subject: [PATCH] Improve reflected attribute tests (html/dom/reflection.js) * Fix reflection tests wrt valueOf Without a toString:null in test values that are objects with a valueOf method, the DOM attribute would actually be set to "[object Object]" without ever calling the valueOf method (effectively turning those tests into behaving exactly the same as the {"test": 6} ones). Most existing tests were passing because the implemented algorithms would actually compute a WRONG expected value. The only test that doesn't compute the expected value ("double") won't actually run because all reflected IDL attributes of type "double" have custom getters. Fixes #44315 * Fix reflection tests wrt -0 for "double" attributes Setting -0 to the IDL attribute will set the DOM attribute to "0" (per ECMAScript's ToString, from the rules for the the best representation of a number as a floating point number), and the rules for parsing flowting-point number values won't ever turn that "0" into a -0 value. This test value is never actually being used though, as all the reflected attributes of type "double" have custom getters. * Add a few more reflection tests around whitespace for numeric types Specifically leading non-ASCII whitespace following leading ASCII whitespace (should be rejected), and trailing non-ASCII whitespace (should be ignored) * Add a few more numeric values to reflection tests Specifically "+" and "-" to exercise all possible code paths of failures. * Add exponential notation to reflection tests of double values * Implement "limited double" in reflection tests * Implement the reflect algorithm for doubles This should make it easier to extend tests with more values. --- html/dom/reflection.js | 234 ++++++++++++++++++++++++++++++++--------- 1 file changed, 182 insertions(+), 52 deletions(-) diff --git a/html/dom/reflection.js b/html/dom/reflection.js index a5f7b3fd0a08e6..b2c3b30aae36b3 100644 --- a/html/dom/reflection.js +++ b/html/dom/reflection.js @@ -87,6 +87,90 @@ ReflectionTests.parseInt = function(input) { return sign * value; }; +/** + * The "rules for parsing floating-point number values" from the HTML spec. + * Returns false on error. + */ +ReflectionTests.parseFloat = function(input) { + var position = 0; + var value = 1; + var divisor = 1; + var exponent = 1; + // Skip whitespace + while (input.length > position && /^[ \t\n\f\r]$/.test(input[position])) { + position++; + } + if (position >= input.length) { + return false; + } + if (input[position] == "-") { + value = -1; + divisor = -1; + position++; + } else if (input[position] == "+") { + position++; + } + if (position >= input.length) { + return false; + } + if (input[position] == "." && position+1 < input.length && /^[0-9]$/.test(input[position+1])) { + value = 0; + // Use "else" branches rather than "jump to label fraction" + } else if (!/^[0-9]$/.test(input[position])) { + return false; + } else { + var val = 0; + while (input.length > position && /^[0-9]$/.test(input[position])) { + val *= 10; + // Don't use parseInt even for single-digit strings . . . + val += input.charCodeAt(position) - "0".charCodeAt(0); + position++; + } + value *= val; + } + // Use nested "if" tests rather than "jump to label conversion" or "skip" + // Fraction: + if (input.length > position && input[position] == ".") { + position++; + while (input.length > position && /^[0-9]$/.test(input[position])) { + divisor *= 10; + // Don't use parseInt even for single-digit strings . . . + value += (input.charCodeAt(position) - "0".charCodeAt(0)) / divisor; + position++; + } + } + if (input.length > position && (input[position] == "e" || input[position] == "E")) { + position++; + if (input.length > position) { + if (input[position] == "-") { + exponent = -1; + position++; + } else if (input[position] == "+") { + position++; + } + if (input.length > position && /^[0-9]$/.test(input[position])) { + var exp = 0; + do { + exp *= 10; + // Don't use parseInt even for single-digit strings . . . + exp += input.charCodeAt(position) - "0".charCodeAt(0); + position++; + } while (input.length > position && /^[0-9]$/.test(input[position])); + exponent *= exp; + value *= Math.pow(10, exponent); + } + } + } + // Conversion: + if (!Number.isFinite(value)) { + return false; + } + if (value === 0) { + return 0; + } + return value; +} + // Used in initializing typeMap var binaryString = "\x00\x01\x02\x03\x04\x05\x06\x07 " + "\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f " @@ -273,19 +357,19 @@ ReflectionTests.typeMap = { "jsType": "number", "defaultVal": 0, "domTests": [-36, -1, 0, 1, maxInt, minInt, maxInt + 1, minInt - 1, - maxUnsigned, maxUnsigned + 1, "", "-1", "-0", "0", "1", + maxUnsigned, maxUnsigned + 1, "", "-", "+", "-1", "-0", "0", "1", " " + binaryString + " foo ", // Test various different whitespace. Only 20, 9, A, C, // and D are whitespace. "\u00097", "\u000B7", "\u000C7", "\u00207", "\u00A07", "\uFEFF7", "\u000A7", "\u000D7", "\u20287", "\u20297", "\u16807", "\u180E7", "\u20007", "\u20017", "\u20027", "\u20037", "\u20047", "\u20057", - "\u20067", "\u20077", "\u20087", "\u20097", "\u200A7", "\u202F7", - "\u30007", + "\u20067", "\u20077", "\u20087", "\u20097", "\u200A7", "\u202F7", "\u30007", + "\t\u000B7", "\n\u000B7","\f\u000B7", "\r\u000B7", "\x20\u000B7", "7\u000B", undefined, 1.5, "5%", "+100", ".5", true, false, {"test": 6}, NaN, +Infinity, -Infinity, "\0", {toString:function() {return 2;}, valueOf: null}, - {valueOf:function() {return 3;}}], + {valueOf:function() {return 3;}, toString: null}], "domExpected": function(val) { var parsed = ReflectionTests.parseInt(String(val)); if (parsed === false || parsed > maxInt || parsed < minInt) { @@ -314,17 +398,17 @@ ReflectionTests.typeMap = { "jsType": "number", "defaultVal": -1, "domTests": [minInt - 1, minInt, -36, -1, -0, 0, 1, maxInt, maxInt + 1, - maxUnsigned, maxUnsigned + 1, "", "-1", "-0", "0", "1", + maxUnsigned, maxUnsigned + 1, "", "-", "+", "-1", "-0", "0", "1", " " + binaryString + " foo ", "\u00097", "\u000B7", "\u000C7", "\u00207", "\u00A07", "\uFEFF7", "\u000A7", "\u000D7", "\u20287", "\u20297", "\u16807", "\u180E7", "\u20007", "\u20017", "\u20027", "\u20037", "\u20047", "\u20057", - "\u20067", "\u20077", "\u20087", "\u20097", "\u200A7", "\u202F7", - "\u30007", + "\u20067", "\u20077", "\u20087", "\u20097", "\u200A7", "\u202F7", "\u30007", + "\t\u000B7", "\n\u000B7","\f\u000B7", "\r\u000B7", "\x20\u000B7", "7\u000B", undefined, 1.5, "5%", "+100", ".5", true, false, {"test": 6}, NaN, +Infinity, -Infinity, "\0", {toString:function() {return 2;}, valueOf: null}, - {valueOf:function() {return 3;}}], + {valueOf:function() {return 3;}, toString: null}], "domExpected": function(val) { var parsed = ReflectionTests.parseNonneg(String(val)); if (parsed === false || parsed > maxInt || parsed < minInt) { @@ -351,16 +435,16 @@ ReflectionTests.typeMap = { "jsType": "number", "defaultVal": 0, "domTests": [minInt - 1, minInt, -36, -1, 0, 1, 257, maxInt, - maxInt + 1, maxUnsigned, maxUnsigned + 1, "", "-1", "-0", "0", "1", + maxInt + 1, maxUnsigned, maxUnsigned + 1, "", "-", "+", "-1", "-0", "0", "1", "\u00097", "\u000B7", "\u000C7", "\u00207", "\u00A07", "\uFEFF7", "\u000A7", "\u000D7", "\u20287", "\u20297", "\u16807", "\u180E7", "\u20007", "\u20017", "\u20027", "\u20037", "\u20047", "\u20057", - "\u20067", "\u20077", "\u20087", "\u20097", "\u200A7", "\u202F7", - "\u30007", + "\u20067", "\u20077", "\u20087", "\u20097", "\u200A7", "\u202F7", "\u30007", + "\t\u000B7", "\n\u000B7","\f\u000B7", "\r\u000B7", "\x20\u000B7", "7\u000B", " " + binaryString + " foo ", undefined, 1.5, "5%", "+100", ".5", true, false, {"test": 6}, NaN, +Infinity, -Infinity, "\0", {toString:function() {return 2;}, valueOf: null}, - {valueOf:function() {return 3;}}], + {valueOf:function() {return 3;}, toString: null}], "domExpected": function(val) { var parsed = ReflectionTests.parseNonneg(String(val)); // Note maxInt, not maxUnsigned. @@ -393,16 +477,16 @@ ReflectionTests.typeMap = { "jsType": "number", "defaultVal": 1, "domTests": [minInt - 1, minInt, -36, -1, 0, 1, maxInt, - maxInt + 1, maxUnsigned, maxUnsigned + 1, "", "-1", "-0", "0", "1", + maxInt + 1, maxUnsigned, maxUnsigned + 1, "", "-", "+", "-1", "-0", "0", "1", "\u00097", "\u000B7", "\u000C7", "\u00207", "\u00A07", "\uFEFF7", "\u000A7", "\u000D7", "\u20287", "\u20297", "\u16807", "\u180E7", "\u20007", "\u20017", "\u20027", "\u20037", "\u20047", "\u20057", - "\u20067", "\u20077", "\u20087", "\u20097", "\u200A7", "\u202F7", - "\u30007", + "\u20067", "\u20077", "\u20087", "\u20097", "\u200A7", "\u202F7", "\u30007", + "\t\u000B7", "\n\u000B7","\f\u000B7", "\r\u000B7", "\x20\u000B7", "7\u000B", " " + binaryString + " foo ", undefined, 1.5, "5%", "+100", ".5", true, false, {"test": 6}, NaN, +Infinity, -Infinity, "\0", {toString:function() {return 2;}, valueOf: null}, - {valueOf:function() {return 3;}}], + {valueOf:function() {return 3;}, toString: null}], "domExpected": function(val) { var parsed = ReflectionTests.parseNonneg(String(val)); // Note maxInt, not maxUnsigned. @@ -433,16 +517,16 @@ ReflectionTests.typeMap = { "limited unsigned long with fallback": { "jsType": "number", "domTests": [minInt - 1, minInt, -36, -1, 0, 1, maxInt, - maxInt + 1, maxUnsigned, maxUnsigned + 1, "", "-1", "-0", "0", "1", + maxInt + 1, maxUnsigned, maxUnsigned + 1, "", "-", "+", "-1", "-0", "0", "1", "\u00097", "\u000B7", "\u000C7", "\u00207", "\u00A07", "\uFEFF7", "\u000A7", "\u000D7", "\u20287", "\u20297", "\u16807", "\u180E7", "\u20007", "\u20017", "\u20027", "\u20037", "\u20047", "\u20057", - "\u20067", "\u20077", "\u20087", "\u20097", "\u200A7", "\u202F7", - "\u30007", + "\u20067", "\u20077", "\u20087", "\u20097", "\u200A7", "\u202F7", "\u30007", + "\t\u000B7", "\n\u000B7","\f\u000B7", "\r\u000B7", "\x20\u000B7", "7\u000B", " " + binaryString + " foo ", undefined, 1.5, "5%", "+100", ".5", true, false, {"test": 6}, NaN, +Infinity, -Infinity, "\0", {toString:function() {return 2;}, valueOf: null}, - {valueOf:function() {return 3;}}], + {valueOf:function() {return 3;}, toString: null}], "domExpected": function(val) { var parsed = ReflectionTests.parseNonneg(String(val)); // Note maxInt, not maxUnsigned. @@ -472,16 +556,16 @@ ReflectionTests.typeMap = { "clamped unsigned long": { "jsType": "number", "domTests": [minInt - 1, minInt, -36, -1, 0, 1, maxInt, - maxInt + 1, maxUnsigned, maxUnsigned + 1, "", "-1", "-0", "0", "1", + maxInt + 1, maxUnsigned, maxUnsigned + 1, "", "-", "+", "-1", "-0", "0", "1", "\u00097", "\u000B7", "\u000C7", "\u00207", "\u00A07", "\uFEFF7", "\u000A7", "\u000D7", "\u20287", "\u20297", "\u16807", "\u180E7", "\u20007", "\u20017", "\u20027", "\u20037", "\u20047", "\u20057", - "\u20067", "\u20077", "\u20087", "\u20097", "\u200A7", "\u202F7", - "\u30007", + "\u20067", "\u20077", "\u20087", "\u20097", "\u200A7", "\u202F7", "\u30007", + "\t\u000B7", "\n\u000B7","\f\u000B7", "\r\u000B7", "\x20\u000B7", "7\u000B", " " + binaryString + " foo ", undefined, 1.5, "5%", "+100", ".5", true, false, {"test": 6}, NaN, +Infinity, -Infinity, "\0", {toString:function() {return 2;}, valueOf: null}, - {valueOf:function() {return 3;}}], + {valueOf:function() {return 3;}, toString: null}], "idlTests": [0, 1, 257, maxInt, "-0", maxInt + 1, maxUnsigned], "idlDomExpected": [0, 1, 257, maxInt, 0, null, null], }, @@ -501,45 +585,84 @@ ReflectionTests.typeMap = { * "Except where otherwise specified, if an IDL attribute that is a * floating point number type (double) is assigned an Infinity or * Not-a-Number (NaN) value, a NOT_SUPPORTED_ERR exception must be raised." - * - * TODO: Implement the actual algorithm so we can run lots more tests. For - * now we're stuck with manually setting up expected values. Of course, - * a lot of care has to be taken in checking equality for floats . . . - * maybe we should have some tolerance for comparing them. I'm not even - * sure whether setting the content attribute to 0 should return 0.0 or - * -0.0 (the former, I hope). */ "double": { "jsType": "number", "defaultVal": 0.0, "domTests": [minInt - 1, minInt, -36, -1, 0, 1, maxInt, - maxInt + 1, maxUnsigned, maxUnsigned + 1, "", + maxInt + 1, maxUnsigned, maxUnsigned + 1, "", "-", "+", + "\u00097", "\u000B7", "\u000C7", "\u00207", "\u00A07", "\uFEFF7", + "\u000A7", "\u000D7", "\u20287", "\u20297", "\u16807", "\u180E7", + "\u20007", "\u20017", "\u20027", "\u20037", "\u20047", "\u20057", + "\u20067", "\u20077", "\u20087", "\u20097", "\u200A7", "\u202F7", "\u30007", + "\t\u000B7", "\n\u000B7","\f\u000B7", "\r\u000B7", "\x20\u000B7", "7\u000B", + " " + binaryString + " foo ", undefined, 1.5, "5%", "+100", ".5", true, false, + "1.", "1e2", "1e+2", "1e-2", "1E2", "1E+2", "1E-2", "1.e2", "1.0e2", + "1. 1", "1 .1", "1. e2", "1 .e2", "1 e2", "1e 2", "1e -2", "1e- 2", + "1.8e308", "-1.8e308", + {"test": 6}, NaN, +Infinity, -Infinity, "\0", + {toString:function() {return 2;}, valueOf: null}, + {valueOf:function() {return 3;}, toString: null}], + "domExpected": function (val) { + var parsed = ReflectionTests.parseFloat(String(val)); + if (parsed === false) { + return null; + } + return parsed; + }, + "idlTests": [ -10000000000, -1, -0, 0, 1, 10000000000, + 1e-10, 1e-4, 1.5, 1e25 ], + "idlIdlExpected": function (val) { + // This is a bit heavy-weight but hopefully will give values + // that compare "better" (without introducing some tolerance) + // when the test cases are expanded with more values. + return ReflectionTests.parseFloat(String(val)); + } + }, + /** + * Reflected IDL attribute of type double, limited to only positive values, + * are similar to the previous case with the following exceptions: + * + * - on getting, if the parsed value is not greater than 0, then return + * the default value + * - on setting, if the value is not greater than 0, then return (leaving) + * the attribute to its previous value. + */ + "limited double": { + "jsType": "number", + "defaultVal": 0.0, + "domTests": [minInt - 1, minInt, -36, -1, 0, 1, maxInt, + maxInt + 1, maxUnsigned, maxUnsigned + 1, "", "-", "+", "\u00097", "\u000B7", "\u000C7", "\u00207", "\u00A07", "\uFEFF7", "\u000A7", "\u000D7", "\u20287", "\u20297", "\u16807", "\u180E7", "\u20007", "\u20017", "\u20027", "\u20037", "\u20047", "\u20057", - "\u20067", "\u20077", "\u20087", "\u20097", "\u200A7", "\u202F7", - "\u30007", + "\u20067", "\u20077", "\u20087", "\u20097", "\u200A7", "\u202F7", "\u30007", + "\t\u000B7", "\n\u000B7","\f\u000B7", "\r\u000B7", "\x20\u000B7", "7\u000B", " " + binaryString + " foo ", undefined, 1.5, "5%", "+100", ".5", true, false, + "1.", "1e2", "1e+2", "1e-2", "1E2", "1E+2", "1E-2", "1.e2", "1.0e2", + "1. 1", "1 .1", "1. e2", "1 .e2", "1 e2", "1e 2", "1e -2", "1e- 2", + "1.8e308", "-1.8e308", {"test": 6}, NaN, +Infinity, -Infinity, "\0", {toString:function() {return 2;}, valueOf: null}, - {valueOf:function() {return 3;}}], - "domExpected": [minInt - 1, minInt, -36, -1, 0, 1, maxInt, - maxInt + 1, maxUnsigned, maxUnsigned + 1, null, - // Leading whitespace tests - 7, null, 7, 7, null, null, - 7, 7, null, null, null, null, - null, null, null, null, null, null, - null, null, null, null, null, null, - null, - // End leading whitespace tests - null, null, 1.5, 5, 100, 0.5, null, null, - null, null, null, null, null, - 2, 3], - // I checked that ES ToString is well-defined for all of these (I - // think). Yes, String(-0) == "0". - "idlTests": [ -10000000000, -1, -0, 0, 1, 10000000000], - "idlDomExpected": ["-10000000000", "-1", "0", "0", "1", "10000000000"], - "idlIdlExpected": [ -10000000000, -1, -0, 0, 1, 10000000000] + {valueOf:function() {return 3;}, toString: null}], + "domExpected": function (val) { + var parsed = ReflectionTests.parseFloat(String(val)); + if (parsed === false || parsed <= 0) { + return null; + } + return parsed; + }, + "idlTests": [ -10000000000, -1, -0, 0, 1, 10000000000, + 1e-10, 1e-4, 1.5, 1e25 ], + "idlIdlExpected": function (val) { + // Non-positive values are special-cased below, as they + // should be ignored, leaving the current value unchanged + + // This is a bit heavy-weight but hopefully will give values + // that compare "better" (without introducing some tolerance) + // when the test cases are expanded with more values. + return ReflectionTests.parseFloat(String(val)); + } } }; @@ -836,6 +959,13 @@ ReflectionTests.reflects = function(data, idlName, idlObj, domName, domObj) { ReflectionHarness.assertThrows("IndexSizeError", function() { idlObj[idlName] = idlTests[i]; }); + } else if (data.type == "limited double" && idlTests[i] <= 0) { + domObj.setAttribute(domName, "previous value"); + var previousIdl = idlObj[idlName]; // should be the default value + idlObj[idlName] = idlTests[i]; + ReflectionHarness.assertEquals(domObj.getAttribute(domName), + "previous value", "getAttribute()"); + ReflectionHarness.assertEquals(idlObj[idlName], previousIdl, "IDL get"); } else { idlObj[idlName] = idlTests[i]; if (data.type == "boolean") {