Update web-platform-tests to revision e92532746b7615dcccdfa060937a87664816b1db

This commit is contained in:
WPT Sync Bot 2018-02-21 20:12:51 -05:00
parent cccca27f4f
commit 726b56aa12
149 changed files with 22796 additions and 1884 deletions

View file

@ -122129,18 +122129,6 @@
{} {}
] ]
], ],
"css/css-position/position-sticky-inflow-position.html": [
[
"/css/css-position/position-sticky-inflow-position.html",
[
[
"/css/css-position/position-sticky-inflow-position-ref.html",
"=="
]
],
{}
]
],
"css/css-position/position-sticky-inline.html": [ "css/css-position/position-sticky-inline.html": [
[ [
"/css/css-position/position-sticky-inline.html", "/css/css-position/position-sticky-inline.html",
@ -122153,18 +122141,6 @@
{} {}
] ]
], ],
"css/css-position/position-sticky-margins.html": [
[
"/css/css-position/position-sticky-margins.html",
[
[
"/css/css-position/position-sticky-margins-ref.html",
"=="
]
],
{}
]
],
"css/css-position/position-sticky-nested-bottom.html": [ "css/css-position/position-sticky-nested-bottom.html": [
[ [
"/css/css-position/position-sticky-nested-bottom.html", "/css/css-position/position-sticky-nested-bottom.html",
@ -122237,18 +122213,6 @@
{} {}
] ]
], ],
"css/css-position/position-sticky-overflow-padding.html": [
[
"/css/css-position/position-sticky-overflow-padding.html",
[
[
"/css/css-position/position-sticky-overflow-padding-ref.html",
"=="
]
],
{}
]
],
"css/css-position/position-sticky-rendering.html": [ "css/css-position/position-sticky-rendering.html": [
[ [
"/css/css-position/position-sticky-rendering.html", "/css/css-position/position-sticky-rendering.html",
@ -122261,18 +122225,6 @@
{} {}
] ]
], ],
"css/css-position/position-sticky-root-scroller.html": [
[
"/css/css-position/position-sticky-root-scroller.html",
[
[
"/css/css-position/position-sticky-root-scroller-ref.html",
"=="
]
],
{}
]
],
"css/css-position/position-sticky-stacking-context.html": [ "css/css-position/position-sticky-stacking-context.html": [
[ [
"/css/css-position/position-sticky-stacking-context.html", "/css/css-position/position-sticky-stacking-context.html",
@ -122393,30 +122345,6 @@
{} {}
] ]
], ],
"css/css-position/position-sticky-transforms-translate.html": [
[
"/css/css-position/position-sticky-transforms-translate.html",
[
[
"/css/css-position/position-sticky-transforms-translate-ref.html",
"=="
]
],
{}
]
],
"css/css-position/position-sticky-transforms.html": [
[
"/css/css-position/position-sticky-transforms.html",
[
[
"/css/css-position/position-sticky-transforms-ref.html",
"=="
]
],
{}
]
],
"css/css-position/position-sticky-writing-modes.html": [ "css/css-position/position-sticky-writing-modes.html": [
[ [
"/css/css-position/position-sticky-writing-modes.html", "/css/css-position/position-sticky-writing-modes.html",
@ -209180,6 +209108,21 @@
{} {}
] ]
], ],
"conformance-checkers/html/elements/script/streams-demo-append-child-isvalid.html": [
[
{}
]
],
"conformance-checkers/html/elements/script/streams-demo-streaming-element-backpressure-isvalid.html": [
[
{}
]
],
"conformance-checkers/html/elements/script/streams-demo-streaming-element-isvalid.html": [
[
{}
]
],
"conformance-checkers/html/elements/small/model-isvalid.html": [ "conformance-checkers/html/elements/small/model-isvalid.html": [
[ [
{} {}
@ -249005,21 +248948,11 @@
{} {}
] ]
], ],
"css/css-position/position-sticky-inflow-position-ref.html": [
[
{}
]
],
"css/css-position/position-sticky-inline-ref.html": [ "css/css-position/position-sticky-inline-ref.html": [
[ [
{} {}
] ]
], ],
"css/css-position/position-sticky-margins-ref.html": [
[
{}
]
],
"css/css-position/position-sticky-nested-bottom-ref.html": [ "css/css-position/position-sticky-nested-bottom-ref.html": [
[ [
{} {}
@ -249050,21 +248983,11 @@
{} {}
] ]
], ],
"css/css-position/position-sticky-overflow-padding-ref.html": [
[
{}
]
],
"css/css-position/position-sticky-rendering-ref.html": [ "css/css-position/position-sticky-rendering-ref.html": [
[ [
{} {}
] ]
], ],
"css/css-position/position-sticky-root-scroller-ref.html": [
[
{}
]
],
"css/css-position/position-sticky-stacking-context-ref.html": [ "css/css-position/position-sticky-stacking-context-ref.html": [
[ [
{} {}
@ -249115,16 +249038,6 @@
{} {}
] ]
], ],
"css/css-position/position-sticky-transforms-ref.html": [
[
{}
]
],
"css/css-position/position-sticky-transforms-translate-ref.html": [
[
{}
]
],
"css/css-position/position-sticky-writing-modes-ref.html": [ "css/css-position/position-sticky-writing-modes-ref.html": [
[ [
{} {}
@ -271730,6 +271643,11 @@
{} {}
] ]
], ],
"fetch/corb/resources/subframe-that-posts-html-containing-blob-url-to-parent.html": [
[
{}
]
],
"fetch/data-urls/README.md": [ "fetch/data-urls/README.md": [
[ [
{} {}
@ -291115,6 +291033,11 @@
{} {}
] ]
], ],
"service-workers/service-worker/resources/embed-image-is-not-intercepted-iframe.html": [
[
{}
]
],
"service-workers/service-worker/resources/embed-is-not-intercepted-iframe.html": [ "service-workers/service-worker/resources/embed-is-not-intercepted-iframe.html": [
[ [
{} {}
@ -291685,6 +291608,11 @@
{} {}
] ]
], ],
"service-workers/service-worker/resources/object-image-is-not-intercepted-iframe.html": [
[
{}
]
],
"service-workers/service-worker/resources/object-is-not-intercepted-iframe.html": [ "service-workers/service-worker/resources/object-is-not-intercepted-iframe.html": [
[ [
{} {}
@ -312579,6 +312507,12 @@
{} {}
] ]
], ],
"css/css-position/position-sticky-inflow-position.html": [
[
"/css/css-position/position-sticky-inflow-position.html",
{}
]
],
"css/css-position/position-sticky-input-box-gets-focused-after-scroll.html": [ "css/css-position/position-sticky-input-box-gets-focused-after-scroll.html": [
[ [
"/css/css-position/position-sticky-input-box-gets-focused-after-scroll.html", "/css/css-position/position-sticky-input-box-gets-focused-after-scroll.html",
@ -312591,6 +312525,12 @@
{} {}
] ]
], ],
"css/css-position/position-sticky-margins.html": [
[
"/css/css-position/position-sticky-margins.html",
{}
]
],
"css/css-position/position-sticky-offset-overflow.html": [ "css/css-position/position-sticky-offset-overflow.html": [
[ [
"/css/css-position/position-sticky-offset-overflow.html", "/css/css-position/position-sticky-offset-overflow.html",
@ -312603,6 +312543,12 @@
{} {}
] ]
], ],
"css/css-position/position-sticky-overflow-padding.html": [
[
"/css/css-position/position-sticky-overflow-padding.html",
{}
]
],
"css/css-position/position-sticky-parsing.html": [ "css/css-position/position-sticky-parsing.html": [
[ [
"/css/css-position/position-sticky-parsing.html", "/css/css-position/position-sticky-parsing.html",
@ -312615,12 +312561,30 @@
{} {}
] ]
], ],
"css/css-position/position-sticky-root-scroller.html": [
[
"/css/css-position/position-sticky-root-scroller.html",
{}
]
],
"css/css-position/position-sticky-top.html": [ "css/css-position/position-sticky-top.html": [
[ [
"/css/css-position/position-sticky-top.html", "/css/css-position/position-sticky-top.html",
{} {}
] ]
], ],
"css/css-position/position-sticky-transforms-translate.html": [
[
"/css/css-position/position-sticky-transforms-translate.html",
{}
]
],
"css/css-position/position-sticky-transforms.html": [
[
"/css/css-position/position-sticky-transforms.html",
{}
]
],
"css/css-pseudo/first-letter-property-whitelist.html": [ "css/css-pseudo/first-letter-property-whitelist.html": [
[ [
"/css/css-pseudo/first-letter-property-whitelist.html", "/css/css-pseudo/first-letter-property-whitelist.html",
@ -315405,12 +315369,6 @@
{} {}
] ]
], ],
"css/css-typed-om/the-stylepropertymap/computed/getProperties.tentative.html": [
[
"/css/css-typed-om/the-stylepropertymap/computed/getProperties.tentative.html",
{}
]
],
"css/css-typed-om/the-stylepropertymap/computed/has.tentative.html": [ "css/css-typed-om/the-stylepropertymap/computed/has.tentative.html": [
[ [
"/css/css-typed-om/the-stylepropertymap/computed/has.tentative.html", "/css/css-typed-om/the-stylepropertymap/computed/has.tentative.html",
@ -315471,12 +315429,6 @@
{} {}
] ]
], ],
"css/css-typed-om/the-stylepropertymap/declared/getProperties.tentative.html": [
[
"/css/css-typed-om/the-stylepropertymap/declared/getProperties.tentative.html",
{}
]
],
"css/css-typed-om/the-stylepropertymap/declared/has.tentative.html": [ "css/css-typed-om/the-stylepropertymap/declared/has.tentative.html": [
[ [
"/css/css-typed-om/the-stylepropertymap/declared/has.tentative.html", "/css/css-typed-om/the-stylepropertymap/declared/has.tentative.html",
@ -315537,12 +315489,6 @@
{} {}
] ]
], ],
"css/css-typed-om/the-stylepropertymap/inline/getProperties.tentative.html": [
[
"/css/css-typed-om/the-stylepropertymap/inline/getProperties.tentative.html",
{}
]
],
"css/css-typed-om/the-stylepropertymap/inline/has.tentative.html": [ "css/css-typed-om/the-stylepropertymap/inline/has.tentative.html": [
[ [
"/css/css-typed-om/the-stylepropertymap/inline/has.tentative.html", "/css/css-typed-om/the-stylepropertymap/inline/has.tentative.html",
@ -323459,6 +323405,12 @@
{} {}
] ]
], ],
"fetch/corb/script-html-via-cross-origin-blob-url.sub.html": [
[
"/fetch/corb/script-html-via-cross-origin-blob-url.sub.html",
{}
]
],
"fetch/corb/script-js-mislabeled-as-html-nosniff.sub.html": [ "fetch/corb/script-js-mislabeled-as-html-nosniff.sub.html": [
[ [
"/fetch/corb/script-js-mislabeled-as-html-nosniff.sub.html", "/fetch/corb/script-js-mislabeled-as-html-nosniff.sub.html",
@ -327749,6 +327701,12 @@
{} {}
] ]
], ],
"html/semantics/embedded-content/media-elements/autoplay-with-broken-track.html": [
[
"/html/semantics/embedded-content/media-elements/autoplay-with-broken-track.html",
{}
]
],
"html/semantics/embedded-content/media-elements/error-codes/error.html": [ "html/semantics/embedded-content/media-elements/error-codes/error.html": [
[ [
"/html/semantics/embedded-content/media-elements/error-codes/error.html", "/html/semantics/embedded-content/media-elements/error-codes/error.html",
@ -414540,6 +414498,18 @@
"e2b95f87aee2b09233d324c898e02629ff83f8ce", "e2b95f87aee2b09233d324c898e02629ff83f8ce",
"support" "support"
], ],
"conformance-checkers/html/elements/script/streams-demo-append-child-isvalid.html": [
"8e4bdab31d350439f73114ac52cbed0f0eb4b83d",
"support"
],
"conformance-checkers/html/elements/script/streams-demo-streaming-element-backpressure-isvalid.html": [
"cb0a490ebe790e4c7fc10ab5908613d4bc555c3f",
"support"
],
"conformance-checkers/html/elements/script/streams-demo-streaming-element-isvalid.html": [
"7d7362011ddab3afc7af925aeebd067188e974e1",
"support"
],
"conformance-checkers/html/elements/small/model-isvalid.html": [ "conformance-checkers/html/elements/small/model-isvalid.html": [
"ed1c313412f5af0134a36fe72fa75b566c83d555", "ed1c313412f5af0134a36fe72fa75b566c83d555",
"support" "support"
@ -501641,7 +501611,7 @@
"support" "support"
], ],
"css/css-paint-api/registered-properties-in-custom-paint.https.html": [ "css/css-paint-api/registered-properties-in-custom-paint.https.html": [
"de1e6dabe19bdd355060a62c7d697c64f49929fe", "dc51ea1e448fc91b632363d5ea394f5a4a90bd06",
"reftest" "reftest"
], ],
"css/css-paint-api/resources/html5.png": [ "css/css-paint-api/resources/html5.png": [
@ -501653,7 +501623,7 @@
"support" "support"
], ],
"css/css-paint-api/style-background-image.https.html": [ "css/css-paint-api/style-background-image.https.html": [
"c3359a8ee23b9c3d9007066235aef894ddbaf47f", "18660b13079289c93431e7ca6111d1c6c7adca80",
"reftest" "reftest"
], ],
"css/css-paint-api/style-before-pseudo-ref.html": [ "css/css-paint-api/style-before-pseudo-ref.html": [
@ -501661,7 +501631,7 @@
"support" "support"
], ],
"css/css-paint-api/style-before-pseudo.https.html": [ "css/css-paint-api/style-before-pseudo.https.html": [
"cd16dfc6e532851b23abdf47cb4cc89f20a6c655", "a46f84e13e91ea23129a557893fcb929d6e191ed",
"reftest" "reftest"
], ],
"css/css-paint-api/style-first-letter-pseudo-ref.html": [ "css/css-paint-api/style-first-letter-pseudo-ref.html": [
@ -501669,7 +501639,7 @@
"support" "support"
], ],
"css/css-paint-api/style-first-letter-pseudo.https.html": [ "css/css-paint-api/style-first-letter-pseudo.https.html": [
"996d9f2e93bc8ba0a54415d0ffcab4cdb7c1ffa7", "180ec110178f0cf9635b19309abb416e79ebf135",
"reftest" "reftest"
], ],
"css/css-paint-api/valid-image-after-load-ref.html": [ "css/css-paint-api/valid-image-after-load-ref.html": [
@ -501796,13 +501766,9 @@
"a06a40f39b4a748c111dc01281261c5451204f95", "a06a40f39b4a748c111dc01281261c5451204f95",
"reftest" "reftest"
], ],
"css/css-position/position-sticky-inflow-position-ref.html": [
"bcce2ded8073a7b5b3477bcf90157cb0e77c2b40",
"support"
],
"css/css-position/position-sticky-inflow-position.html": [ "css/css-position/position-sticky-inflow-position.html": [
"c8e2bcdddf9e8ee93f9306d88b96c3bf1f1bfaf6", "a0fec7d91b7261987e1f2fa5efca966d6f37bc1e",
"reftest" "testharness"
], ],
"css/css-position/position-sticky-inline-ref.html": [ "css/css-position/position-sticky-inline-ref.html": [
"9458cab53d2065e4893d127ee0097bbd53c6b898", "9458cab53d2065e4893d127ee0097bbd53c6b898",
@ -501820,13 +501786,9 @@
"2a04672cdac818a6887eac7d6824ea85d3d0559d", "2a04672cdac818a6887eac7d6824ea85d3d0559d",
"testharness" "testharness"
], ],
"css/css-position/position-sticky-margins-ref.html": [
"0cdb788c913f47a121114ac5b8e6a140bb08c1ff",
"support"
],
"css/css-position/position-sticky-margins.html": [ "css/css-position/position-sticky-margins.html": [
"72fb6ae7d97bf2448ebd68ccf110edd6bae2c92f", "3f6bc9537adf2a4d477f99866d73f42b65c26db3",
"reftest" "testharness"
], ],
"css/css-position/position-sticky-nested-bottom-ref.html": [ "css/css-position/position-sticky-nested-bottom-ref.html": [
"59a8e46358a8a5bf8638a2d1982c63becef5bc77", "59a8e46358a8a5bf8638a2d1982c63becef5bc77",
@ -501884,13 +501846,9 @@
"a25b64d016644c272ea92b6129a59eefb21d2fa0", "a25b64d016644c272ea92b6129a59eefb21d2fa0",
"testharness" "testharness"
], ],
"css/css-position/position-sticky-overflow-padding-ref.html": [
"b3d81934cc90e70dff6bc5cd7789594a8fcd7ecf",
"support"
],
"css/css-position/position-sticky-overflow-padding.html": [ "css/css-position/position-sticky-overflow-padding.html": [
"588502dc7eb4a7f88f78dd1b2cdc857861c89f77", "4a1e1c29bb47027e74437acdc1cfe073ea774f0f",
"reftest" "testharness"
], ],
"css/css-position/position-sticky-parsing.html": [ "css/css-position/position-sticky-parsing.html": [
"224bc984bc6eb4a55931461cf7e51f7b04d219f4", "224bc984bc6eb4a55931461cf7e51f7b04d219f4",
@ -501908,13 +501866,9 @@
"80caf6fb1e6c84dbf3e371a11166ac5b71bba687", "80caf6fb1e6c84dbf3e371a11166ac5b71bba687",
"testharness" "testharness"
], ],
"css/css-position/position-sticky-root-scroller-ref.html": [
"b66947a9f1b39c6c489267477d0122eeaeac7341",
"support"
],
"css/css-position/position-sticky-root-scroller.html": [ "css/css-position/position-sticky-root-scroller.html": [
"8f77892b5a205a392942649476be7d5d54a91788", "b7adc454ecf8b4950c46d99db11cf669b1a5d695",
"reftest" "testharness"
], ],
"css/css-position/position-sticky-stacking-context-ref.html": [ "css/css-position/position-sticky-stacking-context-ref.html": [
"dd6e5d4734c924c1ad08d14db986fb89d7cb03f6", "dd6e5d4734c924c1ad08d14db986fb89d7cb03f6",
@ -502000,21 +501954,13 @@
"bfd49209889fc14cae5af8d7c5e7990fbde451ec", "bfd49209889fc14cae5af8d7c5e7990fbde451ec",
"testharness" "testharness"
], ],
"css/css-position/position-sticky-transforms-ref.html": [
"b01ae263ac6b712912ba2af06edbaeaf75ba0215",
"support"
],
"css/css-position/position-sticky-transforms-translate-ref.html": [
"49d0db4c6b27c9f66bd58f5a075d024cbeaeb076",
"support"
],
"css/css-position/position-sticky-transforms-translate.html": [ "css/css-position/position-sticky-transforms-translate.html": [
"71bdb184c1ad2d1405f683e05a5b4117c8c7362a", "3fe5eb51028f4036f287601e3861dbd1377c588f",
"reftest" "testharness"
], ],
"css/css-position/position-sticky-transforms.html": [ "css/css-position/position-sticky-transforms.html": [
"c3d2c2b167bcf6b8e7c45b90d9a797a216c27632", "1b273a96f9f47c4bbbee71f41a2ceb046f7e9425",
"reftest" "testharness"
], ],
"css/css-position/position-sticky-writing-modes-ref.html": [ "css/css-position/position-sticky-writing-modes-ref.html": [
"407a1831479ccca61f6f7b268abcbf97f667f0bf", "407a1831479ccca61f6f7b268abcbf97f667f0bf",
@ -519041,7 +518987,7 @@
"testharness" "testharness"
], ],
"css/css-typed-om/the-stylepropertymap/computed/computed.tentative.html": [ "css/css-typed-om/the-stylepropertymap/computed/computed.tentative.html": [
"460bdf5cc3e35360eb8d910274cef440837f0eaf", "23845856ad2da4422dc081dd8f4cc1a5d6aecfe3",
"testharness" "testharness"
], ],
"css/css-typed-om/the-stylepropertymap/computed/get-invalid.html": [ "css/css-typed-om/the-stylepropertymap/computed/get-invalid.html": [
@ -519056,10 +519002,6 @@
"2c9c39367ccf02f014b8bf62057f810a70ff58bf", "2c9c39367ccf02f014b8bf62057f810a70ff58bf",
"testharness" "testharness"
], ],
"css/css-typed-om/the-stylepropertymap/computed/getProperties.tentative.html": [
"9e0018aed64fbca86d25f5ec466f88212ebd4906",
"testharness"
],
"css/css-typed-om/the-stylepropertymap/computed/has.tentative.html": [ "css/css-typed-om/the-stylepropertymap/computed/has.tentative.html": [
"b7b16ab44745e235883303b8c495aa1ceb874d0c", "b7b16ab44745e235883303b8c495aa1ceb874d0c",
"testharness" "testharness"
@ -519077,7 +519019,7 @@
"testharness" "testharness"
], ],
"css/css-typed-om/the-stylepropertymap/declared/declared.tentative.html": [ "css/css-typed-om/the-stylepropertymap/declared/declared.tentative.html": [
"7a6bbc1b04f6d1ee82332ddbf58a27604645a86b", "9ea176173ce999d17f26a80fc05b608532e471e0",
"testharness" "testharness"
], ],
"css/css-typed-om/the-stylepropertymap/declared/delete-invalid.html": [ "css/css-typed-om/the-stylepropertymap/declared/delete-invalid.html": [
@ -519100,10 +519042,6 @@
"df90a13e37ebcdbcf046e66f7e90ad2fc7edd9df", "df90a13e37ebcdbcf046e66f7e90ad2fc7edd9df",
"testharness" "testharness"
], ],
"css/css-typed-om/the-stylepropertymap/declared/getProperties.tentative.html": [
"7cb65bf76de24fe33a48178f5193bc05c5ec3f44",
"testharness"
],
"css/css-typed-om/the-stylepropertymap/declared/has.tentative.html": [ "css/css-typed-om/the-stylepropertymap/declared/has.tentative.html": [
"3299b5537f2d535988f2f6dac65b3eaba63833b2", "3299b5537f2d535988f2f6dac65b3eaba63833b2",
"testharness" "testharness"
@ -519144,10 +519082,6 @@
"2a8c50a83cbfd1c3da5614daac7070df0f5c83ff", "2a8c50a83cbfd1c3da5614daac7070df0f5c83ff",
"testharness" "testharness"
], ],
"css/css-typed-om/the-stylepropertymap/inline/getProperties.tentative.html": [
"5187e1eb4fb23b6b9a7fba8130d2649c4ea9152e",
"testharness"
],
"css/css-typed-om/the-stylepropertymap/inline/has.tentative.html": [ "css/css-typed-om/the-stylepropertymap/inline/has.tentative.html": [
"89b6d05db8717f1bc1f82690706d849a57104ec7", "89b6d05db8717f1bc1f82690706d849a57104ec7",
"testharness" "testharness"
@ -519209,7 +519143,7 @@
"testharness" "testharness"
], ],
"css/css-typed-om/the-stylepropertymap/properties/resources/testsuite.js": [ "css/css-typed-om/the-stylepropertymap/properties/resources/testsuite.js": [
"3ac46f9655cf07f63761850d38eb0c0cebdd98bf", "8f23e63849f184553cdc43d2c45aec316300db0a",
"support" "support"
], ],
"css/css-typed-om/the-stylepropertymap/properties/right.html": [ "css/css-typed-om/the-stylepropertymap/properties/right.html": [
@ -543641,7 +543575,7 @@
"support" "support"
], ],
"domparsing/XMLSerializer-serializeToString.html": [ "domparsing/XMLSerializer-serializeToString.html": [
"71314752b8552c19b0951647594b9c5c85ca01b6", "0ff2295477d3d2d690b19976eceae8a8a79720fe",
"testharness" "testharness"
], ],
"domparsing/createContextualFragment.html": [ "domparsing/createContextualFragment.html": [
@ -546673,7 +546607,7 @@
"support" "support"
], ],
"feature-policy/picture-in-picture-default-feature-policy.https.sub.html": [ "feature-policy/picture-in-picture-default-feature-policy.https.sub.html": [
"3d493f9c8c024c4f97a5f0ae3d12977f8719bcc6", "49c95204e8d3478f6c4509740fa0ed605146ca36",
"testharness" "testharness"
], ],
"feature-policy/picture-in-picture-disabled-by-feature-policy.https.sub.html": [ "feature-policy/picture-in-picture-disabled-by-feature-policy.https.sub.html": [
@ -547629,7 +547563,7 @@
"testharness" "testharness"
], ],
"fetch/corb/README.md": [ "fetch/corb/README.md": [
"5dd841770382cd2f6f1a09dca1103ef146bc912a", "95e1cbd716945e9cc53297243af4cec1ac35493e",
"support" "support"
], ],
"fetch/corb/img-html-correctly-labeled.sub-expected.html": [ "fetch/corb/img-html-correctly-labeled.sub-expected.html": [
@ -547732,10 +547666,18 @@
"41e260e7df49e0e4ddb1fc5df11913dbda15edd7", "41e260e7df49e0e4ddb1fc5df11913dbda15edd7",
"support" "support"
], ],
"fetch/corb/resources/subframe-that-posts-html-containing-blob-url-to-parent.html": [
"b9cd3923eeb88157f3bc3f2e62b5ee5e3f166c0c",
"support"
],
"fetch/corb/script-html-correctly-labeled.tentative.sub.html": [ "fetch/corb/script-html-correctly-labeled.tentative.sub.html": [
"71cb97517821177aa1c1d116f830cd2315963d18", "71cb97517821177aa1c1d116f830cd2315963d18",
"testharness" "testharness"
], ],
"fetch/corb/script-html-via-cross-origin-blob-url.sub.html": [
"5fd4e36f6ad89ea7e7ede4c1a54136d80b126dd4",
"testharness"
],
"fetch/corb/script-js-mislabeled-as-html-nosniff.sub.html": [ "fetch/corb/script-js-mislabeled-as-html-nosniff.sub.html": [
"ff421a4a827db6c8eeec97d2a7ee7010fd8fd686", "ff421a4a827db6c8eeec97d2a7ee7010fd8fd686",
"testharness" "testharness"
@ -559400,6 +559342,10 @@
"33bf062c202724a1ca7cd6db052614317acba1f8", "33bf062c202724a1ca7cd6db052614317acba1f8",
"manual" "manual"
], ],
"html/semantics/embedded-content/media-elements/autoplay-with-broken-track.html": [
"98f06519aeb619c9cf1db9ab3d94e987dffa182a",
"testharness"
],
"html/semantics/embedded-content/media-elements/contains.json": [ "html/semantics/embedded-content/media-elements/contains.json": [
"c4e395b396f7f8d9aac3e687d34c2df47dde5589", "c4e395b396f7f8d9aac3e687d34c2df47dde5589",
"support" "support"
@ -588277,7 +588223,7 @@
"testharness" "testharness"
], ],
"service-workers/service-worker/embed-and-object-are-not-intercepted.https.html": [ "service-workers/service-worker/embed-and-object-are-not-intercepted.https.html": [
"a9f08518e369c81036ce0815aab3f1a6895fb6ab", "04b303491f89d1ec808548008aaa0b0747ab8672",
"testharness" "testharness"
], ],
"service-workers/service-worker/extendable-event-async-waituntil.https.html": [ "service-workers/service-worker/extendable-event-async-waituntil.https.html": [
@ -588977,7 +588923,7 @@
"support" "support"
], ],
"service-workers/service-worker/resources/echo-content.py": [ "service-workers/service-worker/resources/echo-content.py": [
"d22a9fe2b426436a8e4901d82ca68833eeb905b3", "5a681fb53d70cf71d8d9ba13348025bee019ee1d",
"support" "support"
], ],
"service-workers/service-worker/resources/echo-message-to-source-worker.js": [ "service-workers/service-worker/resources/echo-message-to-source-worker.js": [
@ -588985,11 +588931,15 @@
"support" "support"
], ],
"service-workers/service-worker/resources/embed-and-object-are-not-intercepted-worker.js": [ "service-workers/service-worker/resources/embed-and-object-are-not-intercepted-worker.js": [
"30d39c9d08dff37858a1ba843d7b51aaa559238b", "ce5630c7ed59cfd0c0bd2e8e0e1bd596260e4e39",
"support"
],
"service-workers/service-worker/resources/embed-image-is-not-intercepted-iframe.html": [
"686ef2a4531b7631bee58a2450c62a7532c6c8aa",
"support" "support"
], ],
"service-workers/service-worker/resources/embed-is-not-intercepted-iframe.html": [ "service-workers/service-worker/resources/embed-is-not-intercepted-iframe.html": [
"e33a11c8d4a2a14693f8eb5eb5feb8218c92de6f", "e810456ed08cfa3be1730291b28b0f8134c093ed",
"support" "support"
], ],
"service-workers/service-worker/resources/embedded-content-from-server.html": [ "service-workers/service-worker/resources/embedded-content-from-server.html": [
@ -589444,6 +589394,10 @@
"ec72a4c120ccfac3a165576f59a0e02b945343b3", "ec72a4c120ccfac3a165576f59a0e02b945343b3",
"support" "support"
], ],
"service-workers/service-worker/resources/object-image-is-not-intercepted-iframe.html": [
"79aa49d7287ce90d72d4cb10511e3927d5f0a664",
"support"
],
"service-workers/service-worker/resources/object-is-not-intercepted-iframe.html": [ "service-workers/service-worker/resources/object-is-not-intercepted-iframe.html": [
"412591c247a38b07cde7f42248bc412a50128b54", "412591c247a38b07cde7f42248bc412a50128b54",
"support" "support"
@ -597697,11 +597651,11 @@
"support" "support"
], ],
"webdriver/tests/element_click/bubbling.py": [ "webdriver/tests/element_click/bubbling.py": [
"5437f5ca12b93e9a0087e9b341d63af6bace4ffd", "dde7ca3d7b8ee65222af0044f07087d3b0725bb9",
"wdspec" "wdspec"
], ],
"webdriver/tests/element_click/select.py": [ "webdriver/tests/element_click/select.py": [
"bbce720456bd6bae72ad256b56b7743be85959f3", "bddc341a0feb0d06e75415b8f98b8e9e0c2a829d",
"wdspec" "wdspec"
], ],
"webdriver/tests/element_click/stale.py": [ "webdriver/tests/element_click/stale.py": [
@ -597761,7 +597715,7 @@
"support" "support"
], ],
"webdriver/tests/execute_script/cyclic.py": [ "webdriver/tests/execute_script/cyclic.py": [
"cbebfbd2413ea0b10f547ab66fcc7159898e684a", "9d8a28b94b8cdac88650b675cb00bf21261444e8",
"wdspec" "wdspec"
], ],
"webdriver/tests/execute_script/user_prompts.py": [ "webdriver/tests/execute_script/user_prompts.py": [
@ -597769,7 +597723,7 @@
"wdspec" "wdspec"
], ],
"webdriver/tests/fullscreen_window.py": [ "webdriver/tests/fullscreen_window.py": [
"817011a8cdff7cfd7e445fb8ecb84e5d91f03993", "67cc7d992d81a176297038f5516c8a9c95018040",
"wdspec" "wdspec"
], ],
"webdriver/tests/get_window_rect.py": [ "webdriver/tests/get_window_rect.py": [

View file

@ -1,4 +1,4 @@
[display-012.xht] [display-012.xht]
type: reftest type: reftest
expected: expected:
if os == "mac": FAIL # Fonts on OSX make this gain an additional pixel of background (also fails on Firefox) if os == "mac": FAIL

View file

@ -0,0 +1,2 @@
[attachment-local-clipping-color-4.html]
expected: FAIL

View file

@ -0,0 +1,4 @@
[script-html-via-cross-origin-blob-url.sub.html]
[Untitled]
expected: FAIL

View file

@ -0,0 +1,5 @@
[autoplay-with-broken-track.html]
expected: TIMEOUT
[<video autoplay> with <track> child]
expected: TIMEOUT

View file

@ -0,0 +1,98 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>Append child writable stream demo</title>
<script src="transforms/transform-stream-polyfill.js"></script>
<script src="transforms/text-encode-transform.js"></script>
<script src="transforms/parse-json.js"></script>
<script src="transforms/split-stream.js"></script>
<script src="resources/highlight.pack.js"></script>
<script src="resources/web-animations.min.js"></script>
<script src="tags/view-source.js"></script>
<link rel=stylesheet href="resources/common.css">
<link rel=stylesheet href="resources/commits.css">
<h1>Append child writable stream demo</h1>
<a id=back-to-index href=index.html>Back to demo index</a>
<view-source></view-source>
<div id=buttons>
<button id="load">Load JSON stream</button>
<button id="reset">Reset</button>
</div>
<div id=target></div>
<script>
'use strict';
const loadButton = document.querySelector('#load');
const resetButton = document.querySelector('#reset');
const targetDiv = document.querySelector('#target');
const FIELDS = ['Hash', 'Date', 'Author', 'Subject'];
const FIELDS_LOWERCASE = FIELDS.map(string => string.toLowerCase());
function createTable(parentElement) {
const table = document.createElement('table');
table.id = 'commits';
const tr = document.createElement('tr');
for (const heading of FIELDS) {
const th = document.createElement('th');
th.textContent = heading;
tr.appendChild(th);
}
table.appendChild(tr);
parentElement.appendChild(table);
return table;
}
// BEGIN SOURCE TO VIEW
function appendChildWritableStream(parentNode) {
return new WritableStream({
write(chunk) {
parentNode.appendChild(chunk);
}
});
}
async function fetchThenJSONToDOM() {
const jsonToElementTransform = new TransformStream({
transform(chunk, controller) {
const tr = document.createElement('tr');
for (const cell of FIELDS_LOWERCASE) {
const td = document.createElement('td');
td.textContent = chunk[cell];
tr.appendChild(td);
}
controller.enqueue(tr);
}
});
const response = await fetch('data/commits.json',
{
mode: 'same-origin',
headers: {
'Cache-Control': 'no-cache, no-store'
}
});
const table = createTable(targetDiv);
return response.body
.pipeThrough(new TextDecoder())
.pipeThrough(splitStream('\n'))
.pipeThrough(parseJSON())
.pipeThrough(jsonToElementTransform)
.pipeTo(appendChildWritableStream(table));
}
// END SOURCE TO VIEW
loadButton.onclick = () => {
loadButton.disabled = true;
setTimeout(fetchThenJSONToDOM, 0);
};
resetButton.onclick = () => {
targetDiv.innerHTML = '';
loadButton.disabled = false;
};
</script>

View file

@ -0,0 +1,60 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>Streaming element with backpressure demo</title>
<script src="transforms/transform-stream-polyfill.js"></script>
<script src="transforms/text-encode-transform.js"></script>
<script src="tags/streaming-element-backpressure.js"></script>
<script src="resources/highlight.pack.js"></script>
<script src="resources/web-animations.min.js"></script>
<script src="tags/view-source.js"></script>
<link rel=stylesheet href="resources/common.css">
<link rel=stylesheet href="resources/jank-meter.css">
<link rel=stylesheet href="resources/commits.css">
<h1>Streaming element with backpressure demo</h1>
<a id=back-to-index href=index.html>Back to demo index</a>
<view-source></view-source>
<div id=buttons>
<button id="load">Load HTML stream, applying backpressure</button>
<button id="reset">Reset</button>
</div>
<div id=jank-meter>JANK METER</div>
<streaming-element-backpressure id=target></streaming-element-backpressure>
<script>
'use strict';
const loadButton = document.querySelector('#load');
const resetButton = document.querySelector('#reset');
const targetDiv = document.querySelector('#target');
loadButton.onclick = async () => {
loadButton.disabled = true;
fetchDirectlyIntoDOM();
};
// BEGIN SOURCE TO VIEW
async function fetchDirectlyIntoDOM() {
const response = await fetch('data/commits.include',
{
mode: 'same-origin',
headers: {
'Cache-Control': 'no-cache, no-store'
}
});
await response.body
.pipeThrough(new TextDecoder())
.pipeTo(targetDiv.writable);
}
// END SOURCE TO VIEW
resetButton.onclick = () => {
targetDiv.reset();
loadButton.disabled = false;
};
</script>

View file

@ -0,0 +1,60 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>Streaming element demo</title>
<script src="transforms/transform-stream-polyfill.js"></script>
<script src="transforms/text-encode-transform.js"></script>
<script src="tags/streaming-element.js"></script>
<script src="resources/highlight.pack.js"></script>
<script src="resources/web-animations.min.js"></script>
<script src="tags/view-source.js"></script>
<link rel=stylesheet href="resources/common.css">
<link rel=stylesheet href="resources/jank-meter.css">
<link rel=stylesheet href="resources/commits.css">
<h1>Streaming element demo</h1>
<a id=back-to-index href=index.html>Back to demo index</a>
<view-source></view-source>
<div id=buttons>
<button id="load">Load HTML stream</button>
<button id="reset">Reset</button>
</div>
<div id=jank-meter>JANK METER</div>
<streaming-element id=target></streaming-element>
<script>
'use strict';
const loadButton = document.querySelector('#load');
const resetButton = document.querySelector('#reset');
const targetDiv = document.querySelector('#target');
// BEGIN SOURCE TO VIEW
async function streamDirectlyIntoDOM() {
const response = await fetch('data/commits.include',
{
mode: 'same-origin',
headers: {
'Cache-Control': 'no-cache, no-store'
}
});
await response.body
.pipeThrough(new TextDecoder())
.pipeTo(targetDiv.writable);
}
loadButton.onclick = () => {
loadButton.disabled = true;
streamDirectlyIntoDOM();
};
// END SOURCE TO VIEW
resetButton.onclick = () => {
targetDiv.reset();
loadButton.disabled = false;
};
</script>

View file

@ -28,7 +28,7 @@ registerPaint('geometry', class {
]; ];
} }
paint(ctx, geom, styleMap) { paint(ctx, geom, styleMap) {
const properties = styleMap.getProperties().sort(); const properties = [...styleMap.keys()].sort();
var serializedStrings = []; var serializedStrings = [];
for (let i = 0; i < properties.length; i++) { for (let i = 0; i < properties.length; i++) {
const value = styleMap.get(properties[i]); const value = styleMap.get(properties[i]);

View file

@ -29,7 +29,7 @@ registerPaint('geometry', class {
]; ];
} }
paint(ctx, geom, styleMap) { paint(ctx, geom, styleMap) {
const properties = styleMap.getProperties().sort(); const properties = [...styleMap.keys()].sort();
var serializedStrings = []; var serializedStrings = [];
for (let i = 0; i < properties.length; i++) { for (let i = 0; i < properties.length; i++) {
const value = styleMap.get(properties[i]); const value = styleMap.get(properties[i]);

View file

@ -32,7 +32,7 @@ registerPaint('geometry', class {
]; ];
} }
paint(ctx, geom, styleMap) { paint(ctx, geom, styleMap) {
const properties = styleMap.getProperties().sort(); const properties = [...styleMap.keys()].sort();
var serializedStrings = []; var serializedStrings = [];
for (let i = 0; i < properties.length; i++) { for (let i = 0; i < properties.length; i++) {
const value = styleMap.get(properties[i]); const value = styleMap.get(properties[i]);

View file

@ -28,7 +28,7 @@ registerPaint('geometry', class {
]; ];
} }
paint(ctx, geom, styleMap) { paint(ctx, geom, styleMap) {
const properties = styleMap.getProperties().sort(); const properties = [...styleMap.keys()].sort();
var serializedStrings = []; var serializedStrings = [];
for (let i = 0; i < properties.length; i++) { for (let i = 0; i < properties.length; i++) {
const value = styleMap.get(properties[i]); const value = styleMap.get(properties[i]);

View file

@ -1,42 +0,0 @@
<!DOCTYPE html>
<title>Reference for position:sticky elements should not affect the flow position of other elements</title>
<style>
.scroller {
height: 200px;
width: 100px;
overflow-y: scroll;
margin-bottom: 15px;
}
.sticky {
background-color: green;
}
.box {
height: 50px;
width: 50px;
}
.before {
background-color: fuchsia;
}
.after {
background-color: orange;
}
.padding {
height: 450px;
}
</style>
<div class="scroller">
<div class="before box"></div>
<div class="box"></div>
<div class="after box"></div>
<div class="sticky box"></div>
<div class="padding"></div>
</div>
<div>You should see a fuchsia box, a one-box gap, an orange box, and then a green box above.</div>

View file

@ -1,46 +1,58 @@
<!DOCTYPE html> <!DOCTYPE html>
<title>position:sticky elements should not affect the flow position of other elements</title> <title>position:sticky elements should not affect the flow position of other elements</title>
<link rel="match" href="position-sticky-inflow-position-ref.html" />
<link rel="help" href="https://www.w3.org/TR/css-position-3/#sticky-pos" /> <link rel="help" href="https://www.w3.org/TR/css-position-3/#sticky-pos" />
<meta name="assert" content="This test checks that position:sticky elements do not affect the flow position of other elements" /> <meta name="assert" content="This test checks that position:sticky elements do not affect the flow position of other elements" />
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<style> <style>
.scroller { .scroller {
position: relative;
height: 200px; height: 200px;
width: 100px; width: 100px;
overflow-y: scroll; overflow: scroll;
margin-bottom: 15px;
} }
.sticky { #sticky {
background-color: green; background-color: green;
position: sticky; position: sticky;
top: 150px; top: 150px;
} }
.box { #before {
height: 50px;
width: 50px;
}
.before {
background-color: fuchsia; background-color: fuchsia;
} }
.after { #after {
background-color: orange; background-color: orange;
} }
.box {
height: 50px;
width: 50px;
}
.padding { .padding {
height: 500px; height: 500px;
} }
</style> </style>
<div class="scroller"> <div class="scroller">
<div class="before box"></div> <div id="before" class="box"></div>
<div class="sticky box"></div> <div id="sticky" class="box"></div>
<div class="after box"></div> <div id="after" class="box"></div>
<div class="padding"></div> <div class="padding"></div>
</div> </div>
<div>You should see a fuchsia box, a one-box gap, an orange box, and then a green box above.</div> <script>
test(() => {
// The sticky element is pushed to be stuck 150 pixels from the top.
assert_equals(sticky.offsetTop, 150);
// Neither 'before' or 'after' should be affected by the change in the sticky
// element's location.
assert_equals(before.offsetTop, 0);
assert_equals(after.offsetTop, before.clientHeight + sticky.clientHeight);
}, 'sticky offset should not affect the position of other elements.');
</script>

View file

@ -1,63 +0,0 @@
<!DOCTYPE html>
<title>Reference for position:sticky elements should properly interact with margins</title>
<style>
.group {
display: inline-block;
position: relative;
width: 180px;
height: 400px;
}
.scroller {
width: 150px;
height: 300px;
overflow-y: scroll;
overflow-x: hidden;
}
.indicator {
position: relative;
background-color: green;
margin: 15px;
}
.box {
width: 100px;
height: 100px;
}
.padding {
height: 385px;
}
</style>
<script>
window.addEventListener('load', function() {
document.getElementById('scroller1').scrollTop = 0;
document.getElementById('scroller2').scrollTop = 60;
document.getElementById('scroller3').scrollTop = 120;
});
</script>
<div class="group">
<div id="scroller1" class="scroller">
<div class="indicator box" style="top: 0;"></div>
<div class="padding"></div>
</div>
</div>
<div class="group">
<div id="scroller2" class="scroller">
<div class="indicator box" style="top: 50px;"></div>
<div class="padding"></div>
</div>
</div>
<div class="group">
<div id="scroller3" class="scroller">
<div class="indicator box" style="top: 85px;"></div>
<div class="padding"></div>
</div>
</div>
<div>You should see three green boxes above. No red should be visible.</div>

View file

@ -1,92 +1,49 @@
<!DOCTYPE html> <!DOCTYPE html>
<title>position:sticky elements should properly interact with margins</title> <title>position:sticky elements should properly interact with margins</title>
<link rel="match" href="position-sticky-margins-ref.html" />
<link rel="help" href="https://www.w3.org/TR/css-position-3/#sticky-pos" /> <link rel="help" href="https://www.w3.org/TR/css-position-3/#sticky-pos" />
<meta name="assert" content="position:sticky elements should ignore margins when sticking, but consider them when making sure sticky elements do not escape their containing block" /> <meta name="assert" content="position:sticky elements should ignore margins when sticking, but consider them when making sure sticky elements do not escape their containing block" />
<style> <script src="/resources/testharness.js"></script>
.group { <script src="/resources/testharnessreport.js"></script>
display: inline-block;
position: relative;
width: 180px;
height: 400px;
}
.scroller { <script src="resources/sticky-util.js"></script>
position: relative;
width: 150px;
height: 300px;
overflow-y: scroll;
overflow-x: hidden;
}
.holder { <body></body>
width: 130px;
height: 200px;
}
.sticky {
position: sticky;
background-color: green;
top: 5px;
margin: 15px;
}
.indicator {
left: 15px;
position: absolute;
background-color: red;
}
.box {
width: 100px;
height: 100px;
}
.padding {
height: 300px;
}
</style>
<script> <script>
window.addEventListener('load', function() { test(() => {
document.getElementById('scroller1').scrollTop = 0; const elements = setupStickyTest('top', 50);
document.getElementById('scroller2').scrollTop = 60; elements.sticky.style.margin = '15px';
document.getElementById('scroller3').scrollTop = 120; elements.scroller.scrollTop = 100;
}); assert_equals(elements.sticky.offsetTop,
elements.container.offsetTop + elements.filler.clientHeight + 15);
}, 'Before sticking, the margin should be obeyed.');
test(() => {
const elements = setupStickyTest('top', 50);
elements.sticky.style.margin = '15px';
elements.scroller.scrollTop = 200;
// This math cancels to sticky.offsetTop == (scroller.scrollTop + 50), but
// for clarity the calculations are left explicit.
const nonStickyTopY = elements.container.offsetTop +
elements.filler.clientHeight;
const targetTopY = elements.scroller.scrollTop + 50;
const stickyOffset = targetTopY - nonStickyTopY;
assert_equals(elements.sticky.offsetTop, nonStickyTopY + stickyOffset);
}, 'Whilst stuck, the margin is irrelevant.');
test(() => {
const elements = setupStickyTest('top', 50);
elements.sticky.style.margin = '15px';
elements.scroller.scrollTop = 300;
const maxOffsetInContainer = elements.container.offsetTop +
elements.container.clientHeight - elements.sticky.clientHeight;
assert_equals(elements.sticky.offsetTop, maxOffsetInContainer - 15);
}, 'The margin is taken into account when making sure the sticky element ' +
'does not escape its container');
</script> </script>
<!-- Before sticking, the margin should be obeyed. -->
<div class="group">
<div id="scroller1" class="scroller">
<div class="indicator box" style="top: 15px;"></div>
<div class="holder">
<div class="sticky box"></div>
</div>
<div class="padding"></div>
</div>
</div>
<!-- Whilst stuck, the margin is irrelevant. -->
<div class="group">
<div id="scroller2" class="scroller">
<div class="indicator box" style="top: 65px;"></div>
<div class="holder">
<div class="sticky box"></div>
</div>
<div class="padding"></div>
</div>
</div>
<!-- The margin does count when making sure the sticky element does not escape
its containing block. -->
<div class="group">
<div id="scroller3" class="scroller">
<div class="indicator box" style="top: 100px;"></div>
<div class="holder">
<div class="sticky box"></div>
</div>
<div class="padding"></div>
</div>
</div>
<div>You should see three green boxes above. No red should be visible.</div>

View file

@ -1,69 +0,0 @@
<!DOCTYPE html>
<title>Reference for position:sticky elements should respect padding on their ancestor overflow element</title>
<style>
.group {
display: inline-block;
position: relative;
width: 150px;
height: 250px;
}
.scroller {
padding: 20px 0;
position: relative;
width: 100px;
height: 200px;
overflow-x: hidden;
overflow-y: auto;
}
.contents {
height: 500px;
}
.indicator {
background-color: green;
position: absolute;
left: 0;
}
.box {
width: 100%;
height: 100px;
}
</style>
<script>
window.addEventListener('load', function() {
document.getElementById('scroller1').scrollTop = 50;
document.getElementById('scroller2').scrollTop = 175;
document.getElementById('scroller3').scrollTop = 220;
});
</script>
<div class="group">
<div id="scroller1" class="scroller">
<div class="contents">
<div class="indicator box" style="top: 170px;"></div>
</div>
</div>
</div>
<div class="group">
<div id="scroller2" class="scroller">
<div class="contents">
<div class="indicator box" style="top: 195px;"></div>
</div>
</div>
</div>
<div class="group">
<div id="scroller3" class="scroller">
<div class="contents">
<div class="indicator box" style="top: 220px;"></div>
</div>
</div>
</div>
<div>You should see three green boxes above. No red should be visible.</div>

View file

@ -1,106 +1,54 @@
<!DOCTYPE html> <!DOCTYPE html>
<title>position:sticky elements should respect padding on their ancestor overflow element</title> <title>position:sticky elements should respect padding on their ancestor overflow element</title>
<link rel="match" href="position-sticky-overflow-padding-ref.html" />
<link rel="help" href="https://www.w3.org/TR/css-position-3/#sticky-pos" /> <link rel="help" href="https://www.w3.org/TR/css-position-3/#sticky-pos" />
<meta name="assert" content="This test checks that position:sticky elements respect padding on their ancestor overflow element" /> <meta name="assert" content="This test checks that position:sticky elements respect padding on their ancestor overflow element" />
<style> <script src="/resources/testharness.js"></script>
.group { <script src="/resources/testharnessreport.js"></script>
display: inline-block;
position: relative;
width: 150px;
height: 250px;
}
.scroller { <script src="resources/sticky-util.js"></script>
/* The target sticky position should be offset by this padding. */
padding: 20px 0;
position: relative;
width: 100px;
height: 200px;
overflow-x: hidden;
overflow-y: auto;
}
.contents { <body></body>
height: 500px;
}
.prepadding {
height: 100px;
}
.container {
height: 200px;
}
.innerpadding {
height: 50px;
}
.indicator {
background-color: red;
position: absolute;
left: 0;
}
.sticky {
background-color: green;
position: sticky;
top: 0;
}
.box {
width: 100%;
height: 100px;
}
</style>
<script> <script>
window.addEventListener('load', function() { test(() => {
document.getElementById('scroller1').scrollTop = 50; const elements = setupStickyTest('top', 50);
document.getElementById('scroller2').scrollTop = 175; elements.scroller.style.padding = '20px 0';
document.getElementById('scroller3').scrollTop = 220;
}); // Before sticking; the element isn't within the padding range.
elements.scroller.scrollTop = 150;
const nonStickyTopY = elements.container.offsetTop +
elements.filler.clientHeight;
assert_equals(elements.sticky.offsetTop, nonStickyTopY);
}, 'A sticky element should not be affected by ancestor padding until it ' +
'reaches it');
test(() => {
const elements = setupStickyTest('top', 50);
elements.sticky.style.top = '0';
elements.scroller.style.padding = '20px 0';
elements.scroller.scrollTop = 200;
// This math cancels to sticky.offsetTop == (scroller.scrollTop + 50), but
// for clarity the calculations are left explicit.
const nonStickyTopY = elements.container.offsetTop +
elements.filler.clientHeight;
const targetTopY = elements.scroller.scrollTop;
const stickyOffset = targetTopY - nonStickyTopY;
assert_equals(elements.sticky.offsetTop, nonStickyTopY + stickyOffset + 20);
}, 'A sticky element should be offset by ancestor padding even when stuck');
test(() => {
const elements = setupStickyTest('top', 50);
elements.sticky.style.top = '0';
elements.scroller.style.padding = '20px 0';
elements.scroller.scrollTop = 315;
const maxOffsetInContainer = elements.container.offsetTop +
elements.container.clientHeight - elements.sticky.clientHeight;
assert_equals(elements.sticky.offsetTop, maxOffsetInContainer);
}, 'Ancestor overflow padding does not allow a sticky element to escape its ' +
'container');
</script> </script>
<div class="group">
<div id="scroller1" class="scroller">
<div class="indicator box" style="top: 170px;"></div>
<div class="contents">
<div class="prepadding"></div>
<div class="container">
<div class="innerpadding"></div>
<div class="sticky box"></div>
</div>
</div>
</div>
</div>
<div class="group">
<div id="scroller2" class="scroller">
<div class="indicator box" style="top: 195px;"></div>
<div class="contents">
<div class="prepadding"></div>
<div class="container">
<div class="innerpadding"></div>
<div class="sticky box"></div>
</div>
</div>
</div>
</div>
<div class="group">
<div id="scroller3" class="scroller">
<div class="indicator box" style="top: 220px;"></div>
<div class="contents">
<div class="prepadding"></div>
<div class="container">
<div class="innerpadding"></div>
<div class="sticky box"></div>
</div>
</div>
</div>
</div>
<div>You should see three green boxes above. No red should be visible.</div>

View file

@ -1,30 +0,0 @@
<!DOCTYPE html>
<title>Reference for position:sticky should operate correctly for the root scroller</title>
<style>
body {
/* Assumption: 3000px is taller than any user agents test window size. */
height: 3000px;
}
.indicator {
background-color: green;
position: absolute;
top: 750px;
}
.box {
width: 200px;
height: 200px;
}
</style>
<script>
window.addEventListener('load', function() {
window.scrollTo(0, 700);
});
</script>
<div class="indicator box"></div>
<div style="position: absolute; top: 1000px;">You should see a green box above. No red should be visible.</div>

View file

@ -1,39 +1,31 @@
<!DOCTYPE html> <!DOCTYPE html>
<title>position:sticky should operate correctly for the root scroller</title> <title>position:sticky should operate correctly for the root scroller</title>
<link rel="match" href="position-sticky-root-scroller-ref.html" />
<link rel="help" href="https://www.w3.org/TR/css-position-3/#sticky-pos" /> <link rel="help" href="https://www.w3.org/TR/css-position-3/#sticky-pos" />
<meta name="assert" content="This test checks that position:sticky elements work when using the root (document) scroller" /> <meta name="assert" content="This test checks that position:sticky elements work when using the root (document) scroller" />
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<style> <style>
body { body {
/* Assumption: 3000px is taller than any user agents test window size. */ /* Assumption: 3000px is taller than any user agents test window size. */
height: 3000px; height: 3000px;
} }
.indicator { #sticky {
background-color: red;
position: absolute;
}
.sticky {
background-color: green;
position: sticky; position: sticky;
top: 50px; top: 50px;
}
.box {
width: 200px; width: 200px;
height: 200px; height: 200px;
background-color: green;
} }
</style> </style>
<div id="sticky"></div>
<script> <script>
window.addEventListener('load', function() { test(() => {
window.scrollTo(0, 700); window.scrollTo(0, 700);
}); assert_equals(sticky.offsetTop, 700 + 50);
}, 'Sticky elements work with the root (document) scroller');
</script> </script>
<div class="indicator box" style="top: 750px;"></div>
<div class="sticky box"></div>
<div style="position: absolute; top: 1000px;">You should see a green box above. No red should be visible.</div>

View file

@ -1,81 +0,0 @@
<!DOCTYPE html>
<title>Reference for transforms on position:sticky elements should apply after sticking</title>
<style>
.group {
display: inline-block;
position: relative;
width: 150px;
height: 250px;
}
.scroller {
position: relative;
width: 100px;
height: 200px;
overflow-x: hidden;
overflow-y: auto;
}
.contents {
height: 500px;
}
.indicator {
background-color: green;
position: relative;
}
.box {
width: 100%;
height: 50px;
}
.rotated {
transform: rotateX(60deg);
height: 100px;
width: 100%;
}
.perspective {
transform: perspective(3px) translateZ(1px);
height: 50px;
width: 50px;
}
</style>
<script>
window.addEventListener('load', function() {
document.getElementById('scroller1').scrollTop = 50;
document.getElementById('scroller2').scrollTop = 50;
document.getElementById('scroller3').scrollTop = 50;
});
</script>
<div class="group">
<div id="scroller1" class="scroller">
<div class="contents">
<div class="indicator box" style="height: 100px; top: 75px;"></div>
</div>
</div>
</div>
<div class="group">
<div id="scroller2" class="scroller">
<div class="contents">
<div class="rotated indicator" style="top: 100px;"></div>
</div>
</div>
</div>
<div class="group">
<div id="scroller3" class="scroller">
<!-- Required for blending. -->
<div class="perspective" style="position: absolute; background: red; top: 100px;"></div>
<div class="contents">
<div class="perspective indicator" style="top: 100px;"></div>
</div>
</div>
</div>
<div>You should see three green boxes above. No red should be visible.</div>

View file

@ -1,67 +0,0 @@
<!DOCTYPE html>
<title>Reference for translations on position:sticky elements should apply after sticking</title>
<style>
.group {
display: inline-block;
position: relative;
width: 150px;
height: 250px;
}
.scroller {
position: relative;
width: 100px;
height: 200px;
overflow-x: hidden;
overflow-y: auto;
}
.contents {
height: 500px;
}
.indicator {
background-color: green;
position: relative;
}
.box {
width: 100%;
height: 50px;
}
</style>
<script>
window.addEventListener('load', function() {
document.getElementById('scroller1').scrollTop = 50;
document.getElementById('scroller2').scrollTop = 70;
document.getElementById('scroller3').scrollTop = 50;
});
</script>
<div class="group">
<div id="scroller1" class="scroller">
<div class="contents">
<div class="indicator box" style="top: 50px;"></div>
</div>
</div>
</div>
<div class="group">
<div id="scroller2" class="scroller">
<div class="contents">
<div class="indicator box" style="top: 50px;"></div>
</div>
</div>
</div>
<div class="group">
<div id="scroller3" class="scroller">
<div class="contents">
<div class="indicator box" style="top: 200px;"></div>
</div>
</div>
</div>
<div>You should see three green boxes above. No red should be visible.</div>

View file

@ -1,92 +1,45 @@
<!DOCTYPE html> <!DOCTYPE html>
<title>translations on position:sticky elements should apply after sticking</title> <title>translations on position:sticky elements should apply after sticking</title>
<link rel="match" href="position-sticky-transforms-translate-ref.html" />
<link rel="help" href="https://www.w3.org/TR/css-position-3/#sticky-pos" /> <link rel="help" href="https://www.w3.org/TR/css-position-3/#sticky-pos" />
<meta name="assert" content="This test checks that translations on position:sticky elements are carried out on their stuck position" /> <meta name="assert" content="This test checks that translations on position:sticky elements are carried out on their stuck position" />
<style> <script src="/resources/testharness.js"></script>
.group { <script src="/resources/testharnessreport.js"></script>
display: inline-block;
position: relative;
width: 150px;
height: 250px;
}
.scroller { <script src="resources/sticky-util.js"></script>
position: relative;
width: 100px;
height: 200px;
overflow-x: hidden;
overflow-y: auto;
}
.contents { <body style="margin: 0;"></body>
height: 500px;
}
.container {
height: 150px;
}
.indicator {
background-color: red;
position: absolute;
left: 0;
}
.sticky {
background-color: green;
position: sticky;
top: 50px;
}
.box {
width: 100%;
height: 50px;
}
</style>
<script> <script>
window.addEventListener('load', function() { test(() => {
document.getElementById('scroller1').scrollTop = 50; const elements = setupStickyTest('top', 50);
document.getElementById('scroller2').scrollTop = 70; elements.sticky.style.transform = 'translateY(-100%)';
document.getElementById('scroller3').scrollTop = 50; elements.scroller.scrollTop = 100;
}); // Transforms don't affect offsetTop, so use getBoundingClientRect.
assert_equals(elements.sticky.getBoundingClientRect().y,
elements.scroller.getBoundingClientRect().y);
}, 'Translation transform can move sticky element past sticking point');
test(() => {
const elements = setupStickyTest('top', 50);
elements.sticky.style.transform = 'translateY(50%)';
elements.scroller.scrollTop = 200;
// Transforms don't affect offsetTop, so use getBoundingClientRect.
const stickyElementOffset = elements.sticky.getBoundingClientRect().y -
elements.scroller.getBoundingClientRect().y;
assert_equals(stickyElementOffset, 100);
}, 'Stuck elements can still be moved via translations');
test(() => {
const elements = setupStickyTest('top', 50);
elements.container.style.transform = 'translateY(100px)';
elements.scroller.scrollTop = 200;
// Transforms don't affect offsetTop, so use getBoundingClientRect.
// Here the sticky element will originally have stuck at 50px from the top,
// but is then 'pulled' downwards by the 100px container transform.
const stickyElementOffset = elements.sticky.getBoundingClientRect().y -
elements.scroller.getBoundingClientRect().y;
assert_equals(stickyElementOffset, 150);
}, 'The sticky element should stick before the container is offset by a ' +
'translation');
</script> </script>
<div class="group">
<div id="scroller1" class="scroller">
<div class="indicator box" style="top: 50px;"></div>
<div class="contents">
<div class="container">
<div class="sticky box" style="transform: translateY(-100%);"></div>
</div>
</div>
</div>
</div>
<!-- The pre-transform sticky is not allowed to escape its containing block. -->
<div class="group">
<div id="scroller2" class="scroller">
<div class="indicator box" style="top: 50px;"></div>
<div class="contents">
<div class="container">
<div class="sticky box" style="transform: translateY(-100%);"></div>
</div>
</div>
</div>
</div>
<!-- The sticky element should stick before the container is transformed. -->
<div class="group">
<div id="scroller3" class="scroller">
<div class="indicator box" style="top: 200px;"></div>
<div class="contents">
<div class="container" style="transform: translateY(100px);">
<div class="sticky box"></div>
</div>
</div>
</div>
</div>
<div>You should see three green boxes above. No red should be visible.</div>

View file

@ -1,101 +1,49 @@
<!DOCTYPE html> <!DOCTYPE html>
<title>transforms on position:sticky elements should apply after sticking</title> <title>transforms on position:sticky elements should apply after sticking</title>
<link rel="match" href="position-sticky-transforms-ref.html" />
<link rel="help" href="https://www.w3.org/TR/css-position-3/#sticky-pos" /> <link rel="help" href="https://www.w3.org/TR/css-position-3/#sticky-pos" />
<meta name="assert" content="This test checks that transforms on position:sticky elements are carried out on their stuck position" /> <meta name="assert" content="This test checks that transforms on position:sticky elements are carried out on their stuck position" />
<style> <script src="/resources/testharness.js"></script>
.group { <script src="/resources/testharnessreport.js"></script>
display: inline-block;
position: relative;
width: 150px;
height: 250px;
}
.scroller { <script src="resources/sticky-util.js"></script>
position: relative;
width: 100px;
height: 200px;
overflow-x: hidden;
overflow-y: auto;
}
.contents { <body style="margin: 0;"></body>
height: 500px;
}
.container {
height: 150px;
}
.indicator {
background-color: red;
position: absolute;
}
.sticky {
background-color: green;
position: sticky;
top: 50px;
}
.box {
width: 100%;
height: 50px;
}
.rotated {
transform: rotateX(60deg);
width: 100%;
height: 100px;
}
.perspective {
transform: perspective(3px) translateZ(1px);
height: 50px;
width: 50px;
}
</style>
<script> <script>
window.addEventListener('load', function() { test(() => {
document.getElementById('scroller1').scrollTop = 50; const elements = setupStickyTest('top', 50);
document.getElementById('scroller2').scrollTop = 50; elements.sticky.style.transform = 'scale(2)';
document.getElementById('scroller3').scrollTop = 50; elements.scroller.scrollTop = 200;
});
// Transforms don't affect offsetTop, so use getBoundingClientRect.
// Scaling the sticky element by 2 means its top-y moves (1/2 * height)
// upwards, in this case placing it at the top of the viewport.
const boundingRect = elements.sticky.getBoundingClientRect();
assert_equals(boundingRect.y, elements.scroller.getBoundingClientRect().y);
}, 'Scale transforms are carried out on the stuck element position');
test(() => {
const elements = setupStickyTest('top', 50);
elements.sticky.style.transform = 'rotateX(60deg)';
elements.scroller.scrollTop = 200;
// Transforms don't affect offsetTop, so use getBoundingClientRect.
// Rotating around the x-axis essentially 'squashes' it (from the camera's
// viewpoint), in this case shifting the offset to 75 rather than 50.
const stickyElementOffset = elements.sticky.getBoundingClientRect().y -
elements.scroller.getBoundingClientRect().y;
assert_equals(stickyElementOffset, 75);
}, 'Rotate transforms are carried out on the stuck element position');
test(() => {
const elements = setupStickyTest('top', 50);
elements.sticky.style.transform = 'perspective(3px) translateZ(1px)';
elements.scroller.scrollTop = 200;
// Transforms don't affect offsetTop, so use getBoundingClientRect.
const stickyElementOffset = elements.sticky.getBoundingClientRect().y -
elements.scroller.getBoundingClientRect().y;
assert_equals(stickyElementOffset, 25);
}, 'Perspective transforms are carried out on the stuck element position');
</script> </script>
<div class="group">
<div id="scroller1" class="scroller">
<div class="indicator box" style="height: 100px; top: 75px;"></div>
<div class="contents">
<div class="container">
<div class="sticky box" style="transform: scale(2);"></div>
</div>
</div>
</div>
</div>
<div class="group">
<div id="scroller2" class="scroller">
<div class="rotated indicator" style="top: 100px;"></div>
<div class="contents">
<div class="container" style="height: 250px;">
<div class="rotated sticky"></div>
</div>
</div>
</div>
</div>
<div class="group">
<div id="scroller3" class="scroller">
<div class="perspective indicator" style="top: 100px;"></div>
<div class="contents">
<div class="container">
<div class="perspective sticky"></div>
</div>
</div>
</div>
</div>
<div>You should see three green boxes above. No red should be visible.</div>

View file

@ -15,11 +15,9 @@
const target = document.getElementById('target'); const target = document.getElementById('target');
const styleMap = target.computedStyleMap(); const styleMap = target.computedStyleMap();
// FIXME(crbug.com/788904): Test currently fails because the 'content' property returns
// incorrect initial CSS value.
test(() => { test(() => {
const computedStyle = [...getComputedStyle(target)].sort(); const computedStyle = [...getComputedStyle(target)].sort();
const properties = styleMap.getProperties(); const properties = [...styleMap.keys()];
// Two extra entries for custom properties // Two extra entries for custom properties
assert_equals(properties.length, computedStyle.length + 2); assert_equals(properties.length, computedStyle.length + 2);

View file

@ -1,18 +0,0 @@
<!doctype html>
<meta charset="utf-8">
<title>StylePropertyMap.getProperties tests</title>
<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-getproperties">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="../../resources/testhelper.js"></script>
<body>
<script>
'use strict';
test(t => {
const styleMap = createComputedStyleMap(t, '--A: A; width: 0px; --C: C; transition-duration: 1s, 2s; color: red; --B: B;');
const expectedProperties = [...getComputedStyle(document.body)].sort().concat('--A', '--B', '--C');
assert_array_equals(styleMap.getProperties(), expectedProperties);
}, 'StylePropertyMap.getProperties returns property names in correct order');
</script>

View file

@ -31,7 +31,7 @@ const target = document.getElementById('target');
const styleMap = document.styleSheets[0].rules[0].styleMap; const styleMap = document.styleSheets[0].rules[0].styleMap;
test(() => { test(() => {
const properties = styleMap.getProperties(); const properties = [...styleMap.keys()];
assert_array_equals(properties, ['height', 'transition-duration', 'width', '--foo']); assert_array_equals(properties, ['height', 'transition-duration', 'width', '--foo']);
}, 'Declared StylePropertyMap only contains properties in the style rule'); }, 'Declared StylePropertyMap only contains properties in the style rule');

View file

@ -1,23 +0,0 @@
<!doctype html>
<meta charset="utf-8">
<title>StylePropertyMap.getProperties tests</title>
<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-getproperties">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="../../resources/testhelper.js"></script>
<body>
<script>
'use strict';
test(t => {
const styleMap = createDeclaredStyleMap(t, '');
assert_array_equals(styleMap.getProperties(), []);
}, 'Calling StylePropertyMap.getProperties on an empty property model returns a zero-length array');
test(t => {
const styleMap = createDeclaredStyleMap(t, '--A: A; width: 0px; --C: C; transition-duration: 1s, 2s; color: red; --B: B;');
assert_array_equals(styleMap.getProperties(),
['color', 'transition-duration', 'width', '--A', '--B', '--C']);
}, 'StylePropertyMap.getProperties returns CSS properties in alphabetical order then custom properties by codepoint');
</script>

View file

@ -1,23 +0,0 @@
<!doctype html>
<meta charset="utf-8">
<title>StylePropertyMap.getProperties tests</title>
<link rel="help" href="https://drafts.css-houdini.org/css-typed-om-1/#dom-stylepropertymap-getproperties">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="../../resources/testhelper.js"></script>
<body>
<script>
'use strict';
test(t => {
const styleMap = createInlineStyleMap(t, '');
assert_array_equals(styleMap.getProperties(), []);
}, 'Calling StylePropertyMap.getProperties on an empty property model returns a zero-length array');
test(t => {
const styleMap = createInlineStyleMap(t, '--A: A; width: 0px; --C: C; transition-duration: 1s, 2s; color: red; --B: B;');
assert_array_equals(styleMap.getProperties(),
['color', 'transition-duration', 'width', '--A', '--B', '--C']);
}, 'StylePropertyMap.getProperties returns property names in correct order');
</script>

View file

@ -16,6 +16,21 @@ function assert_is_equal_with_range_handling(input, result) {
assert_style_value_equals(result, input); assert_style_value_equals(result, input);
} }
const gCssWideKeywordsExamples = [
{
description: 'initial keyword',
input: new CSSKeywordValue('initial')
},
{
description: 'inherit keyword',
input: new CSSKeywordValue('initial')
},
{
description: 'unset keyword',
input: new CSSKeywordValue('initial')
},
];
const gTestSyntaxExamples = { const gTestSyntaxExamples = {
'<length>': { '<length>': {
description: 'a length', description: 'a length',
@ -149,26 +164,28 @@ function testPropertyValid(propertyName, examples, specified, computed, descript
// specified style // specified style
const specifiedResult = element.attributeStyleMap.get(propertyName); const specifiedResult = element.attributeStyleMap.get(propertyName);
assert_not_equals(specifiedResult, null,
'Specified value must not be null');
assert_true(specifiedResult instanceof CSSStyleValue,
'Specified value must be a CSSStyleValue');
if (specified || example.defaultSpecified) { if (specified || example.defaultSpecified) {
(specified || example.defaultSpecified)(example.input, specifiedResult); (specified || example.defaultSpecified)(example.input, specifiedResult);
} else { } else {
assert_not_equals(specifiedResult, null,
'Specified value must not be null');
assert_true(specifiedResult instanceof CSSStyleValue,
'Specified value must be a CSSStyleValue');
assert_style_value_equals(specifiedResult, example.input, assert_style_value_equals(specifiedResult, example.input,
`Setting ${example.description} and getting its specified value`); `Setting ${example.description} and getting its specified value`);
} }
// computed style // computed style
const computedResult = element.computedStyleMap().get(propertyName); const computedResult = element.computedStyleMap().get(propertyName);
assert_not_equals(computedResult, null,
'Computed value must not be null');
assert_true(computedResult instanceof CSSStyleValue,
'Computed value must be a CSSStyleValue');
if (computed || example.defaultComputed) { if (computed || example.defaultComputed) {
(computed || example.defaultComputed)(example.input, computedResult); (computed || example.defaultComputed)(example.input, computedResult);
} else { } else {
assert_not_equals(computedResult, null,
'Computed value must not be null');
assert_true(computedResult instanceof CSSStyleValue,
'Computed value must be a CSSStyleValue');
assert_style_value_equals(computedResult, example.input, assert_style_value_equals(computedResult, example.input,
`Setting ${example.description} and getting its computed value`); `Setting ${example.description} and getting its computed value`);
} }
@ -220,6 +237,13 @@ function createKeywordExample(keyword) {
function runPropertyTests(propertyName, testCases) { function runPropertyTests(propertyName, testCases) {
let syntaxTested = new Set(); let syntaxTested = new Set();
// Every property should at least support CSS-wide keywords.
testPropertyValid(propertyName,
gCssWideKeywordsExamples,
null, // should be as specified
() => {}, // could be anything
'CSS-wide keywords');
for (const testCase of testCases) { for (const testCase of testCases) {
// Retrieve test examples for this test case's syntax. If the syntax // Retrieve test examples for this test case's syntax. If the syntax
// looks like a keyword, then create an example on the fly. // looks like a keyword, then create an example on the fly.

View file

@ -42,10 +42,17 @@ test(function() {
test(function() { test(function() {
var serializer = new XMLSerializer(); var serializer = new XMLSerializer();
var root = createXmlDoc().documentElement; var parser = new DOMParser();
root.firstChild.setAttribute('attr1', 'value1\tvalue2\r\n'); var root = parser.parseFromString('<root />', 'text/xml').documentElement;
var xmlString = serializer.serializeToString(root); root.setAttribute('attr', '\t');
assert_equals(xmlString, '<root><child1 attr1="value1&#9;value2&#xD;&#xA;">value1</child1></root>'); assert_in_array(serializer.serializeToString(root), [
'<root attr="&#9;"/>', '<root attr="&#x9;"/>']);
root.setAttribute('attr', '\n');
assert_in_array(serializer.serializeToString(root), [
'<root attr="&#xA;"/>', '<root attr="&#10;"/>']);
root.setAttribute('attr', '\r');
assert_in_array(serializer.serializeToString(root), [
'<root attr="&#xD;"/>', '<root attr="&#13;"/>']);
}, 'check XMLSerializer.serializeToString escapes attribute values for roundtripping'); }, 'check XMLSerializer.serializeToString escapes attribute values for roundtripping');
</script> </script>

View file

@ -11,7 +11,7 @@
const same_origin_src = '/feature-policy/resources/feature-policy-picture-in-picture.html'; const same_origin_src = '/feature-policy/resources/feature-policy-picture-in-picture.html';
const cross_origin_src = 'https://{{domains[www]}}:{{ports[https][0]}}' + const cross_origin_src = 'https://{{domains[www]}}:{{ports[https][0]}}' +
same_origin_src; same_origin_src;
const header = 'Default "picture-in-picture" feature policy ["self"]'; const header = 'Default "picture-in-picture" feature policy [*]';
async_test(t => { async_test(t => {
isPictureInPictureAllowed().then(t.step_func_done((result) => { isPictureInPictureAllowed().then(t.step_func_done((result) => {
@ -26,8 +26,8 @@
async_test(t => { async_test(t => {
test_feature_availability('picture-in-picture', t, cross_origin_src, test_feature_availability('picture-in-picture', t, cross_origin_src,
expect_feature_unavailable_default,); expect_feature_available_default,);
}, header + ' disallows cross-origin iframes.'); }, header + ' allows cross-origin iframes.');
</script> </script>
</body> </body>

View file

@ -1,9 +1,19 @@
# Tests related to Cross-Origin Resource Blocking (CORB). # Tests related to Cross-Origin Resource Blocking (CORB).
### Summary
This directory contains tests related to the This directory contains tests related to the
[Cross-Origin Resource Blocking (CORB)](https://chromium.googlesource.com/chromium/src/+/master/content/browser/loader/cross_origin_read_blocking_explainer.md) [Cross-Origin Resource Blocking (CORB)](https://chromium.googlesource.com/chromium/src/+/master/content/browser/loader/cross_origin_read_blocking_explainer.md)
algorithm. algorithm.
The tests in this directory interact with various, random features,
but the tests have been grouped together into the `fetch/corb` directory,
because all of these tests verify behavior that is important to the CORB
algorithm.
### Disclaimer: CORB is not standardized yet
Note that CORB is currently in very early stages of standardization path. At Note that CORB is currently in very early stages of standardization path. At
the same time, some tests in this directory (e.g. the same time, some tests in this directory (e.g.
`css-with-json-parser-breaker`) cover behavior spec-ed outside of CORB (making `css-with-json-parser-breaker`) cover behavior spec-ed outside of CORB (making
@ -30,7 +40,35 @@ CORB is enabled. In practice this means that:
`third_party/WebKit/LayoutTests/FlagExpectations/site-per-process` file. `third_party/WebKit/LayoutTests/FlagExpectations/site-per-process` file.
* Such tests may fail in other browsers. * Such tests may fail in other browsers.
The tests in this directory interact with various, random features,
but the tests have been grouped together into the `fetch/corb` directory, ### Limitations of WPT test coverage
because all of these tests verify behavior that is important to the CORB
algorithm. CORB is a defense-in-depth and in general should not cause changes in behavior
that can be observed by web features or by end users. This makes CORB difficult
or even impossible to test via WPT.
WPT tests can cover the following:
* Helping verify CORB has no observable impact in specific scenarios.
Examples:
* image rendering of (an empty response of) a html document blocked by CORB
should be indistinguishable from rendering such html document without CORB -
`img-html-correctly-labeled.sub.html`
* CORB shouldn't block responses that don't sniff as a CORB-protected document
type - `img-png-mislabeled-as-html.sub.html`
* Helping document cases where CORB causes observable changes in behavior.
Examples:
* blocking of nosniff images labeled as non-image, CORB-protected
Content-Type - `img-png-mislabeled-as-html-nosniff.tentative.sub.html`
* blocking of CORB-protected documents can prevent triggering
syntax errors in scripts -
`script-html-via-cross-origin-blob-url.tentative.sub.html`
Examples of aspects that WPT tests cannot cover (these aspects have to be
covered in other, browser-specific tests):
* Verifying that CORB doesn't affect things that are only indirectly
observable by the web (like
[prefetch](https://html.spec.whatwg.org/#link-type-prefetch).
* Verifying that CORB strips non-safe-listed headers of blocked responses.
* Verifying that CORB blocks responses before they reach the process hosting
a cross-origin execution context.

View file

@ -0,0 +1,16 @@
<!DOCTYPE html>
<meta charset="utf-8">
<script>
fetch('html-correctly-labeled.html')
.then(response => response.blob())
.then(blob => {
let msg = { blob_size: blob.size,
blob_type: blob.type,
blob_url: URL.createObjectURL(blob) };
window.parent.postMessage(msg, '*');
})
.catch(error => {
let msg = { error: error };
window.parent.postMessage(msg, '*');
});
</script>

View file

@ -0,0 +1,38 @@
<!DOCTYPE html>
<!-- Test verifies that cross-origin blob URIs are blocked both with and
without CORB.
-->
<meta charset="utf-8">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<div id=log></div>
<script>
async_test(function(t) {
function step1_createSubframe() {
addEventListener("message", function(e) {
t.step(function() { step2_processSubframeMsg(e.data); })
});
var subframe = document.createElement("iframe")
// www1 is cross-origin, to ensure that the received blob will be cross-origin.
subframe.src = 'http://{{domains[www1]}}:{{ports[http][0]}}/fetch/corb/resources/subframe-that-posts-html-containing-blob-url-to-parent.html';
document.body.appendChild(subframe);
}
function step2_processSubframeMsg(msg) {
assert_not_exists(msg, 'error');
assert_equals(msg.blob_type, 'text/html');
assert_equals(msg.blob_size, 147);
// With and without CORB loading of a cross-origin blob should be blocked
// (this is verified by expecting |script.onerror|, but not |script.onload|
// below).
var script = document.createElement("script")
script.src = msg.blob_url;
script.onerror = t.step_func_done(function(){})
script.onload = t.unreached_func("Unexpected load event")
document.body.appendChild(script)
}
step1_createSubframe();
});
</script>

View file

@ -0,0 +1,34 @@
<!DOCTYPE html>
<link rel="help" href="https://html.spec.whatwg.org/multipage/media.html#text-track-model">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/common/media.js"></script>
<div id="log"></div>
<script>
// Media elements have a "list of pending text tracks" which should be populated
// with text tracks with readyState "loading". When the text track src is
// invalid or points to a non-existent resource, it shouldn't be possible to
// block the media element's readyState indefinitely.
function t(trackSrc) {
const track = document.createElement('track');
track.src = trackSrc;
track.default = true;
async_test(t => {
const video = document.createElement('video');
video.autoplay = true;
video.controls = true; // for visual inspection, not part of test
video.src = getVideoURI('/media/movie_5');
video.appendChild(track);
document.body.appendChild(video);
// The playing event isn't used because it's fired in Safari even when the
// playback doesn't actually start.
video.ontimeupdate = t.step_func(() => {
if (video.currentTime > 0)
t.done();
});
}, `<video autoplay> with ${track.outerHTML} child`);
}
t("invalid://url");
t("404");
t("");
</script>

View file

@ -34,7 +34,7 @@ promise_test(t => {
.then(result => { .then(result => {
assert_equals(result, 'request for embedded content was not intercepted'); assert_equals(result, 'request for embedded content was not intercepted');
}); });
}, 'requests for EMBED elements should not be intercepted by service workers'); }, 'requests for EMBED elements of embedded HTML content should not be intercepted by service workers');
promise_test(t => { promise_test(t => {
let frame; let frame;
@ -47,5 +47,32 @@ promise_test(t => {
.then(result => { .then(result => {
assert_equals(result, 'request for embedded content was not intercepted'); assert_equals(result, 'request for embedded content was not intercepted');
}); });
}, 'requests for OBJECT elements should not be intercepted by service workers'); }, 'requests for OBJECT elements of embedded HTML content should not be intercepted by service workers');
promise_test(t => {
let frame;
return with_iframe('resources/embed-image-is-not-intercepted-iframe.html')
.then(f => {
frame = f;
t.add_cleanup(() => { frame.remove(); });
return frame.contentWindow.test_promise;
})
.then(result => {
assert_equals(result, 'request was not intercepted');
});
}, 'requests for EMBED elements of an image should not be intercepted by service workers');
promise_test(t => {
let frame;
return with_iframe('resources/object-image-is-not-intercepted-iframe.html')
.then(f => {
frame = f;
t.add_cleanup(() => { frame.remove(); });
return frame.contentWindow.test_promise;
})
.then(result => {
assert_equals(result, 'request was not intercepted');
});
}, 'requests for OBJECT elements of an image should not be intercepted by service workers');
</script> </script>

View file

@ -4,7 +4,10 @@ def main(request, response):
headers = [("X-Request-Method", request.method), headers = [("X-Request-Method", request.method),
("X-Request-Content-Length", request.headers.get("Content-Length", "NO")), ("X-Request-Content-Length", request.headers.get("Content-Length", "NO")),
("X-Request-Content-Type", request.headers.get("Content-Type", "NO"))] ("X-Request-Content-Type", request.headers.get("Content-Type", "NO")),
# Avoid any kind of content sniffing on the response.
("Content-Type", "text/plain")]
content = request.body content = request.body

View file

@ -4,5 +4,11 @@
self.addEventListener('fetch', e => { self.addEventListener('fetch', e => {
if (e.request.url.indexOf('embedded-content-from-server.html') != -1) { if (e.request.url.indexOf('embedded-content-from-server.html') != -1) {
e.respondWith(fetch('embedded-content-from-service-worker.html')); e.respondWith(fetch('embedded-content-from-service-worker.html'));
return;
}
if (e.request.url.indexOf('green.png') != -1) {
e.respondWith(Promise.reject('network error to show interception occurred'));
return;
} }
}); });

View file

@ -0,0 +1,21 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>iframe for embed-and-object-are-not-intercepted test</title>
<body>
<embed type="image/png" src="/images/green.png"></embed>
<script>
// Our parent (the root frame of the test) will examine this to get the result.
var test_promise = new Promise(resolve => {
if (!navigator.serviceWorker.controller)
resolve('FAIL: this iframe is not controlled');
const elem = document.querySelector('embed');
elem.addEventListener('load', e => {
resolve('request was not intercepted');
});
elem.addEventListener('error', e => {
resolve('FAIL: request was intercepted');
});
});
</script>
</body>

View file

@ -15,4 +15,3 @@ var test_promise = new Promise(resolve => {
<embed src="embedded-content-from-server.html"></embed> <embed src="embedded-content-from-server.html"></embed>
</body> </body>

View file

@ -0,0 +1,21 @@
<!DOCTYPE html>
<meta charset="utf-8">
<title>iframe for embed-and-object-are-not-intercepted test</title>
<body>
<object type="image/png" data="/images/green.png"></embed>
<script>
// Our parent (the root frame of the test) will examine this to get the result.
var test_promise = new Promise(resolve => {
if (!navigator.serviceWorker.controller)
resolve('FAIL: this iframe is not controlled');
const elem = document.querySelector('object');
elem.addEventListener('load', e => {
resolve('request was not intercepted');
});
elem.addEventListener('error', e => {
resolve('FAIL: request was intercepted');
});
});
</script>
</body>

View file

@ -8,7 +8,7 @@ sys.path.insert(0, os.path.join(here))
sys.path.insert(0, os.path.join(here, "six")) sys.path.insert(0, os.path.join(here, "six"))
sys.path.insert(0, os.path.join(here, "html5lib")) sys.path.insert(0, os.path.join(here, "html5lib"))
sys.path.insert(0, os.path.join(here, "wptserve")) sys.path.insert(0, os.path.join(here, "wptserve"))
sys.path.insert(0, os.path.join(here, "pywebsocket", "src")) sys.path.insert(0, os.path.join(here, "pywebsocket"))
sys.path.insert(0, os.path.join(here, "third_party", "attrs", "src")) sys.path.insert(0, os.path.join(here, "third_party", "attrs", "src"))
sys.path.insert(0, os.path.join(here, "third_party", "funcsigs")) sys.path.insert(0, os.path.join(here, "third_party", "funcsigs"))
sys.path.insert(0, os.path.join(here, "third_party", "pluggy")) sys.path.insert(0, os.path.join(here, "third_party", "pluggy"))

View file

@ -91,6 +91,8 @@ class Manifest(object):
hash_changed = True hash_changed = True
else: else:
new_type, manifest_items = old_type, self._data[old_type][rel_path] new_type, manifest_items = old_type, self._data[old_type][rel_path]
if old_type == "reftest" and new_type != old_type:
reftest_changes = True
else: else:
new_type, manifest_items = source_file.manifest_items() new_type, manifest_items = source_file.manifest_items()

View file

@ -232,6 +232,28 @@ def test_reftest_computation_chain_update_remove():
assert list(m) == [("reftest", test2.path, {test2})] assert list(m) == [("reftest", test2.path, {test2})]
def test_reftest_computation_chain_update_test_type():
m = manifest.Manifest()
s1 = SourceFileWithTest("test", "0"*40, item.RefTest, [("/test-ref", "==")])
assert m.update([s1]) is True
test1 = s1.manifest_items()[1][0]
assert list(m) == [("reftest", test1.path, {test1})]
# test becomes a testharness test (hash change because that is determined
# based on the file contents). The updated manifest should not includes the
# old reftest.
s2 = SourceFileWithTest("test", "1"*40, item.TestharnessTest)
assert m.update([s2]) is True
test2 = s2.manifest_items()[1][0]
assert list(m) == [("testharness", test2.path, {test2})]
def test_iterpath(): def test_iterpath():
m = manifest.Manifest() m = manifest.Manifest()

View file

@ -0,0 +1,11 @@
For instructions for contributing code, please read:
https://github.com/google/pywebsocket/wiki/CodeReviewInstruction
You must complete the Individual Contributor License Agreement.
https://cla.developers.google.com/about/google-individual
You can do this online, and it only takes a minute.
If you are contributing on behalf of a corporation, you must fill out the
Corporate Contributor License Agreement
https://cla.developers.google.com/about/google-corporate
and send it to us as described on that page.

View file

@ -0,0 +1,8 @@
# pywebsocket #
The pywebsocket project aims to provide a [WebSocket](https://tools.ietf.org/html/rfc6455) standalone server and a WebSocket extension for [Apache HTTP Server](https://httpd.apache.org/), mod\_pywebsocket.
pywebsocket is intended for **testing** or **experimental** purposes.
Please see [Wiki](../../wiki) for more details.

View file

@ -52,65 +52,35 @@ function getConfig() {
// If the size of each message is small, send/receive multiple messages // If the size of each message is small, send/receive multiple messages
// until the sum of sizes reaches this threshold. // until the sum of sizes reaches this threshold.
minTotal: getIntFromInput('mintotal'), minTotal: getIntFromInput('mintotal'),
multipliers: getIntArrayFromInput('multipliers'), multipliers: getFloatArrayFromInput('multipliers'),
verifyData: getBoolFromCheckBox('verifydata') verifyData: getBoolFromCheckBox('verifydata'),
addToLog: addToLog,
addToSummary: addToSummary,
measureValue: measureValue,
notifyAbort: notifyAbort
}; };
} }
var worker = new Worker('benchmark.js');
worker.onmessage = onMessage;
function onSendBenchmark() { function onSendBenchmark() {
var config = getConfig(); var config = getConfig();
doAction(config, getBoolFromCheckBox('worker'), 'sendBenchmark');
if (getBoolFromCheckBox('worker')) {
worker.postMessage({type: 'sendBenchmark', config: config});
} else {
config.addToLog = addToLog;
config.addToSummary = addToSummary;
config.measureValue = measureValue;
sendBenchmark(config);
}
} }
function onReceiveBenchmark() { function onReceiveBenchmark() {
var config = getConfig(); var config = getConfig();
doAction(config, getBoolFromCheckBox('worker'), 'receiveBenchmark');
if (getBoolFromCheckBox('worker')) {
worker.postMessage({type: 'receiveBenchmark', config: config});
} else {
config.addToLog = addToLog;
config.addToSummary = addToSummary;
config.measureValue = measureValue;
receiveBenchmark(config);
}
} }
function onBatchBenchmark() { function onBatchBenchmark() {
var config = getConfig(); var config = getConfig();
doAction(config, getBoolFromCheckBox('worker'), 'batchBenchmark');
if (getBoolFromCheckBox('worker')) {
worker.postMessage({type: 'batchBenchmark', config: config});
} else {
config.addToLog = addToLog;
config.addToSummary = addToSummary;
config.measureValue = measureValue;
batchBenchmark(config);
}
} }
function onStop() { function onStop() {
var config = getConfig(); var config = getConfig();
doAction(config, getBoolFromCheckBox('worker'), 'stop');
if (getBoolFromCheckBox('worker')) {
worker.postMessage({type: 'stop', config: config});
} else {
config.addToLog = addToLog;
config.addToSummary = addToSummary;
config.measureValue = measureValue;
stop(config);
}
} }
function init() { function init() {
addressBox = document.getElementById('address'); addressBox = document.getElementById('address');
logBox = document.getElementById('log'); logBox = document.getElementById('log');
@ -128,6 +98,8 @@ function init() {
if (!('WebSocket' in window)) { if (!('WebSocket' in window)) {
addToLog('WebSocket is not available'); addToLog('WebSocket is not available');
} }
initWorker('WebSocket', '');
} }
</script> </script>
</head> </head>

View file

@ -32,7 +32,7 @@ function destroyAllSockets() {
sockets = []; sockets = [];
} }
function sendBenchmarkStep(size, config) { function sendBenchmarkStep(size, config, isWarmUp) {
timerID = null; timerID = null;
var totalSize = 0; var totalSize = 0;
@ -41,6 +41,7 @@ function sendBenchmarkStep(size, config) {
var onMessageHandler = function(event) { var onMessageHandler = function(event) {
if (!verifyAcknowledgement(config, event.data, size)) { if (!verifyAcknowledgement(config, event.data, size)) {
destroyAllSockets(); destroyAllSockets();
config.notifyAbort();
return; return;
} }
@ -50,7 +51,8 @@ function sendBenchmarkStep(size, config) {
return; return;
} }
calculateAndLogResult(config, size, benchmark.startTimeInMs, totalSize); calculateAndLogResult(config, size, benchmark.startTimeInMs, totalSize,
isWarmUp);
runNextTask(config); runNextTask(config);
}; };
@ -89,7 +91,7 @@ function sendBenchmarkStep(size, config) {
} }
} }
function receiveBenchmarkStep(size, config) { function receiveBenchmarkStep(size, config, isWarmUp) {
timerID = null; timerID = null;
var totalSize = 0; var totalSize = 0;
@ -101,12 +103,14 @@ function receiveBenchmarkStep(size, config) {
config.addToLog('Expected ' + size + 'B but received ' + config.addToLog('Expected ' + size + 'B but received ' +
bytesReceived + 'B'); bytesReceived + 'B');
destroyAllSockets(); destroyAllSockets();
config.notifyAbort();
return; return;
} }
if (config.verifyData && !verifyArrayBuffer(event.data, 0x61)) { if (config.verifyData && !verifyArrayBuffer(event.data, 0x61)) {
config.addToLog('Response verification failed'); config.addToLog('Response verification failed');
destroyAllSockets(); destroyAllSockets();
config.notifyAbort();
return; return;
} }
@ -116,7 +120,8 @@ function receiveBenchmarkStep(size, config) {
return; return;
} }
calculateAndLogResult(config, size, benchmark.startTimeInMs, totalSize); calculateAndLogResult(config, size, benchmark.startTimeInMs, totalSize,
isWarmUp);
runNextTask(config); runNextTask(config);
}; };
@ -153,12 +158,11 @@ function createSocket(config) {
}; };
socket.onclose = function(event) { socket.onclose = function(event) {
config.addToLog('Closed'); config.addToLog('Closed');
config.notifyAbort();
}; };
return socket; return socket;
} }
var tasks = [];
function startBenchmark(config) { function startBenchmark(config) {
clearTimeout(timerID); clearTimeout(timerID);
destroyAllSockets(); destroyAllSockets();
@ -180,24 +184,6 @@ function startBenchmark(config) {
} }
} }
function runNextTask(config) {
var task = tasks.shift();
if (task == undefined) {
config.addToLog('Finished');
destroyAllSockets();
return;
}
timerID = setTimeout(task, 0);
}
function buildLegendString(config) {
var legend = ''
if (config.printSize)
legend = 'Message size in KiB, Time/message in ms, ';
legend += 'Speed in kB/s';
return legend;
}
function getConfigString(config) { function getConfigString(config) {
return '(WebSocket' + return '(WebSocket' +
', ' + (typeof importScripts !== "undefined" ? 'Worker' : 'Main') + ', ' + (typeof importScripts !== "undefined" ? 'Worker' : 'Main') +
@ -209,69 +195,6 @@ function getConfigString(config) {
')'; ')';
} }
function addTasks(config, stepFunc) {
for (var i = 0;
i < config.numWarmUpIterations + config.numIterations; ++i) {
// Ignore the first |config.numWarmUpIterations| iterations.
if (i == config.numWarmUpIterations)
addResultClearingTask(config);
var multiplierIndex = 0;
for (var size = config.startSize;
size <= config.stopThreshold;
++multiplierIndex) {
var task = stepFunc.bind(
null,
size,
config);
tasks.push(task);
size *= config.multipliers[
multiplierIndex % config.multipliers.length];
}
}
}
function addResultReportingTask(config, title) {
tasks.push(function(){
timerID = null;
config.addToSummary(title);
reportAverageData(config);
clearAverageData();
runNextTask(config);
});
}
function addResultClearingTask(config) {
tasks.push(function(){
timerID = null;
clearAverageData();
runNextTask(config);
});
}
function sendBenchmark(config) {
config.addToLog('Send benchmark');
config.addToLog(buildLegendString(config));
tasks = [];
clearAverageData();
addTasks(config, sendBenchmarkStep);
addResultReportingTask(config, 'Send Benchmark ' + getConfigString(config));
startBenchmark(config);
}
function receiveBenchmark(config) {
config.addToLog('Receive benchmark');
config.addToLog(buildLegendString(config));
tasks = [];
clearAverageData();
addTasks(config, receiveBenchmarkStep);
addResultReportingTask(config,
'Receive Benchmark ' + getConfigString(config));
startBenchmark(config);
}
function batchBenchmark(config) { function batchBenchmark(config) {
config.addToLog('Batch benchmark'); config.addToLog('Batch benchmark');
config.addToLog(buildLegendString(config)); config.addToLog(buildLegendString(config));
@ -286,24 +209,6 @@ function batchBenchmark(config) {
startBenchmark(config); startBenchmark(config);
} }
function stop(config) { function cleanup() {
clearTimeout(timerID);
timerID = null;
config.addToLog('Stopped');
destroyAllSockets(); destroyAllSockets();
} }
onmessage = function (message) {
var config = message.data.config;
config.addToLog = workerAddToLog;
config.addToSummary = workerAddToSummary;
config.measureValue = workerMeasureValue;
if (message.data.type === 'sendBenchmark')
sendBenchmark(config);
else if (message.data.type === 'receiveBenchmark')
receiveBenchmark(config);
else if (message.data.type === 'batchBenchmark')
batchBenchmark(config);
else if (message.data.type === 'stop')
stop(config);
};

View file

@ -0,0 +1,163 @@
<!--
Copyright 2015 Google Inc. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the COPYING file or at
https://developers.google.com/open-source/licenses/bsd
-->
<html>
<head>
<title>Fetch API benchmark</title>
<script src="util_main.js"></script>
<script src="util.js"></script>
<script src="fetch_benchmark.js"></script>
<script>
var addressBox = null;
function getConfig() {
return {
prefixUrl: addressBox.value,
printSize: getBoolFromCheckBox('printsize'),
numFetches: getIntFromInput('numFetches'),
// Initial size of messages.
numIterations: getIntFromInput('numiterations'),
numWarmUpIterations: getIntFromInput('numwarmupiterations'),
startSize: getIntFromInput('startsize'),
// Stops benchmark when the size of message exceeds this threshold.
stopThreshold: getIntFromInput('stopthreshold'),
multipliers: getFloatArrayFromInput('multipliers'),
verifyData: getBoolFromCheckBox('verifydata'),
addToLog: addToLog,
addToSummary: addToSummary,
measureValue: measureValue,
notifyAbort: notifyAbort
};
}
function onSendBenchmark() {
var config = getConfig();
config.dataType = getStringFromRadioBox('datatyperadio');
doAction(config, getBoolFromCheckBox('worker'), 'sendBenchmark');
}
function onReceiveBenchmark() {
var config = getConfig();
config.dataType = getStringFromRadioBox('datatyperadio');
doAction(config, getBoolFromCheckBox('worker'), 'receiveBenchmark');
}
function onBatchBenchmark() {
var config = getConfig();
doAction(config, getBoolFromCheckBox('worker'), 'batchBenchmark');
}
function onStop() {
var config = getConfig();
doAction(config, getBoolFromCheckBox('worker'), 'stop');
}
function init() {
addressBox = document.getElementById('address');
logBox = document.getElementById('log');
summaryBox = document.getElementById('summary');
// Special address of pywebsocket for XHR/Fetch API benchmark.
addressBox.value = '/073be001e10950692ccbf3a2ad21c245';
addToLog(window.navigator.userAgent.toLowerCase());
addToSummary(window.navigator.userAgent.toLowerCase());
initWorker('fetch', '');
}
</script>
</head>
<body onload="init()">
<form id="benchmark_form">
url prefix <input type="text" id="address" size="40">
<input type="button" value="send" onclick="onSendBenchmark()">
<input type="button" value="receive" onclick="onReceiveBenchmark()">
<input type="button" value="batch" onclick="onBatchBenchmark()">
<input type="button" value="stop" onclick="onStop()">
<br/>
<input type="checkbox" id="printsize" checked>
<label for="printsize">Print size and time per message</label>
<input type="checkbox" id="verifydata" checked>
<label for="verifydata">Verify data</label>
<input type="checkbox" id="worker">
<label for="worker">Run on worker</label>
<br/>
Parameters:
<br/>
<table>
<tr>
<td>Number of fetch() requests</td>
<td><input type="text" id="numFetches" value="1"></td>
</tr>
<tr>
<td>Number of iterations</td>
<td><input type="text" id="numiterations" value="1"></td>
</tr>
<tr>
<td>Number of warm-up iterations</td>
<td><input type="text" id="numwarmupiterations" value="0"></td>
</tr>
<tr>
<td>Start size</td>
<td><input type="text" id="startsize" value="10240"></td>
</tr>
<tr>
<td>Stop threshold</td>
<td><input type="text" id="stopthreshold" value="102400000"></td>
</tr>
<tr>
<td>Multipliers</td>
<td><input type="text" id="multipliers" value="5, 2"></td>
</tr>
</table>
Set data type
<input type="radio"
name="datatyperadio"
id="datatyperadiotext"
value="text"
checked><label for="datatyperadiotext">text</label>
<input type="radio"
name="datatyperadio"
id="datatyperadioblob"
value="blob"
><label for="datatyperadioblob">blob</label>
<input type="radio"
name="datatyperadio"
id="datatyperadioarraybuffer"
value="arraybuffer"
><label for="datatyperadioarraybuffer">arraybuffer</label>
</form>
<div id="log_div">
<textarea
id="log" rows="20" style="width: 100%" readonly></textarea>
</div>
<div id="summary_div">
Summary
<textarea
id="summary" rows="20" style="width: 100%" readonly></textarea>
</div>
<div id="note_div">
Note:
<ul>
<li>Effect of RTT and time spent for ArrayBuffer creation in receive benchmarks are not eliminated.</li>
<li>The Stddev column shows NaN when the number of iterations is set to 1.</li>
</ul>
</div>
</body>
</html>

View file

@ -0,0 +1,225 @@
// Copyright 2015 Google Inc. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the COPYING file or at
// https://developers.google.com/open-source/licenses/bsd
var isWorker = typeof importScripts !== "undefined";
if (isWorker) {
// Running on a worker
importScripts('util.js', 'util_worker.js');
}
// Namespace for holding globals.
var benchmark = {};
benchmark.startTimeInMs = 0;
var timerID = null;
function sendBenchmarkStep(size, config, isWarmUp) {
timerID = null;
benchmark.startTimeInMs = null;
// Prepare data.
var dataArray = [];
for (var i = 0; i < config.numFetches; ++i) {
var data = null;
if (config.dataType == 'arraybuffer' ||
config.dataType == 'blob') {
data = new ArrayBuffer(size);
fillArrayBuffer(data, 0x61);
if (config.dataType == 'blob') {
data = new Blob([data]);
}
} else {
data = repeatString('a', size);
}
dataArray.push(data);
}
// Start time measuring.
benchmark.startTimeInMs = getTimeStamp();
// Start fetch.
var promises = [];
for (var i = 0; i < config.numFetches; ++i) {
var data = dataArray[i];
var promise = fetch(config.prefixUrl + '_send',
{method: 'POST', body: data})
.then(function (response) {
if (response.status != 200) {
config.addToLog('Failed (status=' + response.status + ')');
return Promise.reject();
}
// Check and warn if proxy is enabled.
if (response.headers.get('Via') !== null) {
config.addToLog('WARNING: proxy seems enabled.');
}
if (config.verifyData) {
return response.text()
.then(function(text) {
if (!verifyAcknowledgement(config, text, size)) {
return Promise.reject();
}
});
}
});
promises.push(promise);
}
// Finish and report time measuring.
Promise.all(promises)
.then(function() {
if (benchmark.startTimeInMs == null) {
config.addToLog('startTimeInMs not set');
return Promise.reject();
}
calculateAndLogResult(config, size, benchmark.startTimeInMs,
size * config.numFetches, isWarmUp);
runNextTask(config);
})
.catch(function(e) {
config.addToLog("ERROR: " + e);
config.notifyAbort();
});
}
function receiveBenchmarkStep(size, config, isWarmUp) {
timerID = null;
benchmark.startTimeInMs = null;
// Start time measuring.
benchmark.startTimeInMs = getTimeStamp();
// Start fetch.
var promises = [];
for (var i = 0; i < config.numFetches; ++i) {
var request;
if (config.methodAndCache === 'GET-NOCACHE') {
request = new Request(config.prefixUrl + '_receive_getnocache?' + size,
{method: 'GET'});
} else if (config.methodAndCache === 'GET-CACHE') {
request = new Request(config.prefixUrl + '_receive_getcache?' + size,
{method: 'GET'});
} else {
request = new Request(config.prefixUrl + '_receive',
{method: 'POST', body: size + ' none'});
}
var promise = fetch(request)
.then(function(response) {
if (response.status != 200) {
config.addToLog('Failed (status=' + this.status + ')');
return Promise.reject();
}
// Check and warn if proxy is enabled.
if (response.headers.get('Via') !== null) {
config.addToLog('WARNING: proxy seems enabled.');
}
if (config.dataType === 'arraybuffer') {
return response.arrayBuffer()
.then(function(arrayBuffer) {
return [arrayBuffer.byteLength,
(!config.verifyData ||
verifyArrayBuffer(arrayBuffer, 0x61))];
});
} else if (config.dataType == 'blob') {
return response.blob()
.then(function(blob) {
return new Promise(function(resolve, reject) {
if (config.verifyData) {
verifyBlob(config, blob, 0x61,
function(receivedSize, verificationResult) {
resolve([receivedSize, verificationResult]);
});
} else {
resolve([blob.size, true]);
}
});
});
} else {
return response.text()
.then(function(text) {
return [text.length,
(!config.verifyData ||
text == repeatString('a', text.length))];
});
}
})
.then(function(receivedSizeAndVerificationResult) {
var receivedSize = receivedSizeAndVerificationResult[0];
var verificationResult = receivedSizeAndVerificationResult[1];
if (receivedSize !== size) {
config.addToLog('Expected ' + size +
'B but received ' + receivedSize + 'B');
return Promise.reject();
}
if (!verificationResult) {
config.addToLog('Response verification failed');
return Promise.reject();
}
});
promises.push(promise);
}
// Finish and report time measuring.
Promise.all(promises)
.then(function() {
if (benchmark.startTimeInMs == null) {
config.addToLog('startTimeInMs not set');
return Promise.reject();
}
calculateAndLogResult(config, size, benchmark.startTimeInMs,
size * config.numFetches, isWarmUp);
runNextTask(config);
})
.catch(function(e) {
config.addToLog("ERROR: " + e);
config.notifyAbort();
});
}
function getConfigString(config) {
return '(' + config.dataType +
', verifyData=' + config.verifyData +
', ' + (isWorker ? 'Worker' : 'Main') +
', numFetches=' + config.numFetches +
', numIterations=' + config.numIterations +
', numWarmUpIterations=' + config.numWarmUpIterations +
')';
}
function startBenchmark(config) {
clearTimeout(timerID);
runNextTask(config);
}
function batchBenchmark(originalConfig) {
originalConfig.addToLog('Batch benchmark');
tasks = [];
clearAverageData();
var dataTypes = ['text', 'blob', 'arraybuffer'];
var stepFuncs = [sendBenchmarkStep, receiveBenchmarkStep];
var names = ['Send', 'Receive'];
for (var i = 0; i < stepFuncs.length; ++i) {
for (var j = 0; j < dataTypes.length; ++j) {
var config = cloneConfig(originalConfig);
config.dataType = dataTypes[j];
addTasks(config, stepFuncs[i]);
addResultReportingTask(config,
names[i] + ' benchmark ' + getConfigString(config));
}
}
startBenchmark(config);
}
function cleanup() {
}

View file

@ -0,0 +1,6 @@
<!DOCTYPE html>
<head>
<script src="util.js"></script>
<script src="performance_test_iframe.js"></script>
<script src="fetch_benchmark.js"></script>
</head>

View file

@ -0,0 +1,6 @@
<!DOCTYPE html>
<head>
<script src="util.js"></script>
<script src="performance_test_iframe.js"></script>
<script src="benchmark.js"></script>
</head>

View file

@ -0,0 +1,66 @@
function perfTestAddToLog(text) {
parent.postMessage({'command': 'log', 'value': text}, '*');
}
function perfTestAddToSummary(text) {
}
function perfTestMeasureValue(value) {
parent.postMessage({'command': 'measureValue', 'value': value}, '*');
}
function perfTestNotifyAbort() {
parent.postMessage({'command': 'notifyAbort'}, '*');
}
function getConfigForPerformanceTest(connectionType, dataType, async,
verifyData, numIterations,
numWarmUpIterations) {
var prefixUrl;
if (connectionType === 'WebSocket') {
prefixUrl = 'ws://' + location.host + '/benchmark_helper';
} else {
// XHR or fetch
prefixUrl = 'http://' + location.host + '/073be001e10950692ccbf3a2ad21c245';
}
return {
prefixUrl: prefixUrl,
printSize: true,
numXHRs: 1,
numFetches: 1,
numSockets: 1,
// + 1 is for a warmup iteration by the Telemetry framework.
numIterations: numIterations + numWarmUpIterations + 1,
numWarmUpIterations: numWarmUpIterations,
minTotal: 10240000,
startSize: 10240000,
stopThreshold: 10240000,
multipliers: [2],
verifyData: verifyData,
dataType: dataType,
async: async,
addToLog: perfTestAddToLog,
addToSummary: perfTestAddToSummary,
measureValue: perfTestMeasureValue,
notifyAbort: perfTestNotifyAbort
};
}
var data;
onmessage = function(message) {
var action;
if (message.data.command === 'start') {
data = message.data;
initWorker(data.connectionType, 'http://' + location.host);
action = data.benchmarkName;
} else {
action = 'stop';
}
var config = getConfigForPerformanceTest(data.connectionType, data.dataType,
data.async, data.verifyData,
data.numIterations,
data.numWarmUpIterations);
doAction(config, data.isWorker, action);
};

View file

@ -76,21 +76,44 @@ function calculateSpeedInKB(size, timeSpentInMs) {
return Math.round(size / timeSpentInMs * 1000) / 1000; return Math.round(size / timeSpentInMs * 1000) / 1000;
} }
function calculateAndLogResult(config, size, startTimeInMs, totalSize) { function calculateAndLogResult(config, size, startTimeInMs, totalSize,
isWarmUp) {
var timeSpentInMs = getTimeStamp() - startTimeInMs; var timeSpentInMs = getTimeStamp() - startTimeInMs;
var speed = calculateSpeedInKB(totalSize, timeSpentInMs); var speed = calculateSpeedInKB(totalSize, timeSpentInMs);
var timePerMessageInMs = timeSpentInMs / (totalSize / size); var timePerMessageInMs = timeSpentInMs / (totalSize / size);
if (!results[size]) { if (!isWarmUp) {
results[size] = {n: 0, sum_t: 0, sum_t2: 0}; config.measureValue(timePerMessageInMs);
if (!results[size]) {
results[size] = {n: 0, sum_t: 0, sum_t2: 0};
}
results[size].n ++;
results[size].sum_t += timePerMessageInMs;
results[size].sum_t2 += timePerMessageInMs * timePerMessageInMs;
} }
config.measureValue(timePerMessageInMs);
results[size].n ++;
results[size].sum_t += timePerMessageInMs;
results[size].sum_t2 += timePerMessageInMs * timePerMessageInMs;
config.addToLog(formatResultInKiB(size, timePerMessageInMs, -1, speed, config.addToLog(formatResultInKiB(size, timePerMessageInMs, -1, speed,
config.printSize)); config.printSize));
} }
function repeatString(str, count) {
var data = '';
var expChunk = str;
var remain = count;
while (true) {
if (remain % 2) {
data += expChunk;
remain = (remain - 1) / 2;
} else {
remain /= 2;
}
if (remain == 0)
break;
expChunk = expChunk + expChunk;
}
return data;
}
function fillArrayBuffer(buffer, c) { function fillArrayBuffer(buffer, c) {
var i; var i;
@ -175,3 +198,130 @@ function cloneConfig(obj) {
} }
return newObj; return newObj;
} }
var tasks = [];
function runNextTask(config) {
var task = tasks.shift();
if (task == undefined) {
config.addToLog('Finished');
cleanup();
return;
}
timerID = setTimeout(task, 0);
}
function buildLegendString(config) {
var legend = ''
if (config.printSize)
legend = 'Message size in KiB, Time/message in ms, ';
legend += 'Speed in kB/s';
return legend;
}
function addTasks(config, stepFunc) {
for (var i = 0;
i < config.numWarmUpIterations + config.numIterations; ++i) {
var multiplierIndex = 0;
for (var size = config.startSize;
size <= config.stopThreshold;
++multiplierIndex) {
var task = stepFunc.bind(
null,
size,
config,
i < config.numWarmUpIterations);
tasks.push(task);
var multiplier = config.multipliers[
multiplierIndex % config.multipliers.length];
if (multiplier <= 1) {
config.addToLog('Invalid multiplier ' + multiplier);
config.notifyAbort();
throw new Error('Invalid multipler');
}
size = Math.ceil(size * multiplier);
}
}
}
function addResultReportingTask(config, title) {
tasks.push(function(){
timerID = null;
config.addToSummary(title);
reportAverageData(config);
clearAverageData();
runNextTask(config);
});
}
function sendBenchmark(config) {
config.addToLog('Send benchmark');
config.addToLog(buildLegendString(config));
tasks = [];
clearAverageData();
addTasks(config, sendBenchmarkStep);
addResultReportingTask(config, 'Send Benchmark ' + getConfigString(config));
startBenchmark(config);
}
function receiveBenchmark(config) {
config.addToLog('Receive benchmark');
config.addToLog(buildLegendString(config));
tasks = [];
clearAverageData();
addTasks(config, receiveBenchmarkStep);
addResultReportingTask(config,
'Receive Benchmark ' + getConfigString(config));
startBenchmark(config);
}
function stop(config) {
clearTimeout(timerID);
timerID = null;
tasks = [];
config.addToLog('Stopped');
cleanup();
}
var worker;
function initWorker(connectionType, origin) {
var scriptPath =
connectionType === 'WebSocket' ? '/benchmark.js' :
connectionType === 'XHR' ? '/xhr_benchmark.js' :
'/fetch_benchmark.js'; // connectionType === 'fetch'
worker = new Worker(origin + scriptPath);
}
function doAction(config, isWindowToWorker, action) {
if (isWindowToWorker) {
worker.onmessage = function(addToLog, addToSummary,
measureValue, notifyAbort, message) {
if (message.data.type === 'addToLog')
addToLog(message.data.data);
else if (message.data.type === 'addToSummary')
addToSummary(message.data.data);
else if (message.data.type === 'measureValue')
measureValue(message.data.data);
else if (message.data.type === 'notifyAbort')
notifyAbort();
}.bind(undefined, config.addToLog, config.addToSummary,
config.measureValue, config.notifyAbort);
config.addToLog = undefined;
config.addToSummary = undefined;
config.measureValue = undefined;
config.notifyAbort = undefined;
worker.postMessage({type: action, config: config});
} else {
if (action === 'sendBenchmark')
sendBenchmark(config);
else if (action === 'receiveBenchmark')
receiveBenchmark(config);
else if (action === 'batchBenchmark')
batchBenchmark(config);
else if (action === 'stop')
stop(config);
}
}

View file

@ -33,6 +33,12 @@ function addToSummary(log) {
function measureValue(value) { function measureValue(value) {
} }
// config.notifyAbort is called when the benchmark failed and aborted, and
// intended to be used in Performance Tests.
// Do nothing here in non-PerformanceTest.
function notifyAbort() {
}
function getIntFromInput(id) { function getIntFromInput(id) {
return parseInt(document.getElementById(id).value); return parseInt(document.getElementById(id).value);
} }
@ -53,11 +59,7 @@ function getIntArrayFromInput(id) {
return strArray.map(function(str) { return parseInt(str, 10); }); return strArray.map(function(str) { return parseInt(str, 10); });
} }
function onMessage(message) { function getFloatArrayFromInput(id) {
if (message.data.type === 'addToLog') var strArray = document.getElementById(id).value.split(',');
addToLog(message.data.data); return strArray.map(parseFloat);
else if (message.data.type === 'addToSummary')
addToSummary(message.data.data);
else if (message.data.type === 'measureValue')
measureValue(message.data.data);
} }

View file

@ -0,0 +1,20 @@
// Copyright 2014 Google Inc. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the COPYING file or at
// https://developers.google.com/open-source/licenses/bsd
// Utilities for example applications (for the worker threads only).
onmessage = function (message) {
var config = message.data.config;
config.addToLog = function(text) {
postMessage({type: 'addToLog', data: text}); };
config.addToSummary = function(text) {
postMessage({type: 'addToSummary', data: text}); };
config.measureValue = function(value) {
postMessage({type: 'measureValue', data: value}); };
config.notifyAbort = function() { postMessage({type: 'notifyAbort'}); };
doAction(config, false, message.data.type);
};

View file

@ -50,70 +50,36 @@ function getConfig() {
startSize: getIntFromInput('startsize'), startSize: getIntFromInput('startsize'),
// Stops benchmark when the size of message exceeds this threshold. // Stops benchmark when the size of message exceeds this threshold.
stopThreshold: getIntFromInput('stopthreshold'), stopThreshold: getIntFromInput('stopthreshold'),
// If the size of each message is small, send/receive multiple messages multipliers: getFloatArrayFromInput('multipliers'),
// until the sum of sizes reaches this threshold. verifyData: getBoolFromCheckBox('verifydata'),
// minTotal: getIntFromInput('mintotal'), methodAndCache: getStringFromRadioBox('methodandcache'),
// minTotal is not yet implemented on XHR benchmark addToLog: addToLog,
multipliers: getIntArrayFromInput('multipliers'), addToSummary: addToSummary,
verifyData: getBoolFromCheckBox('verifydata') measureValue: measureValue,
notifyAbort: notifyAbort
}; };
} }
var worker = new Worker('xhr_benchmark.js');
worker.onmessage = onMessage;
function onSendBenchmark() { function onSendBenchmark() {
var config = getConfig(); var config = getConfig();
config.dataType = getStringFromRadioBox('datatyperadio'); config.dataType = getStringFromRadioBox('datatyperadio');
doAction(config, getBoolFromCheckBox('worker'), 'sendBenchmark');
if (getBoolFromCheckBox('worker')) {
worker.postMessage({type: 'sendBenchmark', config: config});
} else {
config.addToLog = addToLog;
config.addToSummary = addToSummary;
config.measureValue = measureValue;
sendBenchmark(config);
}
} }
function onReceiveBenchmark() { function onReceiveBenchmark() {
var config = getConfig(); var config = getConfig();
config.dataType = getStringFromRadioBox('datatyperadio'); config.dataType = getStringFromRadioBox('datatyperadio');
doAction(config, getBoolFromCheckBox('worker'), 'receiveBenchmark');
if (getBoolFromCheckBox('worker')) {
worker.postMessage({type: 'receiveBenchmark', config: config});
} else {
config.addToLog = addToLog;
config.addToSummary = addToSummary;
config.measureValue = measureValue;
receiveBenchmark(config);
}
} }
function onBatchBenchmark() { function onBatchBenchmark() {
var config = getConfig(); var config = getConfig();
doAction(config, getBoolFromCheckBox('worker'), 'batchBenchmark');
if (getBoolFromCheckBox('worker')) {
worker.postMessage({type: 'batchBenchmark', config: config});
} else {
config.addToLog = addToLog;
config.addToSummary = addToSummary;
config.measureValue = measureValue;
batchBenchmark(config);
}
} }
function onStop() { function onStop() {
var config = getConfig(); var config = getConfig();
doAction(config, getBoolFromCheckBox('worker'), 'stop');
if (getBoolFromCheckBox('worker')) {
worker.postMessage({type: 'stop', config: config});
} else {
config.addToLog = addToLog;
config.addToSummary = addToSummary;
config.measureValue = measureValue;
stop(config);
}
} }
function init() { function init() {
@ -127,6 +93,8 @@ function init() {
addToLog(window.navigator.userAgent.toLowerCase()); addToLog(window.navigator.userAgent.toLowerCase());
addToSummary(window.navigator.userAgent.toLowerCase()); addToSummary(window.navigator.userAgent.toLowerCase());
initWorker('XHR', '');
} }
</script> </script>
</head> </head>
@ -178,10 +146,6 @@ function init() {
<td>Stop threshold</td> <td>Stop threshold</td>
<td><input type="text" id="stopthreshold" value="102400000"></td> <td><input type="text" id="stopthreshold" value="102400000"></td>
</tr> </tr>
<tr>
<td>Minimum total</td>
<td><input type="text" id="mintotal" value="102400000"></td>
</tr>
<tr> <tr>
<td>Multipliers</td> <td>Multipliers</td>
<td><input type="text" id="multipliers" value="5, 2"></td> <td><input type="text" id="multipliers" value="5, 2"></td>
@ -204,6 +168,26 @@ function init() {
id="datatyperadioarraybuffer" id="datatyperadioarraybuffer"
value="arraybuffer" value="arraybuffer"
><label for="datatyperadioarraybuffer">arraybuffer</label> ><label for="datatyperadioarraybuffer">arraybuffer</label>
<br>
Set HTTP method and cache control
<input type="radio"
name="methodandcache"
id="methodandcachePOST"
value="POST"
checked><label for="methodandcachePOST">POST (No Cache)</label>
<input type="radio"
name="methodandcache"
id="methodandcacheGETNOCACHE"
value="GET-NOCACHE"
><label for="methodandcacheGETNOCACHE">GET (No Cache)</label>
<input type="radio"
name="methodandcache"
id="methodandcacheGETCACHE"
value="GET-CACHE"
><label for="methodandcacheGETCACHE">GET (Cache)</label>
<br>
<span style="font-size: 80%">(Cache control: receive only. Cache is valid for 10 seconds. This config controls Cache-control HTTP response header. Browsers might not cache data e.g. when it is large)</span>
</form> </form>
<div id="log_div"> <div id="log_div">
@ -216,7 +200,13 @@ function init() {
id="summary" rows="20" style="width: 100%" readonly></textarea> id="summary" rows="20" style="width: 100%" readonly></textarea>
</div> </div>
Note: Effect of RTT and time spent for ArrayBuffer creation in receive benchmarks are not eliminated. <div id="note_div">
Note:
<ul>
<li>Effect of RTT and time spent for ArrayBuffer creation in receive benchmarks are not eliminated.</li>
<li>The Stddev column shows NaN when the number of iterations is set to 1.</li>
</ul>
</div>
</body> </body>
</html> </html>

View file

@ -33,27 +33,7 @@ function destroyAllXHRs() {
// gc() might be needed for Chrome/Blob // gc() might be needed for Chrome/Blob
} }
function repeatString(str, count) { function sendBenchmarkStep(size, config, isWarmUp) {
var data = '';
var expChunk = str;
var remain = count;
while (true) {
if (remain % 2) {
data += expChunk;
remain = (remain - 1) / 2;
} else {
remain /= 2;
}
if (remain == 0)
break;
expChunk = expChunk + expChunk;
}
return data;
}
function sendBenchmarkStep(size, config) {
timerID = null; timerID = null;
benchmark.startTimeInMs = null; benchmark.startTimeInMs = null;
@ -68,12 +48,14 @@ function sendBenchmarkStep(size, config) {
if (this.status != 200) { if (this.status != 200) {
config.addToLog('Failed (status=' + this.status + ')'); config.addToLog('Failed (status=' + this.status + ')');
destroyAllXHRs(); destroyAllXHRs();
config.notifyAbort();
return; return;
} }
if (config.verifyData && if (config.verifyData &&
!verifyAcknowledgement(config, this.response, size)) { !verifyAcknowledgement(config, this.response, size)) {
destroyAllXHRs(); destroyAllXHRs();
config.notifyAbort();
return; return;
} }
@ -86,10 +68,17 @@ function sendBenchmarkStep(size, config) {
if (benchmark.startTimeInMs == null) { if (benchmark.startTimeInMs == null) {
config.addToLog('startTimeInMs not set'); config.addToLog('startTimeInMs not set');
destroyAllXHRs(); destroyAllXHRs();
config.notifyAbort();
return; return;
} }
calculateAndLogResult(config, size, benchmark.startTimeInMs, totalSize); // Check and warn if proxy is enabled.
if (this.getResponseHeader('Via') !== null) {
config.addToLog('WARNING: proxy seems enabled.');
}
calculateAndLogResult(config, size, benchmark.startTimeInMs, totalSize,
isWarmUp);
destroyAllXHRs(); destroyAllXHRs();
@ -134,7 +123,7 @@ function sendBenchmarkStep(size, config) {
} }
} }
function receiveBenchmarkStep(size, config) { function receiveBenchmarkStep(size, config, isWarmUp) {
timerID = null; timerID = null;
benchmark.startTimeInMs = null; benchmark.startTimeInMs = null;
@ -145,6 +134,7 @@ function receiveBenchmarkStep(size, config) {
if (!verificationResult) { if (!verificationResult) {
config.addToLog('Response verification failed'); config.addToLog('Response verification failed');
destroyAllXHRs(); destroyAllXHRs();
config.notifyAbort();
return; return;
} }
@ -157,10 +147,12 @@ function receiveBenchmarkStep(size, config) {
if (benchmark.startTimeInMs == null) { if (benchmark.startTimeInMs == null) {
config.addToLog('startTimeInMs not set'); config.addToLog('startTimeInMs not set');
destroyAllXHRs(); destroyAllXHRs();
config.notifyAbort();
return; return;
} }
calculateAndLogResult(config, size, benchmark.startTimeInMs, totalSize); calculateAndLogResult(config, size, benchmark.startTimeInMs, totalSize,
isWarmUp);
destroyAllXHRs(); destroyAllXHRs();
@ -175,9 +167,15 @@ function receiveBenchmarkStep(size, config) {
if (this.status != 200) { if (this.status != 200) {
config.addToLog('Failed (status=' + this.status + ')'); config.addToLog('Failed (status=' + this.status + ')');
destroyAllXHRs(); destroyAllXHRs();
config.notifyAbort();
return; return;
} }
// Check and warn if proxy is enabled.
if (this.getResponseHeader('Via') !== null) {
config.addToLog('WARNING: proxy seems enabled.');
}
var bytesReceived = -1; var bytesReceived = -1;
if (this.responseType == 'arraybuffer') { if (this.responseType == 'arraybuffer') {
bytesReceived = this.response.byteLength; bytesReceived = this.response.byteLength;
@ -190,6 +188,7 @@ function receiveBenchmarkStep(size, config) {
config.addToLog('Expected ' + size + config.addToLog('Expected ' + size +
'B but received ' + bytesReceived + 'B'); 'B but received ' + bytesReceived + 'B');
destroyAllXHRs(); destroyAllXHRs();
config.notifyAbort();
return; return;
} }
@ -220,9 +219,21 @@ function receiveBenchmarkStep(size, config) {
for (var i = 0; i < xhrs.length; ++i) { for (var i = 0; i < xhrs.length; ++i) {
var xhr = xhrs[i]; var xhr = xhrs[i];
xhr.open('POST', config.prefixUrl + '_receive', config.async); if (config.methodAndCache === 'GET-NOCACHE') {
xhr.responseType = config.dataType; xhr.open('GET', config.prefixUrl + '_receive_getnocache?' + size,
xhr.send(size + ' none'); config.async);
xhr.responseType = config.dataType;
xhr.send();
} else if (config.methodAndCache === 'GET-CACHE') {
xhr.open('GET', config.prefixUrl + '_receive_getcache?' + size,
config.async);
xhr.responseType = config.dataType;
xhr.send();
} else {
xhr.open('POST', config.prefixUrl + '_receive', config.async);
xhr.responseType = config.dataType;
xhr.send(size + ' none');
}
} }
} }
@ -245,94 +256,6 @@ function startBenchmark(config) {
runNextTask(config); runNextTask(config);
} }
// TODO(hiroshige): the following code is the same as benchmark.html
// and some of them should be merged into e.g. util.js
var tasks = [];
function runNextTask(config) {
var task = tasks.shift();
if (task == undefined) {
config.addToLog('Finished');
destroyAllXHRs();
return;
}
timerID = setTimeout(task, 0);
}
function buildLegendString(config) {
var legend = ''
if (config.printSize)
legend = 'Message size in KiB, Time/message in ms, ';
legend += 'Speed in kB/s';
return legend;
}
function addTasks(config, stepFunc) {
for (var i = 0;
i < config.numWarmUpIterations + config.numIterations; ++i) {
// Ignore the first |config.numWarmUpIterations| iterations.
if (i == config.numWarmUpIterations)
addResultClearingTask(config);
var multiplierIndex = 0;
for (var size = config.startSize;
size <= config.stopThreshold;
++multiplierIndex) {
var task = stepFunc.bind(
null,
size,
config);
tasks.push(task);
size *= config.multipliers[
multiplierIndex % config.multipliers.length];
}
}
}
function addResultReportingTask(config, title) {
tasks.push(function(){
timerID = null;
config.addToSummary(title);
reportAverageData(config);
clearAverageData();
runNextTask(config);
});
}
function addResultClearingTask(config) {
tasks.push(function(){
timerID = null;
clearAverageData();
runNextTask(config);
});
}
// --------------------------------
function sendBenchmark(config) {
config.addToLog('Send benchmark');
config.addToLog(buildLegendString(config));
tasks = [];
clearAverageData();
addTasks(config, sendBenchmarkStep);
addResultReportingTask(config, 'Send Benchmark ' + getConfigString(config));
startBenchmark(config);
}
function receiveBenchmark(config) {
config.addToLog('Receive benchmark');
config.addToLog(buildLegendString(config));
tasks = [];
clearAverageData();
addTasks(config, receiveBenchmarkStep);
addResultReportingTask(config,
'Receive Benchmark ' + getConfigString(config));
startBenchmark(config);
}
function batchBenchmark(originalConfig) { function batchBenchmark(originalConfig) {
originalConfig.addToLog('Batch benchmark'); originalConfig.addToLog('Batch benchmark');
@ -365,25 +288,5 @@ function batchBenchmark(originalConfig) {
startBenchmark(config); startBenchmark(config);
} }
function cleanup() {
function stop(config) {
destroyAllXHRs();
clearTimeout(timerID);
timerID = null;
config.addToLog('Stopped');
} }
onmessage = function (message) {
var config = message.data.config;
config.addToLog = workerAddToLog;
config.addToSummary = workerAddToSummary;
config.measureValue = workerMeasureValue;
if (message.data.type === 'sendBenchmark')
sendBenchmark(config);
else if (message.data.type === 'receiveBenchmark')
receiveBenchmark(config);
else if (message.data.type === 'batchBenchmark')
batchBenchmark(config);
else if (message.data.type === 'stop')
stop(config);
};

View file

@ -12,15 +12,19 @@ https://developers.google.com/open-source/licenses/bsd
<script src="util_main.js"></script> <script src="util_main.js"></script>
<script> <script>
var events = []; var events = [];
var startTime = 0;
function run() { function run() {
events = []; events = [];
startTime = Date.now();
function pushToLog(type) { function pushToLog(type) {
var time = Date.now();
if (events.length != 0 && type === events[events.length - 1].type) { if (events.length != 0 && type === events[events.length - 1].type) {
events[events.length - 1].count += 1; events[events.length - 1].count += 1;
events[events.length - 1].last = time;
} else { } else {
events.push({type: type, count: 1}); events.push({type: type, count: 1, first: time, last: time});
} }
} }
@ -85,14 +89,23 @@ function print() {
var result = ''; var result = '';
for (var i = 0; i < events.length; ++i) { for (var i = 0; i < events.length; ++i) {
var event = events[i]; var event = events[i];
result += event.type + ' * ' + event.count + '\n'; var line = '';
line += (event.first - startTime) + "ms";
if (event.count > 1)
line += "-" + (event.last - startTime) + "ms";
else
line += " ";
while(line.length < 15)
line += " ";
line += ": " + event.type + ' * ' + event.count + '\n';
result += line;
} }
document.getElementById('log').value = result; document.getElementById('log').value = result;
} }
</script> </script>
<body> <body>
<textarea id="log" rows="10" cols="40" readonly></textarea> <textarea id="log" rows="10" cols="70" readonly></textarea>
<br/> <br/>
Size: <input type="text" id="size" value="65536"><br/> Size: <input type="text" id="size" value="65536"><br/>
<input type="checkbox" id="chunkedresponse"> <input type="checkbox" id="chunkedresponse">

View file

@ -0,0 +1,6 @@
<!DOCTYPE html>
<head>
<script src="util.js"></script>
<script src="performance_test_iframe.js"></script>
<script src="xhr_benchmark.js"></script>
</head>

View file

@ -807,7 +807,8 @@ class Stream(StreamBase):
def close_connection(self, code=common.STATUS_NORMAL_CLOSURE, reason='', def close_connection(self, code=common.STATUS_NORMAL_CLOSURE, reason='',
wait_response=True): wait_response=True):
"""Closes a WebSocket connection. """Closes a WebSocket connection. Note that this method blocks until
it receives acknowledgement to the closing handshake.
Args: Args:
code: Status code for close frame. If code is None, a close code: Status code for close frame. If code is None, a close
@ -824,6 +825,12 @@ class Stream(StreamBase):
'Requested close_connection but server is already terminated') 'Requested close_connection but server is already terminated')
return return
# When we receive a close frame, we call _process_close_message().
# _process_close_message() immediately acknowledges to the
# server-initiated closing handshake and sets server_terminated to
# True. So, here we can assume that we haven't received any close
# frame. We're initiating a closing handshake.
if code is None: if code is None:
if reason is not None and len(reason) > 0: if reason is not None and len(reason) > 0:
raise BadOperationException( raise BadOperationException(

View file

@ -102,10 +102,8 @@ SEC_WEBSOCKET_LOCATION_HEADER = 'Sec-WebSocket-Location'
# Extensions # Extensions
DEFLATE_FRAME_EXTENSION = 'deflate-frame' DEFLATE_FRAME_EXTENSION = 'deflate-frame'
PERMESSAGE_COMPRESSION_EXTENSION = 'permessage-compress'
PERMESSAGE_DEFLATE_EXTENSION = 'permessage-deflate' PERMESSAGE_DEFLATE_EXTENSION = 'permessage-deflate'
X_WEBKIT_DEFLATE_FRAME_EXTENSION = 'x-webkit-deflate-frame' X_WEBKIT_DEFLATE_FRAME_EXTENSION = 'x-webkit-deflate-frame'
X_WEBKIT_PERMESSAGE_COMPRESSION_EXTENSION = 'x-webkit-permessage-compress'
MUX_EXTENSION = 'mux_DO_NOT_USE' MUX_EXTENSION = 'mux_DO_NOT_USE'
# Status codes # Status codes
@ -153,9 +151,8 @@ def is_control_opcode(opcode):
class ExtensionParameter(object): class ExtensionParameter(object):
"""Holds information about an extension which is exchanged on extension
negotiation in opening handshake. """This is exchanged on extension negotiation in opening handshake."""
"""
def __init__(self, name): def __init__(self, name):
self._name = name self._name = name
@ -166,30 +163,39 @@ class ExtensionParameter(object):
self._parameters = [] self._parameters = []
def name(self): def name(self):
"""Return the extension name."""
return self._name return self._name
def add_parameter(self, name, value): def add_parameter(self, name, value):
"""Add a parameter."""
self._parameters.append((name, value)) self._parameters.append((name, value))
def get_parameters(self): def get_parameters(self):
"""Return the parameters."""
return self._parameters return self._parameters
def get_parameter_names(self): def get_parameter_names(self):
"""Return the names of the parameters."""
return [name for name, unused_value in self._parameters] return [name for name, unused_value in self._parameters]
def has_parameter(self, name): def has_parameter(self, name):
"""Test if a parameter exists."""
for param_name, param_value in self._parameters: for param_name, param_value in self._parameters:
if param_name == name: if param_name == name:
return True return True
return False return False
def get_parameter_value(self, name): def get_parameter_value(self, name):
"""Get the value of a specific parameter."""
for param_name, param_value in self._parameters: for param_name, param_value in self._parameters:
if param_name == name: if param_name == name:
return param_value return param_value
class ExtensionParsingException(Exception): class ExtensionParsingException(Exception):
"""Exception to handle errors in extension parsing."""
def __init__(self, name): def __init__(self, name):
super(ExtensionParsingException, self).__init__(name) super(ExtensionParsingException, self).__init__(name)
@ -244,12 +250,11 @@ def _parse_extension(state):
def parse_extensions(data): def parse_extensions(data):
"""Parses Sec-WebSocket-Extensions header value returns a list of """Parse Sec-WebSocket-Extensions header value.
ExtensionParameter objects.
Returns a list of ExtensionParameter objects.
Leading LWSes must be trimmed. Leading LWSes must be trimmed.
""" """
state = http_header_util.ParsingState(data) state = http_header_util.ParsingState(data)
extension_list = [] extension_list = []
@ -279,8 +284,7 @@ def parse_extensions(data):
def format_extension(extension): def format_extension(extension):
"""Formats an ExtensionParameter object.""" """Format an ExtensionParameter object."""
formatted_params = [extension.name()] formatted_params = [extension.name()]
for param_name, param_value in extension.get_parameters(): for param_name, param_value in extension.get_parameters():
if param_value is None: if param_value is None:
@ -292,8 +296,7 @@ def format_extension(extension):
def format_extensions(extension_list): def format_extensions(extension_list):
"""Formats a list of ExtensionParameter objects.""" """Format a list of ExtensionParameter objects."""
formatted_extension_list = [] formatted_extension_list = []
for extension in extension_list: for extension in extension_list:
formatted_extension_list.append(format_extension(extension)) formatted_extension_list.append(format_extension(extension))

View file

@ -327,103 +327,8 @@ _available_processors[common.X_WEBKIT_DEFLATE_FRAME_EXTENSION] = (
_compression_extension_names.append(common.X_WEBKIT_DEFLATE_FRAME_EXTENSION) _compression_extension_names.append(common.X_WEBKIT_DEFLATE_FRAME_EXTENSION)
def _parse_compression_method(data):
"""Parses the value of "method" extension parameter."""
return common.parse_extensions(data)
def _create_accepted_method_desc(method_name, method_params):
"""Creates accepted-method-desc from given method name and parameters"""
extension = common.ExtensionParameter(method_name)
for name, value in method_params:
extension.add_parameter(name, value)
return common.format_extension(extension)
class CompressionExtensionProcessorBase(ExtensionProcessorInterface):
"""Base class for perframe-compress and permessage-compress extension."""
_METHOD_PARAM = 'method'
def __init__(self, request):
ExtensionProcessorInterface.__init__(self, request)
self._logger = util.get_class_logger(self)
self._compression_method_name = None
self._compression_processor = None
self._compression_processor_hook = None
def name(self):
return ''
def _lookup_compression_processor(self, method_desc):
return None
def _get_compression_processor_response(self):
"""Looks up the compression processor based on the self._request and
returns the compression processor's response.
"""
method_list = self._request.get_parameter_value(self._METHOD_PARAM)
if method_list is None:
return None
methods = _parse_compression_method(method_list)
if methods is None:
return None
comression_processor = None
# The current implementation tries only the first method that matches
# supported algorithm. Following methods aren't tried even if the
# first one is rejected.
# TODO(bashi): Need to clarify this behavior.
for method_desc in methods:
compression_processor = self._lookup_compression_processor(
method_desc)
if compression_processor is not None:
self._compression_method_name = method_desc.name()
break
if compression_processor is None:
return None
if self._compression_processor_hook:
self._compression_processor_hook(compression_processor)
processor_response = compression_processor.get_extension_response()
if processor_response is None:
return None
self._compression_processor = compression_processor
return processor_response
def _get_extension_response_internal(self):
processor_response = self._get_compression_processor_response()
if processor_response is None:
return None
response = common.ExtensionParameter(self._request.name())
accepted_method_desc = _create_accepted_method_desc(
self._compression_method_name,
processor_response.get_parameters())
response.add_parameter(self._METHOD_PARAM, accepted_method_desc)
self._logger.debug(
'Enable %s extension (method: %s)' %
(self._request.name(), self._compression_method_name))
return response
def _setup_stream_options_internal(self, stream_options):
if self._compression_processor is None:
return
self._compression_processor.setup_stream_options(stream_options)
def set_compression_processor_hook(self, hook):
self._compression_processor_hook = hook
def get_compression_processor(self):
return self._compression_processor
class PerMessageDeflateExtensionProcessor(ExtensionProcessorInterface): class PerMessageDeflateExtensionProcessor(ExtensionProcessorInterface):
"""permessage-deflate extension processor. It's also used for """permessage-deflate extension processor.
permessage-compress extension when the deflate method is chosen.
Specification: Specification:
http://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-08 http://tools.ietf.org/html/draft-ietf-hybi-permessage-compression-08
@ -434,15 +339,8 @@ class PerMessageDeflateExtensionProcessor(ExtensionProcessorInterface):
_CLIENT_MAX_WINDOW_BITS_PARAM = 'client_max_window_bits' _CLIENT_MAX_WINDOW_BITS_PARAM = 'client_max_window_bits'
_CLIENT_NO_CONTEXT_TAKEOVER_PARAM = 'client_no_context_takeover' _CLIENT_NO_CONTEXT_TAKEOVER_PARAM = 'client_no_context_takeover'
def __init__(self, request, draft08=True): def __init__(self, request):
"""Construct PerMessageDeflateExtensionProcessor """Construct PerMessageDeflateExtensionProcessor."""
Args:
draft08: Follow the constraints on the parameters that were not
specified for permessage-compress but are specified for
permessage-deflate as on
draft-ietf-hybi-permessage-compression-08.
"""
ExtensionProcessorInterface.__init__(self, request) ExtensionProcessorInterface.__init__(self, request)
self._logger = util.get_class_logger(self) self._logger = util.get_class_logger(self)
@ -450,22 +348,18 @@ class PerMessageDeflateExtensionProcessor(ExtensionProcessorInterface):
self._preferred_client_max_window_bits = None self._preferred_client_max_window_bits = None
self._client_no_context_takeover = False self._client_no_context_takeover = False
self._draft08 = draft08
def name(self): def name(self):
# This method returns "deflate" (not "permessage-deflate") for
# compatibility.
return 'deflate' return 'deflate'
def _get_extension_response_internal(self): def _get_extension_response_internal(self):
if self._draft08: for name in self._request.get_parameter_names():
for name in self._request.get_parameter_names(): if name not in [self._SERVER_MAX_WINDOW_BITS_PARAM,
if name not in [self._SERVER_MAX_WINDOW_BITS_PARAM, self._SERVER_NO_CONTEXT_TAKEOVER_PARAM,
self._SERVER_NO_CONTEXT_TAKEOVER_PARAM, self._CLIENT_MAX_WINDOW_BITS_PARAM]:
self._CLIENT_MAX_WINDOW_BITS_PARAM]: self._logger.debug('Unknown parameter: %r', name)
self._logger.debug('Unknown parameter: %r', name) return None
return None
else:
# Any unknown parameter will be just ignored.
pass
server_max_window_bits = None server_max_window_bits = None
if self._request.has_parameter(self._SERVER_MAX_WINDOW_BITS_PARAM): if self._request.has_parameter(self._SERVER_MAX_WINDOW_BITS_PARAM):
@ -494,8 +388,7 @@ class PerMessageDeflateExtensionProcessor(ExtensionProcessorInterface):
# accept client_max_window_bits from a server or not. # accept client_max_window_bits from a server or not.
client_client_max_window_bits = self._request.has_parameter( client_client_max_window_bits = self._request.has_parameter(
self._CLIENT_MAX_WINDOW_BITS_PARAM) self._CLIENT_MAX_WINDOW_BITS_PARAM)
if (self._draft08 and if (client_client_max_window_bits and
client_client_max_window_bits and
self._request.get_parameter_value( self._request.get_parameter_value(
self._CLIENT_MAX_WINDOW_BITS_PARAM) is not None): self._CLIENT_MAX_WINDOW_BITS_PARAM) is not None):
self._logger.debug('%s parameter must not have a value in a ' self._logger.debug('%s parameter must not have a value in a '
@ -529,7 +422,7 @@ class PerMessageDeflateExtensionProcessor(ExtensionProcessorInterface):
self._SERVER_NO_CONTEXT_TAKEOVER_PARAM, None) self._SERVER_NO_CONTEXT_TAKEOVER_PARAM, None)
if self._preferred_client_max_window_bits is not None: if self._preferred_client_max_window_bits is not None:
if self._draft08 and not client_client_max_window_bits: if not client_client_max_window_bits:
self._logger.debug('Processor is configured to use %s but ' self._logger.debug('Processor is configured to use %s but '
'the client cannot accept it', 'the client cannot accept it',
self._CLIENT_MAX_WINDOW_BITS_PARAM) self._CLIENT_MAX_WINDOW_BITS_PARAM)
@ -765,33 +658,6 @@ _available_processors[common.PERMESSAGE_DEFLATE_EXTENSION] = (
_compression_extension_names.append('deflate') _compression_extension_names.append('deflate')
class PerMessageCompressExtensionProcessor(
CompressionExtensionProcessorBase):
"""permessage-compress extension processor.
Specification:
http://tools.ietf.org/html/draft-ietf-hybi-permessage-compression
"""
_DEFLATE_METHOD = 'deflate'
def __init__(self, request):
CompressionExtensionProcessorBase.__init__(self, request)
def name(self):
return common.PERMESSAGE_COMPRESSION_EXTENSION
def _lookup_compression_processor(self, method_desc):
if method_desc.name() == self._DEFLATE_METHOD:
return PerMessageDeflateExtensionProcessor(method_desc, False)
return None
_available_processors[common.PERMESSAGE_COMPRESSION_EXTENSION] = (
PerMessageCompressExtensionProcessor)
_compression_extension_names.append(common.PERMESSAGE_COMPRESSION_EXTENSION)
class MuxExtensionProcessor(ExtensionProcessorInterface): class MuxExtensionProcessor(ExtensionProcessorInterface):
"""WebSocket multiplexing extension processor.""" """WebSocket multiplexing extension processor."""
@ -825,10 +691,9 @@ class MuxExtensionProcessor(ExtensionProcessorInterface):
else: else:
# Mux extension should not be applied before any history-based # Mux extension should not be applied before any history-based
# compression extension. # compression extension.
if (name == common.DEFLATE_FRAME_EXTENSION or if (name == 'deflate' or
name == common.X_WEBKIT_DEFLATE_FRAME_EXTENSION or name == common.DEFLATE_FRAME_EXTENSION or
name == common.PERMESSAGE_COMPRESSION_EXTENSION or name == common.X_WEBKIT_DEFLATE_FRAME_EXTENSION):
name == common.X_WEBKIT_PERMESSAGE_COMPRESSION_EXTENSION):
self.set_active(False) self.set_active(False)
return return

View file

@ -72,6 +72,7 @@ _PYOPT_ALLOW_DRAFT75_DEFINITION = {'off': False, 'on': True}
class ApacheLogHandler(logging.Handler): class ApacheLogHandler(logging.Handler):
"""Wrapper logging.Handler to emit log message to apache's error.log.""" """Wrapper logging.Handler to emit log message to apache's error.log."""
_LEVELS = { _LEVELS = {
@ -136,6 +137,7 @@ _LOGGER = logging.getLogger(__name__)
def _parse_option(name, value, definition): def _parse_option(name, value, definition):
"""Return the meaning of a option value."""
if value is None: if value is None:
return False return False
@ -147,6 +149,7 @@ def _parse_option(name, value, definition):
def _create_dispatcher(): def _create_dispatcher():
"""Initialize a dispatch.Dispatcher."""
_LOGGER.info('Initializing Dispatcher') _LOGGER.info('Initializing Dispatcher')
options = apache.main_server.get_options() options = apache.main_server.get_options()
@ -187,7 +190,6 @@ def headerparserhandler(request):
This function is named headerparserhandler because it is the default This function is named headerparserhandler because it is the default
name for a PythonHeaderParserHandler. name for a PythonHeaderParserHandler.
""" """
handshake_is_done = False handshake_is_done = False
try: try:
# Fallback to default http handler for request paths for which # Fallback to default http handler for request paths for which
@ -234,7 +236,8 @@ def headerparserhandler(request):
request._dispatcher = _dispatcher request._dispatcher = _dispatcher
_dispatcher.transfer_data(request) _dispatcher.transfer_data(request)
except handshake.AbortedByUserException, e: except handshake.AbortedByUserException, e:
request.log_error('mod_pywebsocket: Aborted: %s' % e, apache.APLOG_INFO) request.log_error('mod_pywebsocket: Aborted: %s' % e,
apache.APLOG_INFO)
except Exception, e: except Exception, e:
# DispatchException can also be thrown if something is wrong in # DispatchException can also be thrown if something is wrong in
# pywebsocket code. It's caught here, then. # pywebsocket code. It's caught here, then.

View file

@ -40,6 +40,7 @@ import sys
class MemorizingFile(object): class MemorizingFile(object):
"""MemorizingFile wraps a file and memorizes lines read by readline. """MemorizingFile wraps a file and memorizes lines read by readline.
Note that data read by other methods are not memorized. This behavior Note that data read by other methods are not memorized. This behavior
@ -56,7 +57,6 @@ class MemorizingFile(object):
Only the first max_memorized_lines are memorized. Only the first max_memorized_lines are memorized.
Default: sys.maxint. Default: sys.maxint.
""" """
self._file = file_ self._file = file_
self._memorized_lines = [] self._memorized_lines = []
self._max_memorized_lines = max_memorized_lines self._max_memorized_lines = max_memorized_lines
@ -64,6 +64,11 @@ class MemorizingFile(object):
self._buffered_line = None self._buffered_line = None
def __getattribute__(self, name): def __getattribute__(self, name):
"""Return a file attribute.
Returns the value overridden by this class for some attributes,
and forwards the call to _file for the other attributes.
"""
if name in ('_file', '_memorized_lines', '_max_memorized_lines', if name in ('_file', '_memorized_lines', '_max_memorized_lines',
'_buffered', '_buffered_line', 'readline', '_buffered', '_buffered_line', 'readline',
'get_memorized_lines'): 'get_memorized_lines'):
@ -77,7 +82,6 @@ class MemorizingFile(object):
the whole line will be read out from underlying file object by the whole line will be read out from underlying file object by
subsequent readline calls. subsequent readline calls.
""" """
if self._buffered: if self._buffered:
line = self._buffered_line line = self._buffered_line
self._buffered = False self._buffered = False

View file

@ -104,23 +104,31 @@ _DROP_CODE_BAD_FRAGMENTATION = 3009
class MuxUnexpectedException(Exception): class MuxUnexpectedException(Exception):
"""Exception in handling multiplexing extension.""" """Exception in handling multiplexing extension."""
pass pass
# Temporary # Temporary
class MuxNotImplementedException(Exception): class MuxNotImplementedException(Exception):
"""Raised when a flow enters unimplemented code path.""" """Raised when a flow enters unimplemented code path."""
pass pass
class LogicalConnectionClosedException(Exception): class LogicalConnectionClosedException(Exception):
"""Raised when logical connection is gracefully closed.""" """Raised when logical connection is gracefully closed."""
pass pass
class PhysicalConnectionError(Exception): class PhysicalConnectionError(Exception):
"""Raised when there is a physical connection error.""" """Raised when there is a physical connection error."""
def __init__(self, drop_code, message=''): def __init__(self, drop_code, message=''):
super(PhysicalConnectionError, self).__init__( super(PhysicalConnectionError, self).__init__(
'code=%d, message=%r' % (drop_code, message)) 'code=%d, message=%r' % (drop_code, message))
@ -129,8 +137,11 @@ class PhysicalConnectionError(Exception):
class LogicalChannelError(Exception): class LogicalChannelError(Exception):
"""Raised when there is a logical channel error.""" """Raised when there is a logical channel error."""
def __init__(self, channel_id, drop_code, message=''): def __init__(self, channel_id, drop_code, message=''):
"""Initialize the error with a status message."""
super(LogicalChannelError, self).__init__( super(LogicalChannelError, self).__init__(
'channel_id=%d, code=%d, message=%r' % ( 'channel_id=%d, code=%d, message=%r' % (
channel_id, drop_code, message)) channel_id, drop_code, message))
@ -181,7 +192,7 @@ def _create_drop_channel(channel_id, code=None, message=''):
first_byte = _MUX_OPCODE_DROP_CHANNEL << 5 first_byte = _MUX_OPCODE_DROP_CHANNEL << 5
block = chr(first_byte) + _encode_channel_id(channel_id) block = chr(first_byte) + _encode_channel_id(channel_id)
if code is None: if code is None:
block += _encode_number(0) # Reason size block += _encode_number(0) # Reason size
else: else:
reason = struct.pack('!H', code) + message reason = struct.pack('!H', code) + message
reason_size = _encode_number(len(reason)) reason_size = _encode_number(len(reason))
@ -209,7 +220,7 @@ def _create_new_channel_slot(slots, send_quota):
def _create_fallback_new_channel_slot(): def _create_fallback_new_channel_slot():
first_byte = (_MUX_OPCODE_NEW_CHANNEL_SLOT << 5) | 1 # Set the F flag first_byte = (_MUX_OPCODE_NEW_CHANNEL_SLOT << 5) | 1 # Set the F flag
block = (chr(first_byte) + _encode_number(0) + _encode_number(0)) block = (chr(first_byte) + _encode_number(0) + _encode_number(0))
return block return block
@ -232,7 +243,9 @@ def _parse_request_text(request_text):
class _ControlBlock(object): class _ControlBlock(object):
"""A structure that holds parsing result of multiplexing control block. """A structure that holds parsing result of multiplexing control block.
Control block specific attributes will be added by _MuxFramePayloadParser. Control block specific attributes will be added by _MuxFramePayloadParser.
(e.g. encoded_handshake will be added for AddChannelRequest and (e.g. encoded_handshake will be added for AddChannelRequest and
AddChannelResponse) AddChannelResponse)
@ -243,6 +256,7 @@ class _ControlBlock(object):
class _MuxFramePayloadParser(object): class _MuxFramePayloadParser(object):
"""A class that parses multiplexed frame payload.""" """A class that parses multiplexed frame payload."""
def __init__(self, payload): def __init__(self, payload):
@ -251,13 +265,12 @@ class _MuxFramePayloadParser(object):
self._logger = util.get_class_logger(self) self._logger = util.get_class_logger(self)
def read_channel_id(self): def read_channel_id(self):
"""Reads channel id. """Read channel id.
Raises: Raises:
ValueError: when the payload doesn't contain ValueError: when the payload doesn't contain
valid channel id. valid channel id.
""" """
remaining_length = len(self._data) - self._read_position remaining_length = len(self._data) - self._read_position
pos = self._read_position pos = self._read_position
if remaining_length == 0: if remaining_length == 0:
@ -288,12 +301,11 @@ class _MuxFramePayloadParser(object):
return channel_id return channel_id
def read_inner_frame(self): def read_inner_frame(self):
"""Reads an inner frame. """Read an inner frame.
Raises: Raises:
PhysicalConnectionError: when the inner frame is invalid. PhysicalConnectionError: when the inner frame is invalid.
""" """
if len(self._data) == self._read_position: if len(self._data) == self._read_position:
raise PhysicalConnectionError( raise PhysicalConnectionError(
_DROP_CODE_ENCAPSULATED_FRAME_IS_TRUNCATED) _DROP_CODE_ENCAPSULATED_FRAME_IS_TRUNCATED)
@ -345,12 +357,11 @@ class _MuxFramePayloadParser(object):
return number return number
def _read_size_and_contents(self): def _read_size_and_contents(self):
"""Reads data that consists of followings: """Read data that consists of the following:
- the size of the contents encoded the same way as payload length - the size of the contents encoded the same way as payload length
of the WebSocket Protocol with 1 bit padding at the head. of the WebSocket Protocol with 1 bit padding at the head.
- the contents. - the contents.
""" """
try: try:
size = self._read_number() size = self._read_number()
except ValueError, e: except ValueError, e:
@ -455,14 +466,13 @@ class _MuxFramePayloadParser(object):
return control_block return control_block
def read_control_blocks(self): def read_control_blocks(self):
"""Reads control block(s). """Read control block(s).
Raises: Raises:
PhysicalConnectionError: when the payload contains invalid control PhysicalConnectionError: when the payload contains invalid control
block(s). block(s).
StopIteration: when no control blocks left. StopIteration: when no control blocks left.
""" """
while self._read_position < len(self._data): while self._read_position < len(self._data):
first_byte = ord(self._data[self._read_position]) first_byte = ord(self._data[self._read_position])
self._read_position += 1 self._read_position += 1
@ -488,17 +498,17 @@ class _MuxFramePayloadParser(object):
raise StopIteration raise StopIteration
def remaining_data(self): def remaining_data(self):
"""Returns remaining data.""" """Return remaining data."""
return self._data[self._read_position:] return self._data[self._read_position:]
class _LogicalRequest(object): class _LogicalRequest(object):
"""Mimics mod_python request.""" """Mimics mod_python request."""
def __init__(self, channel_id, command, path, protocol, headers, def __init__(self, channel_id, command, path, protocol, headers,
connection): connection):
"""Constructs an instance. """Construct an instance.
Args: Args:
channel_id: the channel id of the logical channel. channel_id: the channel id of the logical channel.
@ -507,7 +517,6 @@ class _LogicalRequest(object):
headers: HTTP headers. headers: HTTP headers.
connection: _LogicalConnection instance. connection: _LogicalConnection instance.
""" """
self.channel_id = channel_id self.channel_id = channel_id
self.method = command self.method = command
self.uri = path self.uri = path
@ -518,14 +527,16 @@ class _LogicalRequest(object):
self.client_terminated = False self.client_terminated = False
def is_https(self): def is_https(self):
"""Mimics request.is_https(). Returns False because this method is """Mimic request.is_https().
used only by old protocols (hixie and hybi00).
"""
Returns False because this method is used only by old protocols
(hixie and hybi00).
"""
return False return False
class _LogicalConnection(object): class _LogicalConnection(object):
"""Mimics mod_python mp_conn.""" """Mimics mod_python mp_conn."""
# For details, see the comment of set_read_state(). # For details, see the comment of set_read_state().
@ -534,13 +545,12 @@ class _LogicalConnection(object):
STATE_TERMINATED = 3 STATE_TERMINATED = 3
def __init__(self, mux_handler, channel_id): def __init__(self, mux_handler, channel_id):
"""Constructs an instance. """Construct an instance.
Args: Args:
mux_handler: _MuxHandler instance. mux_handler: _MuxHandler instance.
channel_id: channel id of this connection. channel_id: channel id of this connection.
""" """
self._mux_handler = mux_handler self._mux_handler = mux_handler
self._channel_id = channel_id self._channel_id = channel_id
self._incoming_data = '' self._incoming_data = ''
@ -555,25 +565,23 @@ class _LogicalConnection(object):
def get_local_addr(self): def get_local_addr(self):
"""Getter to mimic mp_conn.local_addr.""" """Getter to mimic mp_conn.local_addr."""
return self._mux_handler.physical_connection.get_local_addr() return self._mux_handler.physical_connection.get_local_addr()
local_addr = property(get_local_addr) local_addr = property(get_local_addr)
def get_remote_addr(self): def get_remote_addr(self):
"""Getter to mimic mp_conn.remote_addr.""" """Getter to mimic mp_conn.remote_addr."""
return self._mux_handler.physical_connection.get_remote_addr() return self._mux_handler.physical_connection.get_remote_addr()
remote_addr = property(get_remote_addr) remote_addr = property(get_remote_addr)
def get_memorized_lines(self): def get_memorized_lines(self):
"""Gets memorized lines. Not supported.""" """Get memorized lines. Not supported."""
raise MuxUnexpectedException('_LogicalConnection does not support ' raise MuxUnexpectedException('_LogicalConnection does not support '
'get_memorized_lines') 'get_memorized_lines')
def write(self, data): def write(self, data):
"""Writes data. mux_handler sends data asynchronously. The caller will """Write data. mux_handler sends data asynchronously.
be suspended until write done.
The caller will be suspended until write done.
Args: Args:
data: data to be written. data: data to be written.
@ -582,7 +590,6 @@ class _LogicalConnection(object):
MuxUnexpectedException: when called before finishing the previous MuxUnexpectedException: when called before finishing the previous
write. write.
""" """
try: try:
self._write_condition.acquire() self._write_condition.acquire()
if self._waiting_write_completion: if self._waiting_write_completion:
@ -598,18 +605,18 @@ class _LogicalConnection(object):
self._write_condition.release() self._write_condition.release()
def write_control_data(self, data): def write_control_data(self, data):
"""Writes data via the control channel. Don't wait finishing write """Write data via the control channel.
because this method can be called by mux dispatcher.
Don't wait finishing write because this method can be called by
mux dispatcher.
Args: Args:
data: data to be written. data: data to be written.
""" """
self._mux_handler.send_control_data(data) self._mux_handler.send_control_data(data)
def on_write_data_done(self): def on_write_data_done(self):
"""Called when sending data is completed.""" """Called when sending data is completed."""
try: try:
self._write_condition.acquire() self._write_condition.acquire()
if not self._waiting_write_completion: if not self._waiting_write_completion:
@ -623,7 +630,6 @@ class _LogicalConnection(object):
def on_writer_done(self): def on_writer_done(self):
"""Called by the mux handler when the writer thread has finished.""" """Called by the mux handler when the writer thread has finished."""
try: try:
self._write_condition.acquire() self._write_condition.acquire()
self._waiting_write_completion = False self._waiting_write_completion = False
@ -631,23 +637,24 @@ class _LogicalConnection(object):
finally: finally:
self._write_condition.release() self._write_condition.release()
def append_frame_data(self, frame_data): def append_frame_data(self, frame_data):
"""Appends incoming frame data. Called when mux_handler dispatches """Append incoming frame data.
frame data to the corresponding application.
Called when mux_handler dispatches frame data to the corresponding
application.
Args: Args:
frame_data: incoming frame data. frame_data: incoming frame data.
""" """
self._read_condition.acquire() self._read_condition.acquire()
self._incoming_data += frame_data self._incoming_data += frame_data
self._read_condition.notify() self._read_condition.notify()
self._read_condition.release() self._read_condition.release()
def read(self, length): def read(self, length):
"""Reads data. Blocks until enough data has arrived via physical """Read data.
connection.
Blocks until enough data has arrived via physical connection.
Args: Args:
length: length of data to be read. length: length of data to be read.
@ -657,7 +664,6 @@ class _LogicalConnection(object):
ConnectionTerminatedException: when the physical connection has ConnectionTerminatedException: when the physical connection has
closed, or an error is caused on the reader thread. closed, or an error is caused on the reader thread.
""" """
self._read_condition.acquire() self._read_condition.acquire()
while (self._read_state == self.STATE_ACTIVE and while (self._read_state == self.STATE_ACTIVE and
len(self._incoming_data) < length): len(self._incoming_data) < length):
@ -680,9 +686,9 @@ class _LogicalConnection(object):
return value return value
def set_read_state(self, new_state): def set_read_state(self, new_state):
"""Sets the state of this connection. Called when an event for this """Set the state of this connection.
connection has occurred.
Called when an event for this connection has occurred.
Args: Args:
new_state: state to be set. new_state must be one of followings: new_state: state to be set. new_state must be one of followings:
- STATE_GRACEFULLY_CLOSED: when closing handshake for this - STATE_GRACEFULLY_CLOSED: when closing handshake for this
@ -690,7 +696,6 @@ class _LogicalConnection(object):
- STATE_TERMINATED: when the physical connection has closed or - STATE_TERMINATED: when the physical connection has closed or
DropChannel of this connection has received. DropChannel of this connection has received.
""" """
self._read_condition.acquire() self._read_condition.acquire()
self._read_state = new_state self._read_state = new_state
self._read_condition.notify() self._read_condition.notify()
@ -698,8 +703,8 @@ class _LogicalConnection(object):
class _InnerMessage(object): class _InnerMessage(object):
"""Holds the result of _InnerMessageBuilder.build().
""" """Hold the result of _InnerMessageBuilder.build()."""
def __init__(self, opcode, payload): def __init__(self, opcode, payload):
self.opcode = opcode self.opcode = opcode
@ -707,7 +712,10 @@ class _InnerMessage(object):
class _InnerMessageBuilder(object): class _InnerMessageBuilder(object):
"""A class that holds the context of inner message fragmentation and
"""Class to build an _InnerMessage.
A class that holds the context of inner message fragmentation and
builds a message from fragmented inner frame(s). builds a message from fragmented inner frame(s).
""" """
@ -791,8 +799,10 @@ class _InnerMessageBuilder(object):
return _InnerMessage(opcode, payload) return _InnerMessage(opcode, payload)
def build(self, frame): def build(self, frame):
"""Build an inner message. Returns an _InnerMessage instance when """Build an inner message.
the given frame is the last fragmented frame. Returns None otherwise.
Returns an _InnerMessage instance when the given frame is the last
fragmented frame. Returns None otherwise.
Args: Args:
frame: an inner frame. frame: an inner frame.
@ -801,17 +811,18 @@ class _InnerMessageBuilder(object):
receiving non continuation data opcode but the fin flag of receiving non continuation data opcode but the fin flag of
the previous inner frame was not set.) the previous inner frame was not set.)
""" """
return self._frame_handler(frame) return self._frame_handler(frame)
class _LogicalStream(Stream): class _LogicalStream(Stream):
"""Mimics the Stream class. This class interprets multiplexed WebSocket
frames. """Mimics the Stream class.
This class interprets multiplexed WebSocket frames.
""" """
def __init__(self, request, stream_options, send_quota, receive_quota): def __init__(self, request, stream_options, send_quota, receive_quota):
"""Constructs an instance. """Construct an instance.
Args: Args:
request: _LogicalRequest instance. request: _LogicalRequest instance.
@ -819,7 +830,6 @@ class _LogicalStream(Stream):
send_quota: Initial send quota. send_quota: Initial send quota.
receive_quota: Initial receive quota. receive_quota: Initial receive quota.
""" """
# Physical stream is responsible for masking. # Physical stream is responsible for masking.
stream_options.unmask_receive = False stream_options.unmask_receive = False
Stream.__init__(self, request, stream_options) Stream.__init__(self, request, stream_options)
@ -928,7 +938,6 @@ class _LogicalStream(Stream):
def replenish_send_quota(self, send_quota): def replenish_send_quota(self, send_quota):
"""Replenish send quota.""" """Replenish send quota."""
try: try:
self._send_condition.acquire() self._send_condition.acquire()
if self._send_quota + send_quota > 0x7FFFFFFFFFFFFFFF: if self._send_quota + send_quota > 0x7FFFFFFFFFFFFFFF:
@ -943,8 +952,7 @@ class _LogicalStream(Stream):
self._send_condition.release() self._send_condition.release()
def consume_receive_quota(self, amount): def consume_receive_quota(self, amount):
"""Consumes receive quota. Returns False on failure.""" """Consume receive quota. Returns False on failure."""
if self._receive_quota < amount: if self._receive_quota < amount:
self._logger.debug('Violate quota on channel id %d: %d < %d' % self._logger.debug('Violate quota on channel id %d: %d < %d' %
(self._request.channel_id, (self._request.channel_id,
@ -955,7 +963,6 @@ class _LogicalStream(Stream):
def send_message(self, message, end=True, binary=False): def send_message(self, message, end=True, binary=False):
"""Override Stream.send_message.""" """Override Stream.send_message."""
if self._request.server_terminated: if self._request.server_terminated:
raise BadOperationException( raise BadOperationException(
'Requested send_message after sending out a closing handshake') 'Requested send_message after sending out a closing handshake')
@ -985,14 +992,13 @@ class _LogicalStream(Stream):
self._last_message_was_fragmented = not end self._last_message_was_fragmented = not end
def _receive_frame(self): def _receive_frame(self):
"""Overrides Stream._receive_frame. """Override Stream._receive_frame.
In addition to call Stream._receive_frame, this method adds the amount In addition to call Stream._receive_frame, this method adds the amount
of payload to receiving quota and sends FlowControl to the client. of payload to receiving quota and sends FlowControl to the client.
We need to do it here because Stream.receive_message() handles We need to do it here because Stream.receive_message() handles
control frames internally. control frames internally.
""" """
opcode, payload, fin, rsv1, rsv2, rsv3 = Stream._receive_frame(self) opcode, payload, fin, rsv1, rsv2, rsv3 = Stream._receive_frame(self)
amount = len(payload) amount = len(payload)
# Replenish extra one octet when receiving the first fragmented frame. # Replenish extra one octet when receiving the first fragmented frame.
@ -1007,9 +1013,7 @@ class _LogicalStream(Stream):
return opcode, payload, fin, rsv1, rsv2, rsv3 return opcode, payload, fin, rsv1, rsv2, rsv3
def _get_message_from_frame(self, frame): def _get_message_from_frame(self, frame):
"""Overrides Stream._get_message_from_frame. """Override Stream._get_message_from_frame."""
"""
try: try:
inner_message = self._inner_message_builder.build(frame) inner_message = self._inner_message_builder.build(frame)
except InvalidFrameException: except InvalidFrameException:
@ -1022,8 +1026,7 @@ class _LogicalStream(Stream):
return inner_message.payload return inner_message.payload
def receive_message(self): def receive_message(self):
"""Overrides Stream.receive_message.""" """Override Stream.receive_message."""
# Just call Stream.receive_message(), but catch # Just call Stream.receive_message(), but catch
# LogicalConnectionClosedException, which is raised when the logical # LogicalConnectionClosedException, which is raised when the logical
# connection has closed gracefully. # connection has closed gracefully.
@ -1034,8 +1037,7 @@ class _LogicalStream(Stream):
return None return None
def _send_closing_handshake(self, code, reason): def _send_closing_handshake(self, code, reason):
"""Overrides Stream._send_closing_handshake.""" """Override Stream._send_closing_handshake."""
body = create_closing_handshake_body(code, reason) body = create_closing_handshake_body(code, reason)
self._logger.debug('Sending closing handshake for %d: (%r, %r)' % self._logger.debug('Sending closing handshake for %d: (%r, %r)' %
(self._request.channel_id, code, reason)) (self._request.channel_id, code, reason))
@ -1044,8 +1046,7 @@ class _LogicalStream(Stream):
self._request.server_terminated = True self._request.server_terminated = True
def send_ping(self, body=''): def send_ping(self, body=''):
"""Overrides Stream.send_ping""" """Override Stream.send_ping."""
self._logger.debug('Sending ping on logical channel %d: %r' % self._logger.debug('Sending ping on logical channel %d: %r' %
(self._request.channel_id, body)) (self._request.channel_id, body))
self._write_inner_frame(common.OPCODE_PING, body, end=True) self._write_inner_frame(common.OPCODE_PING, body, end=True)
@ -1053,23 +1054,20 @@ class _LogicalStream(Stream):
self._ping_queue.append(body) self._ping_queue.append(body)
def _send_pong(self, body): def _send_pong(self, body):
"""Overrides Stream._send_pong""" """Override Stream._send_pong."""
self._logger.debug('Sending pong on logical channel %d: %r' % self._logger.debug('Sending pong on logical channel %d: %r' %
(self._request.channel_id, body)) (self._request.channel_id, body))
self._write_inner_frame(common.OPCODE_PONG, body, end=True) self._write_inner_frame(common.OPCODE_PONG, body, end=True)
def close_connection(self, code=common.STATUS_NORMAL_CLOSURE, reason=''): def close_connection(self, code=common.STATUS_NORMAL_CLOSURE, reason=''):
"""Overrides Stream.close_connection.""" """Override Stream.close_connection."""
# TODO(bashi): Implement # TODO(bashi): Implement
self._logger.debug('Closing logical connection %d' % self._logger.debug('Closing logical connection %d' %
self._request.channel_id) self._request.channel_id)
self._request.server_terminated = True self._request.server_terminated = True
def stop_sending(self): def stop_sending(self):
"""Stops accepting new send operation (_write_inner_frame).""" """Stop accepting new send operation (_write_inner_frame)."""
self._send_condition.acquire() self._send_condition.acquire()
self._send_closed = True self._send_closed = True
self._send_condition.notify() self._send_condition.notify()
@ -1077,7 +1075,10 @@ class _LogicalStream(Stream):
class _OutgoingData(object): class _OutgoingData(object):
"""A structure that holds data to be sent via physical connection and
"""Simple data/channel container.
A structure that holds data to be sent via physical connection and
origin of the data. origin of the data.
""" """
@ -1087,6 +1088,7 @@ class _OutgoingData(object):
class _PhysicalConnectionWriter(threading.Thread): class _PhysicalConnectionWriter(threading.Thread):
"""A thread that is responsible for writing data to physical connection. """A thread that is responsible for writing data to physical connection.
TODO(bashi): Make sure there is no thread-safety problem when the reader TODO(bashi): Make sure there is no thread-safety problem when the reader
@ -1094,12 +1096,11 @@ class _PhysicalConnectionWriter(threading.Thread):
""" """
def __init__(self, mux_handler): def __init__(self, mux_handler):
"""Constructs an instance. """Construct an instance.
Args: Args:
mux_handler: _MuxHandler instance. mux_handler: _MuxHandler instance.
""" """
threading.Thread.__init__(self) threading.Thread.__init__(self)
self._logger = util.get_class_logger(self) self._logger = util.get_class_logger(self)
self._mux_handler = mux_handler self._mux_handler = mux_handler
@ -1118,16 +1119,14 @@ class _PhysicalConnectionWriter(threading.Thread):
self._deque_condition = threading.Condition() self._deque_condition = threading.Condition()
def put_outgoing_data(self, data): def put_outgoing_data(self, data):
"""Puts outgoing data. """Put outgoing data.
Args: Args:
data: _OutgoingData instance. data: _OutgoingData instance.
Raises: Raises:
BadOperationException: when the thread has been requested to BadOperationException: when the thread has been requested to
terminate. terminate.
""" """
try: try:
self._deque_condition.acquire() self._deque_condition.acquire()
if self._stop_requested: if self._stop_requested:
@ -1193,8 +1192,7 @@ class _PhysicalConnectionWriter(threading.Thread):
self._mux_handler.notify_writer_done() self._mux_handler.notify_writer_done()
def stop(self, close_code=common.STATUS_NORMAL_CLOSURE): def stop(self, close_code=common.STATUS_NORMAL_CLOSURE):
"""Stops the writer thread.""" """Stop the writer thread."""
self._deque_condition.acquire() self._deque_condition.acquire()
self._stop_requested = True self._stop_requested = True
self._close_code = close_code self._close_code = close_code
@ -1203,16 +1201,16 @@ class _PhysicalConnectionWriter(threading.Thread):
class _PhysicalConnectionReader(threading.Thread): class _PhysicalConnectionReader(threading.Thread):
"""A thread that is responsible for reading data from physical connection. """A thread that is responsible for reading data from physical connection.
""" """
def __init__(self, mux_handler): def __init__(self, mux_handler):
"""Constructs an instance. """Construct an instance.
Args: Args:
mux_handler: _MuxHandler instance. mux_handler: _MuxHandler instance.
""" """
threading.Thread.__init__(self) threading.Thread.__init__(self)
self._logger = util.get_class_logger(self) self._logger = util.get_class_logger(self)
self._mux_handler = mux_handler self._mux_handler = mux_handler
@ -1254,18 +1252,18 @@ class _PhysicalConnectionReader(threading.Thread):
class _Worker(threading.Thread): class _Worker(threading.Thread):
"""A thread that is responsible for running the corresponding application """A thread that is responsible for running the corresponding application
handler. handler.
""" """
def __init__(self, mux_handler, request): def __init__(self, mux_handler, request):
"""Constructs an instance. """Construct an instance.
Args: Args:
mux_handler: _MuxHandler instance. mux_handler: _MuxHandler instance.
request: _LogicalRequest instance. request: _LogicalRequest instance.
""" """
threading.Thread.__init__(self) threading.Thread.__init__(self)
self._logger = util.get_class_logger(self) self._logger = util.get_class_logger(self)
self._mux_handler = mux_handler self._mux_handler = mux_handler
@ -1286,19 +1284,20 @@ class _Worker(threading.Thread):
class _MuxHandshaker(hybi.Handshaker): class _MuxHandshaker(hybi.Handshaker):
"""Opening handshake processor for multiplexing.""" """Opening handshake processor for multiplexing."""
_DUMMY_WEBSOCKET_KEY = 'dGhlIHNhbXBsZSBub25jZQ==' _DUMMY_WEBSOCKET_KEY = 'dGhlIHNhbXBsZSBub25jZQ=='
def __init__(self, request, dispatcher, send_quota, receive_quota): def __init__(self, request, dispatcher, send_quota, receive_quota):
"""Constructs an instance. """Construct an instance.
Args: Args:
request: _LogicalRequest instance. request: _LogicalRequest instance.
dispatcher: Dispatcher instance (dispatch.Dispatcher). dispatcher: Dispatcher instance (dispatch.Dispatcher).
send_quota: Initial send quota. send_quota: Initial send quota.
receive_quota: Initial receive quota. receive_quota: Initial receive quota.
""" """
hybi.Handshaker.__init__(self, request, dispatcher) hybi.Handshaker.__init__(self, request, dispatcher)
self._send_quota = send_quota self._send_quota = send_quota
self._receive_quota = receive_quota self._receive_quota = receive_quota
@ -1316,7 +1315,6 @@ class _MuxHandshaker(hybi.Handshaker):
def _create_stream(self, stream_options): def _create_stream(self, stream_options):
"""Override hybi.Handshaker._create_stream.""" """Override hybi.Handshaker._create_stream."""
self._logger.debug('Creating logical stream for %d' % self._logger.debug('Creating logical stream for %d' %
self._request.channel_id) self._request.channel_id)
return _LogicalStream( return _LogicalStream(
@ -1325,7 +1323,6 @@ class _MuxHandshaker(hybi.Handshaker):
def _create_handshake_response(self, accept): def _create_handshake_response(self, accept):
"""Override hybi._create_handshake_response.""" """Override hybi._create_handshake_response."""
response = [] response = []
response.append('HTTP/1.1 101 Switching Protocols\r\n') response.append('HTTP/1.1 101 Switching Protocols\r\n')
@ -1348,7 +1345,6 @@ class _MuxHandshaker(hybi.Handshaker):
def _send_handshake(self, accept): def _send_handshake(self, accept):
"""Override hybi.Handshaker._send_handshake.""" """Override hybi.Handshaker._send_handshake."""
# Don't send handshake response for the default channel # Don't send handshake response for the default channel
if self._request.channel_id == _DEFAULT_CHANNEL_ID: if self._request.channel_id == _DEFAULT_CHANNEL_ID:
return return
@ -1363,8 +1359,8 @@ class _MuxHandshaker(hybi.Handshaker):
class _LogicalChannelData(object): class _LogicalChannelData(object):
"""A structure that holds information about logical channel.
""" """A structure that holds information about logical channel."""
def __init__(self, request, worker): def __init__(self, request, worker):
self.request = request self.request = request

Some files were not shown because too many files have changed in this diff Show more