Skip to content

Commit

Permalink
Improve reflected attribute tests (html/dom/reflection.js)
Browse files Browse the repository at this point in the history
* 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.
  • Loading branch information
tbroyer authored Feb 6, 2024
1 parent e3f0f30 commit cdafbec
Showing 1 changed file with 182 additions and 52 deletions.
234 changes: 182 additions & 52 deletions html/dom/reflection.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 "
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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],
},
Expand All @@ -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));
}
}
};

Expand Down Expand Up @@ -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") {
Expand Down

0 comments on commit cdafbec

Please sign in to comment.